@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<?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}