diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index ab072b3d63..395297b957 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -48,15 +48,19 @@ phutil_register_library_map(array( 'AphrontFormToggleButtonsControl' => 'view/form/control/togglebuttons', 'AphrontFormTokenizerControl' => 'view/form/control/tokenizer', 'AphrontFormView' => 'view/form/base', + 'AphrontHTTPSink' => 'aphront/sink/base', + 'AphrontHTTPSinkTestCase' => 'aphront/sink/base/__tests__', 'AphrontHeadsupActionListView' => 'view/layout/headsup/actionlist', 'AphrontHeadsupActionView' => 'view/layout/headsup/action', 'AphrontIsolatedDatabaseConnection' => 'storage/connection/isolated', 'AphrontIsolatedDatabaseConnectionTestCase' => 'storage/connection/isolated/__tests__', + 'AphrontIsolatedHTTPSink' => 'aphront/sink/test', 'AphrontJavelinView' => 'view/javelin-view', 'AphrontKeyboardShortcutsAvailableView' => 'view/widget/keyboardshortcuts', 'AphrontListFilterView' => 'view/layout/listfilter', 'AphrontMySQLDatabaseConnection' => 'storage/connection/mysql', 'AphrontNullView' => 'view/null', + 'AphrontPHPHTTPSink' => 'aphront/sink/php', 'AphrontPageView' => 'view/page/base', 'AphrontPagerView' => 'view/control/pager', 'AphrontPanelView' => 'view/layout/panel', @@ -868,15 +872,18 @@ phutil_register_library_map(array( 'AphrontFormToggleButtonsControl' => 'AphrontFormControl', 'AphrontFormTokenizerControl' => 'AphrontFormControl', 'AphrontFormView' => 'AphrontView', + 'AphrontHTTPSinkTestCase' => 'PhabricatorTestCase', 'AphrontHeadsupActionListView' => 'AphrontView', 'AphrontHeadsupActionView' => 'AphrontView', 'AphrontIsolatedDatabaseConnection' => 'AphrontDatabaseConnection', 'AphrontIsolatedDatabaseConnectionTestCase' => 'PhabricatorTestCase', + 'AphrontIsolatedHTTPSink' => 'AphrontHTTPSink', 'AphrontJavelinView' => 'AphrontView', 'AphrontKeyboardShortcutsAvailableView' => 'AphrontView', 'AphrontListFilterView' => 'AphrontView', 'AphrontMySQLDatabaseConnection' => 'AphrontDatabaseConnection', 'AphrontNullView' => 'AphrontView', + 'AphrontPHPHTTPSink' => 'AphrontHTTPSink', 'AphrontPageView' => 'AphrontView', 'AphrontPagerView' => 'AphrontView', 'AphrontPanelView' => 'AphrontView', diff --git a/src/aphront/sink/base/AphrontHTTPSink.php b/src/aphront/sink/base/AphrontHTTPSink.php new file mode 100644 index 0000000000..1b1d537ecd --- /dev/null +++ b/src/aphront/sink/base/AphrontHTTPSink.php @@ -0,0 +1,113 @@ +emitHTTPStatus($code); + } + + + /** + * Write HTTP headers to the output. + * + * @param list List of pairs. + * @return void + */ + final public function writeHeaders(array $headers) { + foreach ($headers as $header) { + if (!is_array($header) || count($header) !== 2) { + throw new Exception('Malformed header.'); + } + list($name, $value) = $header; + + if (strpos($name, ':') !== false) { + throw new Exception( + "Declining to emit response with malformed HTTP header name: ". + $name); + } + + // Attackers may perform an "HTTP response splitting" attack by making + // the application emit certain types of headers containing newlines: + // + // http://en.wikipedia.org/wiki/HTTP_response_splitting + // + // PHP has built-in protections against HTTP response-splitting, but they + // are of dubious trustworthiness: + // + // http://news.php.net/php.internals/57655 + + if (preg_match('/[\r\n\0]/', $name.$value)) { + throw new Exception( + "Declining to emit response with unsafe HTTP header: ". + "<'".$name."', '".$value."'>."); + } + } + + foreach ($headers as $header) { + $this->emitHeader($name, $value); + } + } + + + /** + * Write HTTP body data to the output. + * + * @param string Body data. + * @return void + */ + final public function writeData($data) { + $this->emitData($data); + } + + +/* -( Emitting the Response )---------------------------------------------- */ + + + abstract protected function emitHTTPStatus($code); + abstract protected function emitHeader($name, $value); + abstract protected function emitData($data); +} diff --git a/src/aphront/sink/base/__init__.php b/src/aphront/sink/base/__init__.php new file mode 100644 index 0000000000..a258cb3183 --- /dev/null +++ b/src/aphront/sink/base/__init__.php @@ -0,0 +1,10 @@ +writeHTTPStatus(200); + $sink->writeHeaders(array(array('X-Test', 'test'))); + $sink->writeData('test'); + + $this->assertEqual(200, $sink->getEmittedHTTPStatus()); + $this->assertEqual( + array(array('X-Test', 'test')), + $sink->getEmittedHeaders()); + $this->assertEqual('test', $sink->getEmittedData()); + } + + public function testHTTPSinkStatusCode() { + $input = $this->tryTestCaseMap( + array( + 200 => true, + '201' => true, + 1 => false, + 1000 => false, + 'apple' => false, + '' => false, + ), + array($this, 'tryHTTPSinkStatusCode')); + } + + protected function tryHTTPSinkStatusCode($input) { + $sink = new AphrontIsolatedHTTPSink(); + $sink->writeHTTPStatus($input); + } + + public function testHTTPSinkResponseSplitting() { + $input = $this->tryTestCaseMap( + array( + "test" => true, + "test\nx" => false, + "test\rx" => false, + "test\0x" => false, + ), + array($this, 'tryHTTPSinkResponseSplitting')); + } + + protected function tryHTTPSinkResponseSplitting($input) { + $sink = new AphrontIsolatedHTTPSink(); + $sink->writeHeaders(array(array('X-Test', $input))); + } + + public function testHTTPHeaderNames() { + $input = $this->tryTestCaseMap( + array( + 'test' => true, + 'test:' => false, + ), + array($this, 'tryHTTPHeaderNames')); + } + + protected function tryHTTPHeaderNames($input) { + $sink = new AphrontIsolatedHTTPSink(); + $sink->writeHeaders(array(array($input, 'value'))); + } + +} diff --git a/src/aphront/sink/base/__tests__/__init__.php b/src/aphront/sink/base/__tests__/__init__.php new file mode 100644 index 0000000000..515c8033f1 --- /dev/null +++ b/src/aphront/sink/base/__tests__/__init__.php @@ -0,0 +1,13 @@ +status = $code; + } + + protected function emitHeader($name, $value) { + $this->headers[] = array($name, $value); + } + + protected function emitData($data) { + $this->data .= $data; + } + + public function getEmittedHTTPStatus() { + return $this->status; + } + + public function getEmittedHeaders() { + return $this->headers; + } + + public function getEmittedData() { + return $this->data; + } + +} diff --git a/src/aphront/sink/test/__init__.php b/src/aphront/sink/test/__init__.php new file mode 100644 index 0000000000..315b2f0ec5 --- /dev/null +++ b/src/aphront/sink/test/__init__.php @@ -0,0 +1,12 @@ +dispose(); - -$code = $response->getHTTPResponseCode(); -if ($code != 200) { - header("HTTP/1.0 {$code}"); -} +$sink = new AphrontPHPHTTPSink(); +$sink->writeHTTPStatus($response->getHTTPResponseCode()); $headers = $response->getCacheHeaders(); $headers = array_merge($headers, $response->getHeaders()); -foreach ($headers as $header) { - list($header, $value) = $header; - header("{$header}: {$value}"); -} +$sink->writeHeaders($headers); // TODO: This shouldn't be possible in a production-configured environment. if (isset($_REQUEST['__profile__']) && @@ -178,11 +172,11 @@ if (isset($_REQUEST['__profile__']) && ''.$profile, $response_string); } else { - echo $profile; + $sink->writeData($profile); } } -echo $response_string; +$sink->writeData($response_string); /** * @group aphront