Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[0.3] Replace guzzle/http with custom implementation #131

Merged
merged 8 commits into from
Feb 24, 2013
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 0 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
"require": {
"php": ">=5.3.3",
"evenement/evenement": "1.0.*",
"guzzle/http": "3.0.*",
"guzzle/parser": "3.0.*",
"react/promise": "1.0.*"
},
Expand Down
30 changes: 24 additions & 6 deletions src/React/Http/Response.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
namespace React\Http;

use Evenement\EventEmitter;
use Guzzle\Http\Message\Response as GuzzleResponse;
use React\Socket\ConnectionInterface;
use React\Stream\WritableStreamInterface;

Expand Down Expand Up @@ -59,18 +58,37 @@ public function writeHead($status = 200, array $headers = array())
$this->chunkedEncoding = false;
}

$response = new GuzzleResponse($status);
$response->setHeader('X-Powered-By', 'React/alpha');
$response->addHeaders($headers);
$headers = array_merge(
array('X-Powered-By' => 'React/alpha'),
$headers
);
if ($this->chunkedEncoding) {
$response->setHeader('Transfer-Encoding', 'chunked');
$headers['Transfer-Encoding'] = 'chunked';
}
$data = (string) $response;

$data = $this->formatHead($status, $headers);
$this->conn->write($data);

$this->headWritten = true;
}

private function formatHead($status, array $headers)
{
$status = (int) $status;
$text = isset(ResponseCodes::$statusTexts[$status]) ? ResponseCodes::$statusTexts[$status] : '';
$data = "HTTP/1.1 $status $text\r\n";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should verify that $status and $text cannot contain "\r\n" or "\n" to protect against http-header injections

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are from ResponseCodes, which is a trusted source.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$status is not enforced to be a number, therefore could also contain "\r\n" and finally used for a injection

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like your attention to detail, fixed.


foreach ($headers as $name => $value) {
$name = str_replace(array("\r", "\n"), '', $name);
$value = str_replace(array("\r", "\n"), '', $value);

$data .= "$name: $value\r\n";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

}
$data .= "\r\n";

return $data;
}

public function write($data)
{
if (!$this->headWritten) {
Expand Down
72 changes: 72 additions & 0 deletions src/React/Http/ResponseCodes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

namespace React\Http;

/**
* This is copy-pasted from Symfony2's Response class
*/
class ResponseCodes
{
public static $statusTexts = array(
100 => 'Continue',
101 => 'Switching Protocols',
102 => 'Processing', // RFC2518
200 => 'OK',
201 => 'Created',
202 => 'Accepted',
203 => 'Non-Authoritative Information',
204 => 'No Content',
205 => 'Reset Content',
206 => 'Partial Content',
207 => 'Multi-Status', // RFC4918
208 => 'Already Reported', // RFC5842
226 => 'IM Used', // RFC3229
300 => 'Multiple Choices',
301 => 'Moved Permanently',
302 => 'Found',
303 => 'See Other',
304 => 'Not Modified',
305 => 'Use Proxy',
306 => 'Reserved',
307 => 'Temporary Redirect',
308 => 'Permanent Redirect', // RFC-reschke-http-status-308-07
400 => 'Bad Request',
401 => 'Unauthorized',
402 => 'Payment Required',
403 => 'Forbidden',
404 => 'Not Found',
405 => 'Method Not Allowed',
406 => 'Not Acceptable',
407 => 'Proxy Authentication Required',
408 => 'Request Timeout',
409 => 'Conflict',
410 => 'Gone',
411 => 'Length Required',
412 => 'Precondition Failed',
413 => 'Request Entity Too Large',
414 => 'Request-URI Too Long',
415 => 'Unsupported Media Type',
416 => 'Requested Range Not Satisfiable',
417 => 'Expectation Failed',
418 => 'I\'m a teapot', // RFC2324
422 => 'Unprocessable Entity', // RFC4918
423 => 'Locked', // RFC4918
424 => 'Failed Dependency', // RFC4918
425 => 'Reserved for WebDAV advanced collections expired proposal', // RFC2817
426 => 'Upgrade Required', // RFC2817
428 => 'Precondition Required', // RFC6585
429 => 'Too Many Requests', // RFC6585
431 => 'Request Header Fields Too Large', // RFC6585
500 => 'Internal Server Error',
501 => 'Not Implemented',
502 => 'Bad Gateway',
503 => 'Service Unavailable',
504 => 'Gateway Timeout',
505 => 'HTTP Version Not Supported',
506 => 'Variant Also Negotiates (Experimental)', // RFC2295
507 => 'Insufficient Storage', // RFC4918
508 => 'Loop Detected', // RFC5842
510 => 'Not Extended', // RFC2774
511 => 'Network Authentication Required', // RFC6585
);
}
1 change: 0 additions & 1 deletion src/React/Http/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
"license": "MIT",
"require": {
"php": ">=5.3.3",
"guzzle/http": "3.0.*",
"guzzle/parser": "3.0.*",
"react/socket": "0.2.*"
},
Expand Down
11 changes: 4 additions & 7 deletions src/React/HttpClient/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,15 @@

namespace React\HttpClient;

use Guzzle\Http\Message\Request as GuzzleRequest;
use React\EventLoop\LoopInterface;
use React\HttpClient\ConnectionManager;
use React\HttpClient\Request as ClientRequest;
use React\HttpClient\Request;
use React\HttpClient\SecureConnectionManager;

class Client
{
private $loop;

private $connectionManager;

private $secureConnectionManager;

public function __construct(LoopInterface $loop, ConnectionManagerInterface $connectionManager, ConnectionManagerInterface $secureConnectionManager)
Expand All @@ -25,9 +22,9 @@ public function __construct(LoopInterface $loop, ConnectionManagerInterface $con

public function request($method, $url, array $headers = array())
{
$guzzleRequest = new GuzzleRequest($method, $url, $headers);
$connectionManager = $this->getConnectionManagerForScheme($guzzleRequest->getScheme());
return new ClientRequest($this->loop, $connectionManager, $guzzleRequest);
$requestData = new RequestData($method, $url, $headers);
$connectionManager = $this->getConnectionManagerForScheme($requestData->getScheme());
return new Request($this->loop, $connectionManager, $requestData);
}

public function setConnectionManager(ConnectionManagerInterface $connectionManager)
Expand Down
21 changes: 10 additions & 11 deletions src/React/HttpClient/Request.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
namespace React\HttpClient;

use Evenement\EventEmitter;
use Guzzle\Http\Message\Request as GuzzleRequest;
use Guzzle\Http\Url;
use Guzzle\Parser\Message\MessageParser;
use React\EventLoop\LoopInterface;
use React\HttpClient\ConnectionManagerInterface;
Expand All @@ -24,20 +22,21 @@ class Request extends EventEmitter implements WritableStreamInterface
const STATE_HEAD_WRITTEN = 2;
const STATE_END = 3;

private $request;
private $loop;
private $connectionManager;
private $requestData;

private $stream;
private $buffer;
private $responseFactory;
private $response;
private $state = self::STATE_INIT;

public function __construct(LoopInterface $loop, ConnectionManagerInterface $connectionManager, GuzzleRequest $request)
public function __construct(LoopInterface $loop, ConnectionManagerInterface $connectionManager, RequestData $requestData)
{
$this->loop = $loop;
$this->connectionManager = $connectionManager;
$this->request = $request;
$this->requestData = $requestData;
}

public function isWritable()
Expand All @@ -54,23 +53,23 @@ public function writeHead()
$this->state = self::STATE_WRITING_HEAD;

$that = $this;
$request = $this->request;
$requestData = $this->requestData;
$streamRef = &$this->stream;
$stateRef = &$this->state;

$this
->connect()
->then(
function ($stream) use ($that, $request, &$streamRef, &$stateRef) {
function ($stream) use ($that, $requestData, &$streamRef, &$stateRef) {
$streamRef = $stream;

$stream->on('drain', array($that, 'handleDrain'));
$stream->on('data', array($that, 'handleData'));
$stream->on('end', array($that, 'handleEnd'));
$stream->on('error', array($that, 'handleError'));

$request->setProtocolVersion('1.0');
$headers = (string) $request;
$requestData->setProtocolVersion('1.0');
$headers = (string) $requestData;

$stream->write($headers);

Expand Down Expand Up @@ -215,8 +214,8 @@ protected function parseResponse($data)

protected function connect()
{
$host = $this->request->getHost();
$port = $this->request->getPort();
$host = $this->requestData->getHost();
$port = $this->requestData->getPort();

return $this->connectionManager
->getConnection($host, $port);
Expand Down
81 changes: 81 additions & 0 deletions src/React/HttpClient/RequestData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

namespace React\HttpClient;

class RequestData
{
private $method;
private $url;
private $headers;

private $protocolVersion = '1.1';

public function __construct($method, $url, array $headers = array())
{
$this->method = $method;
$this->url = $url;
$this->headers = $headers;
}

private function mergeDefaultheaders(array $headers)
{
$port = ($this->getDefaultPort() === $this->getPort()) ? '' : ":{$this->getPort()}";
$connectionHeaders = ('1.1' === $this->protocolVersion) ? array('Connection' => 'close') : array();

return array_merge(
array(
'Host' => $this->getHost().$port,
'User-Agent' => 'React/alpha',
),
$connectionHeaders,
$headers
);
}

public function getScheme()
{
return parse_url($this->url, PHP_URL_SCHEME);
}

public function getHost()
{
return parse_url($this->url, PHP_URL_HOST);
}

public function getPort()
{
return (int) parse_url($this->url, PHP_URL_PORT) ?: $this->getDefaultPort();
}

public function getDefaultPort()
{
return ('https' === $this->getScheme()) ? 443 : 80;
}

public function getPath()
{
$path = parse_url($this->url, PHP_URL_PATH) ?: '/';
$queryString = parse_url($this->url, PHP_URL_QUERY);

return $path.($queryString ? "?$queryString" : '');
}

public function setProtocolVersion($version)
{
$this->protocolVersion = $version;
}

public function __toString()
{
$headers = $this->mergeDefaultheaders($this->headers);

$data = '';
$data .= "{$this->method} {$this->getPath()} HTTP/{$this->protocolVersion}\r\n";
foreach ($headers as $name => $value) {
$data .= "$name: $value\r\n";
}
$data .= "\r\n";

return $data;
}
}
1 change: 0 additions & 1 deletion src/React/HttpClient/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
"license": "MIT",
"require": {
"php": ">=5.3.3",
"guzzle/http": "2.8.*",
"guzzle/parser": "2.8.*",
"react/socket": "0.2.*",
"react/dns": "0.2.*"
Expand Down
4 changes: 2 additions & 2 deletions tests/React/Tests/Http/RequestHeaderParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ public function testHeadersEventShouldParsePathAndQueryString()
$this->assertSame('1.1', $request->getHttpVersion());
$headers = array(
'Host' => 'example.com:80',
'User-Agent' => 'Guzzle/2.0 (Language=PHP/5.3.8; curl=7.21.4; Host=universal-apple-darwin11.0)',
'User-Agent' => 'react/alpha',
'Connection' => 'close',
);
$this->assertSame($headers, $request->getHeaders());
Expand Down Expand Up @@ -128,7 +128,7 @@ private function createAdvancedPostRequest()
{
$data = "POST /foo?bar=baz HTTP/1.1\r\n";
$data .= "Host: example.com:80\r\n";
$data .= "User-Agent: Guzzle/2.0 (Language=PHP/5.3.8; curl=7.21.4; Host=universal-apple-darwin11.0)\r\n";
$data .= "User-Agent: react/alpha\r\n";
$data .= "Connection: close\r\n";
$data .= "\r\n";

Expand Down
39 changes: 39 additions & 0 deletions tests/React/Tests/Http/ResponseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,4 +124,43 @@ public function shouldForwardEndDrainAndErrorEvents()

$response = new Response($conn);
}

/** @test */
public function shouldRemoveNewlinesFromHeaders()
{
$expected = '';
$expected .= "HTTP/1.1 200 OK\r\n";
$expected .= "X-Powered-By: React/alpha\r\n";
$expected .= "FooBar: BazQux\r\n";
$expected .= "Transfer-Encoding: chunked\r\n";
$expected .= "\r\n";

$conn = $this->getMock('React\Socket\ConnectionInterface');
$conn
->expects($this->once())
->method('write')
->with($expected);

$response = new Response($conn);
$response->writeHead(200, array("Foo\nBar" => "Baz\rQux"));
}

/** @test */
public function missingStatusCodeTextShouldResultInNumberOnlyStatus()
{
$expected = '';
$expected .= "HTTP/1.1 700 \r\n";
$expected .= "X-Powered-By: React/alpha\r\n";
$expected .= "Transfer-Encoding: chunked\r\n";
$expected .= "\r\n";

$conn = $this->getMock('React\Socket\ConnectionInterface');
$conn
->expects($this->once())
->method('write')
->with($expected);

$response = new Response($conn);
$response->writeHead(700);
}
}