diff --git a/src/MultipartParser.php b/src/MultipartParser.php new file mode 100644 index 00000000..d4c0f4b9 --- /dev/null +++ b/src/MultipartParser.php @@ -0,0 +1,203 @@ +input = $input; + $this->boundary = $boundary; + } + + /** + * @return array + */ + public function getPost() + { + return $this->post; + } + + /** + * @return array + */ + public function getFiles() + { + return $this->files; + } + + /** + * Do the actual parsing + */ + public function parse() + { + $blocks = $this->split($this->boundary); + + foreach ($blocks as $value) { + if (empty($value)) { + continue; + } + + $this->parseBlock($value); + } + } + + /** + * @param $boundary string + * @returns Array + */ + protected function split($boundary) + { + $boundary = preg_quote($boundary); + $result = preg_split("/\\-+$boundary/", $this->input); + array_pop($result); + return $result; + } + + /** + * Decide if we handle a file, post value or octet stream + * + * @param $string string + * @returns void + */ + protected function parseBlock($string) + { + if (strpos($string, 'filename') !== false) { + $this->file($string); + return; + } + + // This may never be called, if an octet stream + // has a filename it is catched by the previous + // condition already. + if (strpos($string, 'application/octet-stream') !== false) { + $this->octetStream($string); + return; + } + + $this->post($string); + } + + /** + * Parse a raw octet stream + * + * @param $string + * @return array + */ + protected function octetStream($string) + { + preg_match('/name=\"([^\"]*)\".*stream[\n|\r]+([^\n\r].*)?$/s', $string, $match); + + $this->addResolved('post', $match[1], $match[2]); + } + + /** + * Parse a file + * + * @param $string + * @return array + */ + protected function file($string) + { + preg_match('/name=\"([^\"]*)\"; filename=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $string, $match); + preg_match('/Content-Type: (.*)?/', $match[3], $mime); + + $content = preg_replace('/Content-Type: (.*)[^\n\r]/', '', $match[3]); + $content = ltrim($content, "\r\n"); + + // Put content in a stream + $stream = fopen('php://memory', 'r+'); + if ($content !== '') { + fwrite($stream, $content); + fseek($stream, 0); + } + + $data = [ + 'name' => $match[2], + 'type' => trim($mime[1]), + 'stream' => $stream, // Instead of writing to a file, we write to a stream. + 'error' => UPLOAD_ERR_OK, + 'size' => function_exists('mb_strlen')? mb_strlen($content, '8bit') : strlen($content), + ]; + + //TODO :: have an option to write to files to emulate the same functionality as a real php server + //$path = tempnam(sys_get_temp_dir(), "php"); + //$err = file_put_contents($path, $content); + //$data['tmp_name'] = $path; + //$data['error'] = ($err === false) ? UPLOAD_ERR_NO_FILE : UPLOAD_ERR_OK; + + $this->addResolved('files', $match[1], $data); + } + + /** + * Parse POST values + * + * @param $string + * @return array + */ + protected function post($string) + { + preg_match('/name=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $string, $match); + + $this->addResolved('post', $match[1], $match[2]); + } + + /** + * Put the file or post where it belongs, + * The key names can be simple, or containing [] + * it can also be a named key + * + * @param $type + * @param $key + * @param $content + */ + protected function addResolved($type, $key, $content) + { + if (preg_match('/^(.*)\[(.*)\]$/i', $key, $tmp)) { + if (!empty($tmp[2])) { + $this->{$type}[$tmp[1]][$tmp[2]] = $content; + } else { + $this->{$type}[$tmp[1]][] = $content; + } + } else { + $this->{$type}[$key] = $content; + } + } +} diff --git a/src/Request.php b/src/Request.php index 605b909e..0607b8e7 100644 --- a/src/Request.php +++ b/src/Request.php @@ -11,21 +11,25 @@ class Request extends EventEmitter implements ReadableStreamInterface { private $readable = true; private $method; - private $path; + private $url; private $query; private $httpVersion; private $headers; + private $body; + private $post = []; + private $files = []; // metadata, implicitly added externally public $remoteAddress; - public function __construct($method, $path, $query = array(), $httpVersion = '1.1', $headers = array()) + public function __construct($method, $url, $query = array(), $httpVersion = '1.1', $headers = array(), $body = '') { $this->method = $method; - $this->path = $path; + $this->url = $url; $this->query = $query; $this->httpVersion = $httpVersion; $this->headers = $headers; + $this->body = $body; } public function getMethod() @@ -35,7 +39,12 @@ public function getMethod() public function getPath() { - return $this->path; + return $this->url->getPath(); + } + + public function getUrl() + { + return $this->url; } public function getQuery() @@ -53,6 +62,41 @@ public function getHeaders() return $this->headers; } + public function getBody() + { + return $this->body; + } + + public function setBody($body) + { + $this->body = $body; + } + + public function getFiles() + { + return $this->files; + } + + public function setFiles($files) + { + $this->files = $files; + } + + public function getPost() + { + return $this->post; + } + + public function setPost($post) + { + $this->post = $post; + } + + public function getRemoteAddress() + { + return $this->remoteAddress; + } + public function expectsContinue() { return isset($this->headers['Expect']) && '100-continue' === $this->headers['Expect']; diff --git a/src/RequestHeaderParser.php b/src/RequestHeaderParser.php deleted file mode 100644 index 5f0dc1f0..00000000 --- a/src/RequestHeaderParser.php +++ /dev/null @@ -1,64 +0,0 @@ -buffer) + strlen($data) > $this->maxSize) { - $this->emit('error', array(new \OverflowException("Maximum header size of {$this->maxSize} exceeded."), $this)); - - return; - } - - $this->buffer .= $data; - - if (false !== strpos($this->buffer, "\r\n\r\n")) { - list($request, $bodyBuffer) = $this->parseRequest($this->buffer); - - $this->emit('headers', array($request, $bodyBuffer)); - $this->removeAllListeners(); - } - } - - public function parseRequest($data) - { - list($headers, $bodyBuffer) = explode("\r\n\r\n", $data, 2); - - $psrRequest = g7\parse_request($headers); - - $parsedQuery = []; - $queryString = $psrRequest->getUri()->getQuery(); - if ($queryString) { - parse_str($queryString, $parsedQuery); - } - - $headers = $psrRequest->getHeaders(); - array_walk($headers, function(&$val) { - if (1 === count($val)) { - $val = $val[0]; - } - }); - - $request = new Request( - $psrRequest->getMethod(), - $psrRequest->getUri()->getPath(), - $parsedQuery, - $psrRequest->getProtocolVersion(), - $headers - ); - - return array($request, $bodyBuffer); - } -} diff --git a/src/RequestParser.php b/src/RequestParser.php new file mode 100644 index 00000000..c213ec23 --- /dev/null +++ b/src/RequestParser.php @@ -0,0 +1,148 @@ +buffer .= $data; + + if (!$this->request && false !== strpos($this->buffer, "\r\n\r\n")) { + + // Extract the header from the buffer + // in case the content isn't complete + list($headers, $this->buffer) = explode("\r\n\r\n", $this->buffer, 2); + + // Fail before parsing if the + if (strlen($headers) > $this->maxSize) { + $this->headerSizeExceeded(); + return; + } + + $this->request = $this->parseHeaders($headers . "\r\n\r\n"); + } + + // if there is a request (meaning the headers are parsed) and + // we have the right content size, we can finish the parsing + if ($this->request && $this->isRequestComplete()) { + $this->parseBody(substr($this->buffer, 0, $this->length)); + $this->finishParsing(); + return; + } + + // fail if the header hasn't finished but it is already too large + if (!$this->request && strlen($this->buffer) > $this->maxSize) { + $this->headerSizeExceeded(); + return; + } + } + + protected function isRequestComplete() + { + $headers = $this->request->getHeaders(); + + // if there is no content length, there should + // be no content so we can say it's done + if (!array_key_exists('Content-Length', $headers)) { + return true; + } + + // if the content is present and has the + // right length, we're good to go + if (array_key_exists('Content-Length', $headers) && strlen($this->buffer) >= $headers['Content-Length']) { + + // store the expected content length + $this->length = $this->request->getHeaders()['Content-Length']; + + return true; + } + + return false; + } + + protected function finishParsing() + { + $this->emit('headers', array($this->request, $this->request->getBody())); + $this->removeAllListeners(); + $this->request = null; + } + + protected function headerSizeExceeded() + { + $this->emit('error', array(new \OverflowException("Maximum header size of {$this->maxSize} exceeded."), $this)); + } + + public function parseHeaders($data) + { + $psrRequest = g7\parse_request($data); + + $parsedQuery = []; + $queryString = $psrRequest->getUri()->getQuery(); + if ($queryString) { + parse_str($queryString, $parsedQuery); + } + + $headers = $psrRequest->getHeaders(); + array_walk($headers, function(&$val) { + if (1 === count($val)) { + $val = $val[0]; + } + }); + + return new Request( + $psrRequest->getMethod(), + $psrRequest->getUri(), + $parsedQuery, + $psrRequest->getProtocolVersion(), + $headers + ); + } + + public function parseBody($content) + { + $headers = $this->request->getHeaders(); + + if (array_key_exists('Content-Type', $headers)) { + if (strpos($headers['Content-Type'], 'multipart/') === 0) { + //TODO :: parse the content while it is streaming + preg_match("/boundary=\"?(.*)\"?$/", $headers['Content-Type'], $matches); + $boundary = $matches[1]; + + $parser = new MultipartParser($content, $boundary); + $parser->parse(); + + $this->request->setPost($parser->getPost()); + $this->request->setFiles($parser->getFiles()); + return; + } + + if (strtolower($headers['Content-Type']) == 'application/x-www-form-urlencoded') { + parse_str(urldecode($content), $result); + $this->request->setPost($result); + + return; + } + } + + + + $this->request->setBody($content); + } +} diff --git a/src/Server.php b/src/Server.php index e1abfd8e..5e420a40 100644 --- a/src/Server.php +++ b/src/Server.php @@ -18,9 +18,8 @@ public function __construct(SocketServerInterface $io) $this->io->on('connection', function (ConnectionInterface $conn) { // TODO: http 1.1 keep-alive // TODO: chunked transfer encoding (also for outgoing data) - // TODO: multipart parsing - $parser = new RequestHeaderParser(); + $parser = new RequestParser(); $parser->on('headers', function (Request $request, $bodyBuffer) use ($conn, $parser) { // attach remote ip to the request as metadata $request->remoteAddress = $conn->getRemoteAddress(); diff --git a/tests/MultipartParserTest.php b/tests/MultipartParserTest.php new file mode 100644 index 00000000..39a02c71 --- /dev/null +++ b/tests/MultipartParserTest.php @@ -0,0 +1,102 @@ +parse(); + + $this->assertEmpty($parser->getFiles()); + $this->assertEquals( + ['users' => ['one' => 'single', 'two' => 'second']], + $parser->getPost() + ); + } + + public function testFileUpload() { + + $file = base64_decode("R0lGODlhAQABAIAAAP///wAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw=="); + + $boundary = "---------------------------12758086162038677464950549563"; + + $data = "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"user\"\r\n"; + $data .= "\r\n"; + $data .= "single\r\n"; + $data .= "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"user2\"\r\n"; + $data .= "\r\n"; + $data .= "second\r\n"; + $data .= "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"users[]\"\r\n"; + $data .= "\r\n"; + $data .= "first in array\r\n"; + $data .= "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"users[]\"\r\n"; + $data .= "\r\n"; + $data .= "second in array\r\n"; + $data .= "--$boundary\r\n"; + $data .= "Content-Disposition: form-data; name=\"file\"; filename=\"User.php\"\r\n"; + $data .= "Content-Type: text/php\r\n"; + $data .= "\r\n"; + $data .= "parse(); + + $this->assertEquals(2, count($parser->getFiles())); + $this->assertEquals(2, count($parser->getFiles()['files'])); + $this->assertEquals( + ['user' => 'single', 'user2' => 'second', 'users' => ['first in array', 'second in array']], + $parser->getPost() + ); + + $uploaded_blank = $parser->getFiles()['files'][0]; + + // The original test was `file_get_contents($uploaded_blank['tmp_name'])` + // but as we moved to resources, we can't use that anymore, this is the only + // difference with a stock php implementation + $this->assertEquals($file, stream_get_contents($uploaded_blank['stream'])); + + $uploaded_blank['stream'] = 'file'; //override the resource as it is random + $expected_file = [ + 'name' => 'blank.gif', + 'type' => 'image/gif', + 'stream' => 'file', + 'error' => 0, + 'size' => 43, + ]; + + $this->assertEquals($expected_file, $uploaded_blank); + } +} diff --git a/tests/RequestHeaderParserTest.php b/tests/RequestHeaderParserTest.php deleted file mode 100644 index dd3a0cbe..00000000 --- a/tests/RequestHeaderParserTest.php +++ /dev/null @@ -1,136 +0,0 @@ -on('headers', $this->expectCallableNever()); - - $parser->feed("GET / HTTP/1.1\r\n"); - $parser->feed("Host: example.com:80\r\n"); - $parser->feed("Connection: close\r\n"); - - $parser->removeAllListeners(); - $parser->on('headers', $this->expectCallableOnce()); - - $parser->feed("\r\n"); - } - - public function testFeedInOneGo() - { - $parser = new RequestHeaderParser(); - $parser->on('headers', $this->expectCallableOnce()); - - $data = $this->createGetRequest(); - $parser->feed($data); - } - - public function testHeadersEventShouldReturnRequestAndBodyBuffer() - { - $request = null; - $bodyBuffer = null; - - $parser = new RequestHeaderParser(); - $parser->on('headers', function ($parsedRequest, $parsedBodyBuffer) use (&$request, &$bodyBuffer) { - $request = $parsedRequest; - $bodyBuffer = $parsedBodyBuffer; - }); - - $data = $this->createGetRequest(); - $data .= 'RANDOM DATA'; - $parser->feed($data); - - $this->assertInstanceOf('React\Http\Request', $request); - $this->assertSame('GET', $request->getMethod()); - $this->assertSame('/', $request->getPath()); - $this->assertSame(array(), $request->getQuery()); - $this->assertSame('1.1', $request->getHttpVersion()); - $this->assertSame(array('Host' => 'example.com:80', 'Connection' => 'close'), $request->getHeaders()); - - $this->assertSame('RANDOM DATA', $bodyBuffer); - } - - public function testHeadersEventShouldReturnBinaryBodyBuffer() - { - $bodyBuffer = null; - - $parser = new RequestHeaderParser(); - $parser->on('headers', function ($parsedRequest, $parsedBodyBuffer) use (&$bodyBuffer) { - $bodyBuffer = $parsedBodyBuffer; - }); - - $data = $this->createGetRequest(); - $data .= "\0x01\0x02\0x03\0x04\0x05"; - $parser->feed($data); - - $this->assertSame("\0x01\0x02\0x03\0x04\0x05", $bodyBuffer); - } - - public function testHeadersEventShouldParsePathAndQueryString() - { - $request = null; - - $parser = new RequestHeaderParser(); - $parser->on('headers', function ($parsedRequest, $parsedBodyBuffer) use (&$request) { - $request = $parsedRequest; - }); - - $data = $this->createAdvancedPostRequest(); - $parser->feed($data); - - $this->assertInstanceOf('React\Http\Request', $request); - $this->assertSame('POST', $request->getMethod()); - $this->assertSame('/foo', $request->getPath()); - $this->assertSame(array('bar' => 'baz'), $request->getQuery()); - $this->assertSame('1.1', $request->getHttpVersion()); - $headers = array( - 'Host' => 'example.com:80', - 'User-Agent' => 'react/alpha', - 'Connection' => 'close', - ); - $this->assertSame($headers, $request->getHeaders()); - } - - public function testHeaderOverflowShouldEmitError() - { - $error = null; - - $parser = new RequestHeaderParser(); - $parser->on('headers', $this->expectCallableNever()); - $parser->on('error', function ($message) use (&$error) { - $error = $message; - }); - - $data = str_repeat('A', 4097); - $parser->feed($data); - - $this->assertInstanceOf('OverflowException', $error); - $this->assertSame('Maximum header size of 4096 exceeded.', $error->getMessage()); - } - - private function createGetRequest() - { - $data = "GET / HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; - $data .= "Connection: close\r\n"; - $data .= "\r\n"; - - return $data; - } - - private function createAdvancedPostRequest() - { - $data = "POST /foo?bar=baz HTTP/1.1\r\n"; - $data .= "Host: example.com:80\r\n"; - $data .= "User-Agent: react/alpha\r\n"; - $data .= "Connection: close\r\n"; - $data .= "\r\n"; - - return $data; - } -} diff --git a/tests/RequestParserTest.php b/tests/RequestParserTest.php new file mode 100644 index 00000000..728166b3 --- /dev/null +++ b/tests/RequestParserTest.php @@ -0,0 +1,309 @@ +on('headers', $this->expectCallableNever()); + + $parser->feed("GET / HTTP/1.1\r\n"); + $parser->feed("Host: example.com:80\r\n"); + $parser->feed("Connection: close\r\n"); + + $parser->removeAllListeners(); + $parser->on('headers', $this->expectCallableOnce()); + + $parser->feed("\r\n"); + } + + public function testFeedInOneGo() + { + $parser = new RequestParser(); + $parser->on('headers', $this->expectCallableOnce()); + + $data = $this->createGetRequest(); + $parser->feed($data); + } + + public function testHeadersEventShouldReturnRequestAndBodyBuffer() + { + $request = null; + $bodyBuffer = null; + + $parser = new RequestParser(); + $parser->on('headers', function ($parsedRequest, $parsedBodyBuffer) use (&$request, &$bodyBuffer) { + $request = $parsedRequest; + $bodyBuffer = $parsedBodyBuffer; + }); + + $data = $this->createGetRequest('RANDOM DATA', 11); + $parser->feed($data); + + $this->assertInstanceOf('React\Http\Request', $request); + $this->assertSame('GET', $request->getMethod()); + $this->assertSame('/', $request->getPath()); + $this->assertSame(array(), $request->getQuery()); + $this->assertSame('1.1', $request->getHttpVersion()); + $this->assertSame( + array('Host' => 'example.com:80', 'Connection' => 'close', 'Content-Length' => '11'), + $request->getHeaders() + ); + + $this->assertSame('RANDOM DATA', $bodyBuffer); + } + + public function testHeadersEventShouldReturnBinaryBodyBuffer() + { + $bodyBuffer = null; + + $parser = new RequestParser(); + $parser->on('headers', function ($parsedRequest, $parsedBodyBuffer) use (&$bodyBuffer) { + $bodyBuffer = $parsedBodyBuffer; + }); + + $data = $this->createGetRequest("\0x01\0x02\0x03\0x04\0x05", strlen("\0x01\0x02\0x03\0x04\0x05")); + $parser->feed($data); + + $this->assertSame("\0x01\0x02\0x03\0x04\0x05", $bodyBuffer); + } + + public function testHeadersEventShouldParsePathAndQueryString() + { + $request = null; + + $parser = new RequestParser(); + $parser->on('headers', function ($parsedRequest, $parsedBodyBuffer) use (&$request) { + $request = $parsedRequest; + }); + + $data = $this->createAdvancedPostRequest(); + $parser->feed($data); + + $this->assertInstanceOf('React\Http\Request', $request); + $this->assertSame('POST', $request->getMethod()); + $this->assertSame('/foo', $request->getPath()); + $this->assertSame(array('bar' => 'baz'), $request->getQuery()); + $this->assertSame('1.1', $request->getHttpVersion()); + $headers = array( + 'Host' => 'example.com:80', + 'User-Agent' => 'react/alpha', + 'Connection' => 'close', + ); + $this->assertSame($headers, $request->getHeaders()); + } + + public function testShouldReceiveBodyContent() + { + $content1 = "{\"test\":"; $content2 = " \"value\"}"; + + $request = null; + $body = null; + + $parser = new RequestParser(); + $parser->on('headers', function ($parsedRequest, $parsedBodyBuffer) use (&$request, &$body) { + $request = $parsedRequest; + $body = $parsedBodyBuffer; + }); + + $data = $this->createAdvancedPostRequest('', 17); + $parser->feed($data); + $parser->feed($content1); + $parser->feed($content2 . "\r\n"); + + $this->assertInstanceOf('React\Http\Request', $request); + $this->assertEquals($content1 . $content2, $request->getBody()); + $this->assertSame($body, $request->getBody()); + } + + public function testShouldReceiveMultiPartBody() + { + + $request = null; + $body = null; + + $parser = new RequestParser(); + $parser->on('headers', function ($parsedRequest, $parsedBodyBuffer) use (&$request, &$body) { + $request = $parsedRequest; + $body = $parsedBodyBuffer; + }); + + $parser->feed($this->createMultipartRequest()); + + $this->assertInstanceOf('React\Http\Request', $request); + $this->assertEquals( + $request->getPost(), + ['user' => 'single', 'user2' => 'second', 'users' => ['first in array', 'second in array']] + ); + $this->assertEquals(2, count($request->getFiles())); + $this->assertEquals(2, count($request->getFiles()['files'])); + } + + public function testShouldReceivePostInBody() + { + $request = null; + $body = null; + + $parser = new RequestParser(); + $parser->on('headers', function ($parsedRequest, $parsedBodyBuffer) use (&$request, &$body) { + $request = $parsedRequest; + $body = $parsedBodyBuffer; + }); + + $parser->feed($this->createPostWithContent()); + + $this->assertInstanceOf('React\Http\Request', $request); + $this->assertSame('', $body); + $this->assertEquals( + $request->getPost(), + ['user' => 'single', 'user2' => 'second', 'users' => ['first in array', 'second in array']] + ); + } + + public function testHeaderOverflowShouldEmitError() + { + $error = null; + + $parser = new RequestParser(); + $parser->on('headers', $this->expectCallableNever()); + $parser->on('error', function ($message) use (&$error) { + $error = $message; + }); + + $data = str_repeat('A', 4097); + $parser->feed($data); + + $this->assertInstanceOf('OverflowException', $error); + $this->assertSame('Maximum header size of 4096 exceeded.', $error->getMessage()); + } + + public function testOnePassHeaderTooLarge() + { + $error = null; + + $parser = new RequestParser(); + $parser->on('headers', $this->expectCallableNever()); + $parser->on('error', function ($message) use (&$error) { + $error = $message; + }); + + $data = "POST /foo?bar=baz HTTP/1.1\r\n"; + $data .= "Host: example.com:80\r\n"; + $data .= "Cookie: " . str_repeat('A', 4097) . "\r\n"; + $data .= "\r\n"; + $parser->feed($data); + + $this->assertInstanceOf('OverflowException', $error); + $this->assertSame('Maximum header size of 4096 exceeded.', $error->getMessage()); + } + + public function testBodyShouldNotOverflowHeader() + { + $error = null; + + $parser = new RequestParser(); + $parser->on('headers', $this->expectCallableOnce()); + $parser->on('error', function ($message) use (&$error) { + $error = $message; + }); + + $data = str_repeat('A', 4097); + $parser->feed($this->createAdvancedPostRequest() . $data); + + $this->assertNull($error); + } + + private function createGetRequest($content = '', $len = 0) + { + $data = "GET / HTTP/1.1\r\n"; + $data .= "Host: example.com:80\r\n"; + $data .= "Connection: close\r\n"; + if($len) { + $data .= "Content-Length: $len\r\n"; + } + $data .= "\r\n"; + $data .= $content; + + return $data; + } + + private function createAdvancedPostRequest($content = '', $len = 0) + { + $data = "POST /foo?bar=baz HTTP/1.1\r\n"; + $data .= "Host: example.com:80\r\n"; + $data .= "User-Agent: react/alpha\r\n"; + $data .= "Connection: close\r\n"; + if($len) { + $data .= "Content-Length: $len\r\n"; + } + $data .= "\r\n"; + $data .= $content; + + return $data; + } + + private function createPostWithContent() + { + $data = "POST /foo?bar=baz HTTP/1.1\r\n"; + $data .= "Host: localhost:8080\r\n"; + $data .= "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:32.0) Gecko/20100101 Firefox/32.0\r\n"; + $data .= "Connection: close\r\n"; + $data .= "Content-Type: application/x-www-form-urlencoded\r\n"; + $data .= "Content-Length: 79\r\n"; + $data .= "\r\n"; + $data .= "user=single&user2=second&users%5B%5D=first+in+array&users%5B%5D=second+in+array\r\n"; + + return $data; + } + + private function createMultipartRequest() + { + $data = "POST / HTTP/1.1\r\n"; + $data .= "Host: localhost:8080\r\n"; + $data .= "Connection: close\r\n"; + $data .= "Content-Type: multipart/form-data; boundary=---------------------------12758086162038677464950549563\r\n"; + $data .= "Content-Length: 1097\r\n"; + $data .= "\r\n"; + + $data .= "-----------------------------12758086162038677464950549563\r\n"; + $data .= "Content-Disposition: form-data; name=\"user\"\r\n"; + $data .= "\r\n"; + $data .= "single\r\n"; + $data .= "-----------------------------12758086162038677464950549563\r\n"; + $data .= "Content-Disposition: form-data; name=\"user2\"\r\n"; + $data .= "\r\n"; + $data .= "second\r\n"; + $data .= "-----------------------------12758086162038677464950549563\r\n"; + $data .= "Content-Disposition: form-data; name=\"users[]\"\r\n"; + $data .= "\r\n"; + $data .= "first in array\r\n"; + $data .= "-----------------------------12758086162038677464950549563\r\n"; + $data .= "Content-Disposition: form-data; name=\"users[]\"\r\n"; + $data .= "\r\n"; + $data .= "second in array\r\n"; + $data .= "-----------------------------12758086162038677464950549563\r\n"; + $data .= "Content-Disposition: form-data; name=\"file\"; filename=\"User.php\"\r\n"; + $data .= "Content-Type: text/php\r\n"; + $data .= "\r\n"; + $data .= "assertInstanceOf('React\Http\Request', $request); $this->assertSame('/', $request->getPath()); $this->assertSame('GET', $request->getMethod()); - $this->assertSame('127.0.0.1', $request->remoteAddress); + $this->assertSame('127.0.0.1', $request->getRemoteAddress()); $this->assertInstanceOf('React\Http\Response', $response); });