@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Proxy the "git upload-pack" wire protocol

Summary:
Depends on D20380. Ref T8093. When prototypes are enabled, inject a (hopefully?) no-op proxy into the Git wire protocol.

This proxy decodes "git upload-pack" and allows the list of references to be rewritten, in a similar way to how we already proxy the Subversion protocol to rewrite URIs and proxy the Mercurial protocol to distinguish between read and write operations.

The piece we care about comes at the beginning, and looks like this:

```
<frame-length><ref-hash> <ref-name>\0<server-capabilities>\n
<frame-length><ref-hash> <ref-name>\n
<frame-length><ref-hash> <ref-name>\n
...
<0000>
```

We can add, remove, or modify this section to make it appear that the server has different refs than the refs that exist on disk.

Things I have tried:

- `git ls-remote`
- `git ls-remote` where the server hides some refs.
- `git fetch` where the fetch is a no-op.

Things I have not tried:

- `git fetch` where the fetch is not a no-op.
- Tricking things into doing protocol v2. Or: I tried this, I wasn't successful. In v2, additional "\0" tricks are used to hide data in the capabilities, I think?
- `git ls-remote` where we rewrite/hide the first ref in the list, and need to move the capabilities frame elsewhere.
- `git ls-remote` where the server has no refs at all, or we remove every ref.

So the "interesting" piece of this works, but it almost certainly needs some cleanup to survive interaction with the real world.

Test Plan: See above.

Reviewers: amckinley

Reviewed By: amckinley

Maniphest Tasks: T8093

Differential Revision: https://secure.phabricator.com/D20381

+412 -2
+4
src/__phutil_library_map__.php
··· 813 813 'DiffusionGitResponse' => 'applications/diffusion/response/DiffusionGitResponse.php', 814 814 'DiffusionGitSSHWorkflow' => 'applications/diffusion/ssh/DiffusionGitSSHWorkflow.php', 815 815 'DiffusionGitUploadPackSSHWorkflow' => 'applications/diffusion/ssh/DiffusionGitUploadPackSSHWorkflow.php', 816 + 'DiffusionGitUploadPackWireProtocol' => 'applications/diffusion/protocol/DiffusionGitUploadPackWireProtocol.php', 817 + 'DiffusionGitWireProtocol' => 'applications/diffusion/protocol/DiffusionGitWireProtocol.php', 816 818 'DiffusionGraphController' => 'applications/diffusion/controller/DiffusionGraphController.php', 817 819 'DiffusionHistoryController' => 'applications/diffusion/controller/DiffusionHistoryController.php', 818 820 'DiffusionHistoryListView' => 'applications/diffusion/view/DiffusionHistoryListView.php', ··· 6460 6462 'DiffusionRepositoryClusterEngineLogInterface', 6461 6463 ), 6462 6464 'DiffusionGitUploadPackSSHWorkflow' => 'DiffusionGitSSHWorkflow', 6465 + 'DiffusionGitUploadPackWireProtocol' => 'DiffusionGitWireProtocol', 6466 + 'DiffusionGitWireProtocol' => 'Phobject', 6463 6467 'DiffusionGraphController' => 'DiffusionController', 6464 6468 'DiffusionHistoryController' => 'DiffusionController', 6465 6469 'DiffusionHistoryListView' => 'DiffusionHistoryView',
+335
src/applications/diffusion/protocol/DiffusionGitUploadPackWireProtocol.php
··· 1 + <?php 2 + 3 + final class DiffusionGitUploadPackWireProtocol 4 + extends DiffusionGitWireProtocol { 5 + 6 + private $readMode = 'length'; 7 + private $readBuffer; 8 + private $readFrameLength; 9 + private $readFrames = array(); 10 + 11 + private $readFrameMode = 'refs'; 12 + private $refFrames = array(); 13 + 14 + private $readMessages = array(); 15 + 16 + public function willReadBytes($bytes) { 17 + if ($this->readBuffer === null) { 18 + $this->readBuffer = new PhutilRope(); 19 + } 20 + $buffer = $this->readBuffer; 21 + 22 + $buffer->append($bytes); 23 + 24 + while (true) { 25 + $len = $buffer->getByteLength(); 26 + switch ($this->readMode) { 27 + case 'length': 28 + // We're expecting 4 bytes containing the length of the protocol 29 + // frame as hexadecimal in ASCII text, like "01ab". Wait until we 30 + // see at least 4 bytes on the wire. 31 + if ($len < 4) { 32 + if ($len > 0) { 33 + $bytes = $this->peekBytes($len); 34 + if (!preg_match('/^[0-9a-f]+\z/', $bytes)) { 35 + throw new Exception( 36 + pht( 37 + 'Bad frame length character in Git protocol ("%s"), '. 38 + 'expected a 4-digit hexadecimal value encoded as ASCII '. 39 + 'text.', 40 + $bytes)); 41 + } 42 + } 43 + 44 + // We can't make any more progress until we get enough bytes, so 45 + // we're done with state processing. 46 + break 2; 47 + } 48 + 49 + $frame_length = $this->readBytes(4); 50 + $frame_length = hexdec($frame_length); 51 + 52 + // Note that the frame length includes the 4 header bytes, so we 53 + // usually expect a length of 5 or larger. Frames with length 0 54 + // are boundaries. 55 + if ($frame_length === 0) { 56 + $this->readFrames[] = $this->newProtocolFrame('null', ''); 57 + } else if ($frame_length >= 1 && $frame_length <= 3) { 58 + throw new Exception( 59 + pht( 60 + 'Encountered Git protocol frame with unexpected frame '. 61 + 'length (%s)!', 62 + $frame_length)); 63 + } else { 64 + $this->readFrameLength = $frame_length - 4; 65 + $this->readMode = 'frame'; 66 + } 67 + 68 + break; 69 + case 'frame': 70 + // We're expecting a protocol frame of a specified length. Note that 71 + // it is possible for a frame to have length 0. 72 + 73 + // We don't have enough bytes yet, so wait for more. 74 + if ($len < $this->readFrameLength) { 75 + break 2; 76 + } 77 + 78 + if ($this->readFrameLength > 0) { 79 + $bytes = $this->readBytes($this->readFrameLength); 80 + } else { 81 + $bytes = ''; 82 + } 83 + 84 + // Emit a protocol frame. 85 + $this->readFrames[] = $this->newProtocolFrame('data', $bytes); 86 + $this->readMode = 'length'; 87 + break; 88 + } 89 + } 90 + 91 + while (true) { 92 + switch ($this->readFrameMode) { 93 + case 'refs': 94 + if (!$this->readFrames) { 95 + break 2; 96 + } 97 + 98 + foreach ($this->readFrames as $key => $frame) { 99 + unset($this->readFrames[$key]); 100 + 101 + if ($frame['type'] === 'null') { 102 + $ref_frames = $this->refFrames; 103 + $this->refFrames = array(); 104 + 105 + $ref_frames[] = $frame; 106 + 107 + $this->readMessages[] = $this->newProtocolRefMessage($ref_frames); 108 + $this->readFrameMode = 'passthru'; 109 + break; 110 + } else { 111 + $this->refFrames[] = $frame; 112 + } 113 + } 114 + 115 + break; 116 + case 'passthru': 117 + if (!$this->readFrames) { 118 + break 2; 119 + } 120 + 121 + $this->readMessages[] = $this->newProtocolDataMessage( 122 + $this->readFrames); 123 + $this->readFrames = array(); 124 + 125 + break; 126 + } 127 + } 128 + 129 + $wire = array(); 130 + foreach ($this->readMessages as $key => $message) { 131 + $wire[] = $message; 132 + unset($this->readMessages[$key]); 133 + } 134 + $wire = implode('', $wire); 135 + 136 + return $wire; 137 + } 138 + 139 + public function willWriteBytes($bytes) { 140 + return $bytes; 141 + } 142 + 143 + private function readBytes($count) { 144 + $buffer = $this->readBuffer; 145 + 146 + $bytes = $buffer->getPrefixBytes($count); 147 + $buffer->removeBytesFromHead($count); 148 + 149 + return $bytes; 150 + } 151 + 152 + private function peekBytes($count) { 153 + $buffer = $this->readBuffer; 154 + return $buffer->getPrefixBytes($count); 155 + } 156 + 157 + private function newProtocolFrame($type, $bytes) { 158 + return array( 159 + 'type' => $type, 160 + 'length' => strlen($bytes), 161 + 'bytes' => $bytes, 162 + ); 163 + } 164 + 165 + private function newProtocolRefMessage(array $frames) { 166 + $head_key = head_key($frames); 167 + $last_key = last_key($frames); 168 + 169 + $output = array(); 170 + foreach ($frames as $key => $frame) { 171 + $is_last = ($key === $last_key); 172 + if ($is_last) { 173 + $output[] = $frame; 174 + // This is a "0000" frame at the end of the list of refs, so we pass 175 + // it through unmodified. 176 + continue; 177 + } 178 + 179 + $is_first = ($key === $head_key); 180 + 181 + // Otherwise, we expect a list of: 182 + // 183 + // <hash> <ref-name>\0<capabilities> 184 + // <hash> <ref-name> 185 + // ... 186 + 187 + $bytes = $frame['bytes']; 188 + $matches = array(); 189 + if ($is_first) { 190 + $ok = preg_match( 191 + '('. 192 + '^'. 193 + '(?P<hash>[0-9a-f]{40})'. 194 + ' '. 195 + '(?P<name>[^\0\n]+)'. 196 + '\0'. 197 + '(?P<capabilities>[^\n]+)'. 198 + '\n'. 199 + '\z'. 200 + ')', 201 + $bytes, 202 + $matches); 203 + if (!$ok) { 204 + throw new Exception( 205 + pht( 206 + 'Unexpected "git upload-pack" initial protocol frame: expected '. 207 + '"<hash> <name>\0<capabilities>\n", got "%s".', 208 + $bytes)); 209 + } 210 + } else { 211 + $ok = preg_match( 212 + '('. 213 + '^'. 214 + '(?P<hash>[0-9a-f]{40})'. 215 + ' '. 216 + '(?P<name>[^\0\n]+)'. 217 + '\n'. 218 + '\z'. 219 + ')', 220 + $bytes, 221 + $matches); 222 + if (!$ok) { 223 + throw new Exception( 224 + pht( 225 + 'Unexpected "git upload-pack" protocol frame: expected '. 226 + '"<hash> <name>\n", got "%s".', 227 + $bytes)); 228 + } 229 + } 230 + 231 + $hash = $matches['hash']; 232 + $name = $matches['name']; 233 + $capabilities = idx($matches, 'capabilities'); 234 + 235 + $ref = array( 236 + 'hash' => $hash, 237 + 'name' => $name, 238 + 'capabilities' => $capabilities, 239 + ); 240 + 241 + $old_ref = $ref; 242 + 243 + $ref = $this->willReadRef($ref); 244 + 245 + $new_ref = $ref; 246 + 247 + $this->didRewriteRef($old_ref, $new_ref); 248 + 249 + if ($ref === null) { 250 + continue; 251 + } 252 + 253 + if (isset($ref['capabilities'])) { 254 + $result = sprintf( 255 + "%s %s\0%s\n", 256 + $ref['hash'], 257 + $ref['name'], 258 + $ref['capabilities']); 259 + } else { 260 + $result = sprintf( 261 + "%s %s\n", 262 + $ref['hash'], 263 + $ref['name']); 264 + } 265 + 266 + $output[] = $this->newProtocolFrame('data', $result); 267 + } 268 + 269 + return $this->newProtocolDataMessage($output); 270 + } 271 + 272 + private function newProtocolDataMessage(array $frames) { 273 + $message = array(); 274 + 275 + foreach ($frames as $frame) { 276 + switch ($frame['type']) { 277 + case 'null': 278 + $message[] = '0000'; 279 + break; 280 + case 'data': 281 + $message[] = sprintf( 282 + '%04x%s', 283 + $frame['length'] + 4, 284 + $frame['bytes']); 285 + break; 286 + } 287 + } 288 + 289 + $message = implode('', $message); 290 + 291 + return $message; 292 + } 293 + 294 + private function willReadRef(array $ref) { 295 + return $ref; 296 + } 297 + 298 + private function didRewriteRef($old_ref, $new_ref) { 299 + $log = $this->getProtocolLog(); 300 + if (!$log) { 301 + return; 302 + } 303 + 304 + if (!$old_ref) { 305 + $old_name = null; 306 + } else { 307 + $old_name = $old_ref['name']; 308 + } 309 + 310 + if (!$new_ref) { 311 + $new_name = null; 312 + } else { 313 + $new_name = $new_ref['name']; 314 + } 315 + 316 + if ($old_name === $new_name) { 317 + return; 318 + } 319 + 320 + if ($old_name === null) { 321 + $old_name = '<null>'; 322 + } 323 + 324 + if ($new_name === null) { 325 + $new_name = '<null>'; 326 + } 327 + 328 + $log->didWriteFrame( 329 + pht( 330 + 'Rewrite Ref: %s -> %s', 331 + $old_name, 332 + $new_name)); 333 + } 334 + 335 + }
+19
src/applications/diffusion/protocol/DiffusionGitWireProtocol.php
··· 1 + <?php 2 + 3 + abstract class DiffusionGitWireProtocol extends Phobject { 4 + 5 + private $protocolLog; 6 + 7 + final public function setProtocolLog(PhabricatorProtocolLog $protocol_log) { 8 + $this->protocolLog = $protocol_log; 9 + return $this; 10 + } 11 + 12 + final public function getProtocolLog() { 13 + return $this->protocolLog; 14 + } 15 + 16 + abstract public function willReadBytes($bytes); 17 + abstract public function willWriteBytes($bytes); 18 + 19 + }
+24 -2
src/applications/diffusion/ssh/DiffusionGitSSHWorkflow.php
··· 7 7 private $engineLogProperties = array(); 8 8 private $protocolLog; 9 9 10 + private $wireProtocol; 11 + 10 12 protected function writeError($message) { 11 13 // Git assumes we'll add our own newlines. 12 14 return parent::writeError($message."\n"); ··· 74 76 return null; 75 77 } 76 78 77 - protected function getProtocolLog() { 79 + final protected function getProtocolLog() { 78 80 return $this->protocolLog; 79 81 } 80 82 81 - protected function setProtocolLog(PhabricatorProtocolLog $log) { 83 + final protected function setProtocolLog(PhabricatorProtocolLog $log) { 82 84 $this->protocolLog = $log; 83 85 } 84 86 87 + final protected function getWireProtocol() { 88 + return $this->wireProtocol; 89 + } 90 + 91 + final protected function setWireProtocol( 92 + DiffusionGitWireProtocol $protocol) { 93 + $this->wireProtocol = $protocol; 94 + return $this; 95 + } 96 + 85 97 public function willWriteMessageCallback( 86 98 PhabricatorSSHPassthruCommand $command, 87 99 $message) { ··· 91 103 $log->didWriteBytes($message); 92 104 } 93 105 106 + $protocol = $this->getWireProtocol(); 107 + if ($protocol) { 108 + $message = $protocol->willWriteBytes($message); 109 + } 110 + 94 111 return $message; 95 112 } 96 113 ··· 101 118 $log = $this->getProtocolLog(); 102 119 if ($log) { 103 120 $log->didReadBytes($message); 121 + } 122 + 123 + $protocol = $this->getWireProtocol(); 124 + if ($protocol) { 125 + $message = $protocol->willReadBytes($message); 104 126 } 105 127 106 128 return $message;
+10
src/applications/diffusion/ssh/DiffusionGitUploadPackSSHWorkflow.php
··· 60 60 $log->didStartSession($command); 61 61 } 62 62 63 + if (!$is_proxy) { 64 + if (PhabricatorEnv::getEnvConfig('phabricator.show-prototypes')) { 65 + $protocol = new DiffusionGitUploadPackWireProtocol(); 66 + if ($log) { 67 + $protocol->setProtocolLog($log); 68 + } 69 + $this->setWireProtocol($protocol); 70 + } 71 + } 72 + 63 73 $err = $this->newPassthruCommand() 64 74 ->setIOChannel($this->getIOChannel()) 65 75 ->setCommandChannelFromExecFuture($future)
+20
src/infrastructure/log/PhabricatorProtocolLog.php
··· 41 41 $this->buffer[] = $bytes; 42 42 } 43 43 44 + public function didReadFrame($frame) { 45 + $this->writeFrame('<*', $frame); 46 + } 47 + 48 + public function didWriteFrame($frame) { 49 + $this->writeFrame('>*', $frame); 50 + } 51 + 52 + private function writeFrame($header, $frame) { 53 + $this->flush(); 54 + 55 + $frame = explode("\n", $frame); 56 + foreach ($frame as $key => $line) { 57 + $frame[$key] = $header.' '.$this->escapeBytes($line); 58 + } 59 + $frame = implode("\n", $frame)."\n\n"; 60 + 61 + $this->writeMessage($frame); 62 + } 63 + 44 64 private function setMode($mode) { 45 65 if ($this->mode === $mode) { 46 66 return $this;