Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/diffusion/ssh/DiffusionSubversionServeSSHWorkflow.php
12242 views
1
<?php
2
3
/**
4
* This protocol has a good spec here:
5
*
6
* http://svn.apache.org/repos/asf/subversion/trunk/subversion/libsvn_ra_svn/protocol
7
*/
8
final class DiffusionSubversionServeSSHWorkflow
9
extends DiffusionSubversionSSHWorkflow {
10
11
private $didSeeWrite;
12
13
private $inProtocol;
14
private $outProtocol;
15
16
private $inSeenGreeting;
17
18
private $outPhaseCount = 0;
19
20
private $internalBaseURI;
21
private $externalBaseURI;
22
private $peekBuffer;
23
private $command;
24
private $isProxying;
25
26
private function getCommand() {
27
return $this->command;
28
}
29
30
protected function didConstruct() {
31
$this->setName('svnserve');
32
$this->setArguments(
33
array(
34
array(
35
'name' => 'tunnel',
36
'short' => 't',
37
),
38
));
39
}
40
41
protected function identifyRepository() {
42
// NOTE: In SVN, we need to read the first few protocol frames before we
43
// can determine which repository the user is trying to access. We're
44
// going to peek at the data on the wire to identify the repository.
45
46
$io_channel = $this->getIOChannel();
47
48
// Before the client will send us the first protocol frame, we need to send
49
// it a connection frame with server capabilities. To figure out the
50
// correct frame we're going to start `svnserve`, read the frame from it,
51
// send it to the client, then kill the subprocess.
52
53
// TODO: This is pretty inelegant and the protocol frame will change very
54
// rarely. We could cache it if we can find a reasonable way to dirty the
55
// cache.
56
57
$command = csprintf('svnserve -t');
58
$command = PhabricatorDaemon::sudoCommandAsDaemonUser($command);
59
$future = new ExecFuture('%C', $command);
60
$exec_channel = new PhutilExecChannel($future);
61
$exec_protocol = new DiffusionSubversionWireProtocol();
62
63
while (true) {
64
PhutilChannel::waitForAny(array($exec_channel));
65
$exec_channel->update();
66
67
$exec_message = $exec_channel->read();
68
if ($exec_message !== null) {
69
$messages = $exec_protocol->writeData($exec_message);
70
if ($messages) {
71
$message = head($messages);
72
$raw = $message['raw'];
73
74
// Write the greeting frame to the client.
75
$io_channel->write($raw);
76
77
// Kill the subprocess.
78
$future->resolveKill();
79
break;
80
}
81
}
82
83
if (!$exec_channel->isOpenForReading()) {
84
throw new Exception(
85
pht(
86
'%s subprocess exited before emitting a protocol frame.',
87
'svnserve'));
88
}
89
}
90
91
$io_protocol = new DiffusionSubversionWireProtocol();
92
while (true) {
93
PhutilChannel::waitForAny(array($io_channel));
94
$io_channel->update();
95
96
$in_message = $io_channel->read();
97
if ($in_message !== null) {
98
$this->peekBuffer .= $in_message;
99
if (strlen($this->peekBuffer) > (1024 * 1024)) {
100
throw new Exception(
101
pht(
102
'Client transmitted more than 1MB of data without transmitting '.
103
'a recognizable protocol frame.'));
104
}
105
106
$messages = $io_protocol->writeData($in_message);
107
if ($messages) {
108
$message = head($messages);
109
$struct = $message['structure'];
110
111
// This is the:
112
//
113
// ( version ( cap1 ... ) url ... )
114
//
115
// The `url` allows us to identify the repository.
116
117
$uri = $struct[2]['value'];
118
$path = $this->getPathFromSubversionURI($uri);
119
120
return $this->loadRepositoryWithPath(
121
$path,
122
PhabricatorRepositoryType::REPOSITORY_TYPE_SVN);
123
}
124
}
125
126
if (!$io_channel->isOpenForReading()) {
127
throw new Exception(
128
pht(
129
'Client closed connection before sending a complete protocol '.
130
'frame.'));
131
}
132
133
// If the client has disconnected, kill the subprocess and bail.
134
if (!$io_channel->isOpenForWriting()) {
135
throw new Exception(
136
pht(
137
'Client closed connection before receiving response.'));
138
}
139
}
140
}
141
142
protected function executeRepositoryOperations() {
143
$repository = $this->getRepository();
144
145
$args = $this->getArgs();
146
if (!$args->getArg('tunnel')) {
147
throw new Exception(pht('Expected `%s`!', 'svnserve -t'));
148
}
149
150
if ($this->shouldProxy()) {
151
// NOTE: We're always requesting a writable device here. The request
152
// might be read-only, but we can't currently tell, and SVN requests
153
// can mix reads and writes.
154
$command = $this->getProxyCommand(true);
155
$this->isProxying = true;
156
$cwd = null;
157
} else {
158
$command = csprintf(
159
'svnserve -t --tunnel-user=%s',
160
$this->getSSHUser()->getUsername());
161
$cwd = PhabricatorEnv::getEmptyCWD();
162
}
163
164
$command = PhabricatorDaemon::sudoCommandAsDaemonUser($command);
165
$future = new ExecFuture('%C', $command);
166
167
// If we're receiving a commit, svnserve will fail to execute the commit
168
// hook with an unhelpful error if the CWD isn't readable by the user we
169
// are sudoing to. Switch to a readable, empty CWD before running
170
// svnserve. See T10941.
171
if ($cwd !== null) {
172
$future->setCWD($cwd);
173
}
174
175
$this->inProtocol = new DiffusionSubversionWireProtocol();
176
$this->outProtocol = new DiffusionSubversionWireProtocol();
177
178
$this->command = id($this->newPassthruCommand())
179
->setIOChannel($this->getIOChannel())
180
->setCommandChannelFromExecFuture($future)
181
->setWillWriteCallback(array($this, 'willWriteMessageCallback'))
182
->setWillReadCallback(array($this, 'willReadMessageCallback'));
183
184
$this->command->setPauseIOReads(true);
185
186
$err = $this->command->execute();
187
188
if (!$err && $this->didSeeWrite) {
189
$this->getRepository()->writeStatusMessage(
190
PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE,
191
PhabricatorRepositoryStatusMessage::CODE_OKAY);
192
}
193
194
return $err;
195
}
196
197
public function willWriteMessageCallback(
198
PhabricatorSSHPassthruCommand $command,
199
$message) {
200
201
$proto = $this->inProtocol;
202
$messages = $proto->writeData($message);
203
204
$result = array();
205
foreach ($messages as $message) {
206
$message_raw = $message['raw'];
207
$struct = $message['structure'];
208
209
if (!$this->inSeenGreeting) {
210
$this->inSeenGreeting = true;
211
212
// The first message the client sends looks like:
213
//
214
// ( version ( cap1 ... ) url ... )
215
//
216
// We want to grab the URL, load the repository, make sure it exists and
217
// is accessible, and then replace it with the location of the
218
// repository on disk.
219
220
$uri = $struct[2]['value'];
221
$struct[2]['value'] = $this->makeInternalURI($uri);
222
223
$message_raw = $proto->serializeStruct($struct);
224
} else if (isset($struct[0]) && $struct[0]['type'] == 'word') {
225
226
if (!$proto->isReadOnlyCommand($struct)) {
227
$this->didSeeWrite = true;
228
$this->requireWriteAccess($struct[0]['value']);
229
}
230
231
// Several other commands also pass in URLs. We need to translate
232
// all of these into the internal representation; this also makes sure
233
// they're valid and accessible.
234
235
switch ($struct[0]['value']) {
236
case 'reparent':
237
// ( reparent ( url ) )
238
$struct[1]['value'][0]['value'] = $this->makeInternalURI(
239
$struct[1]['value'][0]['value']);
240
$message_raw = $proto->serializeStruct($struct);
241
break;
242
case 'switch':
243
// ( switch ( ( rev ) target recurse url ... ) )
244
$struct[1]['value'][3]['value'] = $this->makeInternalURI(
245
$struct[1]['value'][3]['value']);
246
$message_raw = $proto->serializeStruct($struct);
247
break;
248
case 'diff':
249
// ( diff ( ( rev ) target recurse ignore-ancestry url ... ) )
250
$struct[1]['value'][4]['value'] = $this->makeInternalURI(
251
$struct[1]['value'][4]['value']);
252
$message_raw = $proto->serializeStruct($struct);
253
break;
254
case 'add-file':
255
case 'add-dir':
256
// ( add-file ( path dir-token file-token [ copy-path copy-rev ] ) )
257
// ( add-dir ( path parent child [ copy-path copy-rev ] ) )
258
if (isset($struct[1]['value'][3]['value'][0]['value'])) {
259
$copy_from = $struct[1]['value'][3]['value'][0]['value'];
260
$copy_from = $this->makeInternalURI($copy_from);
261
$struct[1]['value'][3]['value'][0]['value'] = $copy_from;
262
}
263
$message_raw = $proto->serializeStruct($struct);
264
break;
265
}
266
}
267
268
$result[] = $message_raw;
269
}
270
271
if (!$result) {
272
return null;
273
}
274
275
return implode('', $result);
276
}
277
278
public function willReadMessageCallback(
279
PhabricatorSSHPassthruCommand $command,
280
$message) {
281
282
$proto = $this->outProtocol;
283
$messages = $proto->writeData($message);
284
285
$result = array();
286
foreach ($messages as $message) {
287
$message_raw = $message['raw'];
288
$struct = $message['structure'];
289
290
if (isset($struct[0]) && ($struct[0]['type'] == 'word')) {
291
292
if ($struct[0]['value'] == 'success') {
293
switch ($this->outPhaseCount) {
294
case 0:
295
// This is the "greeting", which announces capabilities.
296
297
// We already sent this when we were figuring out which
298
// repository this request is for, so we aren't going to send
299
// it again.
300
301
// Instead, we're going to replay the client's response (which
302
// we also already read).
303
304
$command = $this->getCommand();
305
$command->writeIORead($this->peekBuffer);
306
$command->setPauseIOReads(false);
307
308
$message_raw = null;
309
break;
310
case 1:
311
// This responds to the client greeting, and announces auth.
312
break;
313
case 2:
314
// This responds to auth, which should be trivial over SSH.
315
break;
316
case 3:
317
// This contains the URI of the repository. We need to edit it;
318
// if it does not match what the client requested it will reject
319
// the response.
320
$struct[1]['value'][1]['value'] = $this->makeExternalURI(
321
$struct[1]['value'][1]['value']);
322
$message_raw = $proto->serializeStruct($struct);
323
break;
324
default:
325
// We don't care about other protocol frames.
326
break;
327
}
328
329
$this->outPhaseCount++;
330
} else if ($struct[0]['value'] == 'failure') {
331
// Find any error messages which include the internal URI, and
332
// replace the text with the external URI.
333
foreach ($struct[1]['value'] as $key => $error) {
334
$code = $error['value'][0]['value'];
335
$message = $error['value'][1]['value'];
336
337
$message = str_replace(
338
$this->internalBaseURI,
339
$this->externalBaseURI,
340
$message);
341
342
// Derp derp derp derp derp. The structure looks like this:
343
// ( failure ( ( code message ... ) ... ) )
344
$struct[1]['value'][$key]['value'][1]['value'] = $message;
345
}
346
$message_raw = $proto->serializeStruct($struct);
347
}
348
349
}
350
351
if ($message_raw !== null) {
352
$result[] = $message_raw;
353
}
354
}
355
356
if (!$result) {
357
return null;
358
}
359
360
return implode('', $result);
361
}
362
363
private function getPathFromSubversionURI($uri_string) {
364
$uri = new PhutilURI($uri_string);
365
366
$proto = $uri->getProtocol();
367
if ($proto !== 'svn+ssh') {
368
throw new Exception(
369
pht(
370
'Protocol for URI "%s" MUST be "%s".',
371
$uri_string,
372
'svn+ssh'));
373
}
374
$path = $uri->getPath();
375
376
// Subversion presumably deals with this, but make sure there's nothing
377
// sketchy going on with the URI.
378
if (preg_match('(/\\.\\./)', $path)) {
379
throw new Exception(
380
pht(
381
'String "%s" is invalid in path specification "%s".',
382
'/../',
383
$uri_string));
384
}
385
386
$path = $this->normalizeSVNPath($path);
387
388
return $path;
389
}
390
391
private function makeInternalURI($uri_string) {
392
if ($this->isProxying) {
393
return $uri_string;
394
}
395
396
$uri = new PhutilURI($uri_string);
397
398
$repository = $this->getRepository();
399
400
$path = $this->getPathFromSubversionURI($uri_string);
401
$external_base = $this->getBaseRequestPath();
402
403
// Replace "/diffusion/X" in the request with the repository local path,
404
// so "/diffusion/X/master/" becomes "/path/to/repository/X/master/".
405
$local_path = rtrim($repository->getLocalPath(), '/');
406
$path = $local_path.substr($path, strlen($external_base));
407
408
// NOTE: We are intentionally NOT removing username information from the
409
// URI. Subversion retains it over the course of the request and considers
410
// two repositories with different username identifiers to be distinct and
411
// incompatible.
412
413
$uri->setPath($path);
414
415
// If this is happening during the handshake, these are the base URIs for
416
// the request.
417
if ($this->externalBaseURI === null) {
418
$pre = (string)id(clone $uri)->setPath('');
419
420
$external_path = $external_base;
421
$external_path = $this->normalizeSVNPath($external_path);
422
$this->externalBaseURI = $pre.$external_path;
423
424
$internal_path = rtrim($repository->getLocalPath(), '/');
425
$internal_path = $this->normalizeSVNPath($internal_path);
426
$this->internalBaseURI = $pre.$internal_path;
427
}
428
429
return (string)$uri;
430
}
431
432
private function makeExternalURI($uri) {
433
if ($this->isProxying) {
434
return $uri;
435
}
436
437
$internal = $this->internalBaseURI;
438
$external = $this->externalBaseURI;
439
440
if (strncmp($uri, $internal, strlen($internal)) === 0) {
441
$uri = $external.substr($uri, strlen($internal));
442
}
443
444
return $uri;
445
}
446
447
private function normalizeSVNPath($path) {
448
// Subversion normalizes redundant slashes internally, so normalize them
449
// here as well to make sure things match up.
450
$path = preg_replace('(/+)', '/', $path);
451
452
return $path;
453
}
454
455
protected function raiseWrongVCSException(
456
PhabricatorRepository $repository) {
457
throw new Exception(
458
pht(
459
'This repository ("%s") is not a Subversion repository. Use "%s" to '.
460
'interact with this repository.',
461
$repository->getDisplayName(),
462
$repository->getVersionControlSystem()));
463
}
464
465
}
466
467