@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
at upstream/main 173 lines 5.2 kB view raw
1<?php 2 3/** 4 * Abstract class which wraps some sort of output mechanism for HTTP responses. 5 * Normally this is just @{class:AphrontPHPHTTPSink}, which uses "echo" and 6 * "header()" to emit responses. 7 * 8 * @task write Writing Response Components 9 * @task emit Emitting the Response 10 */ 11abstract class AphrontHTTPSink extends Phobject { 12 13 private $showStackTraces = false; 14 15 final public function setShowStackTraces($show_stack_traces) { 16 $this->showStackTraces = $show_stack_traces; 17 return $this; 18 } 19 20 final public function getShowStackTraces() { 21 return $this->showStackTraces; 22 } 23 24 25/* -( Writing Response Components )---------------------------------------- */ 26 27 28 /** 29 * Write an HTTP status code to the output. 30 * 31 * @param int $code Numeric HTTP status code. 32 * @param string $message (optional) 33 * @return void 34 */ 35 final public function writeHTTPStatus($code, $message = '') { 36 if (!preg_match('/^\d{3}$/', (string)$code)) { 37 throw new Exception(pht("Malformed HTTP status code '%s'!", $code)); 38 } 39 40 $code = (int)$code; 41 $this->emitHTTPStatus($code, $message); 42 } 43 44 45 /** 46 * Write HTTP headers to the output. 47 * 48 * @param list<array> $headers List of <name, value> pairs. 49 * @return void 50 */ 51 final public function writeHeaders(array $headers) { 52 foreach ($headers as $header) { 53 if (!is_array($header) || count($header) !== 2) { 54 throw new Exception(pht('Malformed header.')); 55 } 56 list($name, $value) = $header; 57 58 if (strpos($name, ':') !== false) { 59 throw new Exception( 60 pht( 61 'Declining to emit response with malformed HTTP header name: %s', 62 $name)); 63 } 64 65 // Attackers may perform an "HTTP response splitting" attack by making 66 // the application emit certain types of headers containing newlines: 67 // 68 // https://en.wikipedia.org/wiki/HTTP_response_splitting 69 // 70 // PHP has built-in protections against HTTP response-splitting, but they 71 // are of dubious trustworthiness: 72 // 73 // https://news-web.php.net/php.internals/57655 74 75 if (preg_match('/[\r\n\0]/', $name.$value)) { 76 throw new Exception( 77 pht( 78 'Declining to emit response with unsafe HTTP header: %s', 79 "<'".$name."', '".$value."'>.")); 80 } 81 } 82 83 foreach ($headers as $header) { 84 list($name, $value) = $header; 85 $this->emitHeader($name, $value); 86 } 87 } 88 89 90 /** 91 * Write HTTP body data to the output. 92 * 93 * @param string $data Body data. 94 * @return void 95 */ 96 final public function writeData($data) { 97 $this->emitData($data); 98 } 99 100 101 /** 102 * Write an entire @{class:AphrontResponse} to the output. 103 * 104 * @param AphrontResponse $response The response object to write. 105 * @return void 106 */ 107 final public function writeResponse(AphrontResponse $response) { 108 $response->willBeginWrite(); 109 110 // Build the content iterator first, in case it throws. Ideally, we'd 111 // prefer to handle exceptions before we emit the response status or any 112 // HTTP headers. 113 $data = $response->getContentIterator(); 114 115 // This isn't an exceptionally clean separation of concerns, but we need 116 // to add CSP headers for all response types (including both web pages 117 // and dialogs) and can't determine the correct CSP until after we render 118 // the page (because page elements like Recaptcha may add CSP rules). 119 $static = CelerityAPI::getStaticResourceResponse(); 120 foreach ($static->getContentSecurityPolicyURIMap() as $kind => $uris) { 121 foreach ($uris as $uri) { 122 $response->addContentSecurityPolicyURI($kind, $uri); 123 } 124 } 125 126 $all_headers = array_merge( 127 $response->getHeaders(), 128 $response->getCacheHeaders()); 129 130 $this->writeHTTPStatus( 131 $response->getHTTPResponseCode(), 132 $response->getHTTPResponseMessage()); 133 $this->writeHeaders($all_headers); 134 135 // Allow clients an unlimited amount of time to download the response. 136 137 // This allows clients to perform a "slow loris" attack, where they 138 // download a large response very slowly to tie up process slots. However, 139 // concurrent connection limits and "RequestReadTimeout" already prevent 140 // this attack. We could add our own minimum download rate here if we want 141 // to make this easier to configure eventually. 142 143 // For normal page responses, we've fully rendered the page into a string 144 // already so all that's left is writing it to the client. 145 146 // For unusual responses (like large file downloads) we may still be doing 147 // some meaningful work, but in theory that work is intrinsic to streaming 148 // the response. 149 150 set_time_limit(0); 151 152 $abort = false; 153 foreach ($data as $block) { 154 if (!$this->isWritable()) { 155 $abort = true; 156 break; 157 } 158 $this->writeData($block); 159 } 160 161 $response->didCompleteWrite($abort); 162 } 163 164 165/* -( Emitting the Response )---------------------------------------------- */ 166 167 168 abstract protected function emitHTTPStatus($code, $message = ''); 169 abstract protected function emitHeader($name, $value); 170 abstract protected function emitData($data); 171 abstract protected function isWritable(); 172 173}