From 5205bc7db65abdc79f354de8910077c9a1f0380f Mon Sep 17 00:00:00 2001
From: Matt Bonneau <matt@bonneau.net>
Date: Fri, 31 Mar 2017 11:46:27 -0400
Subject: [PATCH 1/3] Allow connection upgrades

---
 src/Server.php | 15 ++++++++++-----
 1 file changed, 10 insertions(+), 5 deletions(-)

diff --git a/src/Server.php b/src/Server.php
index e081fb62..4d4f0a11 100644
--- a/src/Server.php
+++ b/src/Server.php
@@ -282,7 +282,9 @@ function ($error) use ($that, $conn, $request) {
             }
         );
 
-        if ($contentLength === 0) {
+        $upgradeConnection = $request->hasHeader('Connection') && $request->getHeaderLine('Connection') === 'Upgrade';
+
+        if (!$upgradeConnection && $contentLength === 0) {
             // If Body is empty or Content-Length is 0 and won't emit further data,
             // 'data' events from other streams won't be called anymore
             $stream->emit('end');
@@ -349,7 +351,7 @@ public function handleResponse(ConnectionInterface $connection, RequestInterface
 
         // HTTP/1.1 assumes persistent connection support by default
         // we do not support persistent connections, so let the client know
-        if ($request->getProtocolVersion() === '1.1') {
+        if ($request->getProtocolVersion() === '1.1' && $response->getStatusCode() !== 101) {
             $response = $response->withHeader('Connection', 'close');
         }
 
@@ -359,9 +361,12 @@ public function handleResponse(ConnectionInterface $connection, RequestInterface
             $response = $response->withoutHeader('Content-Length')->withoutHeader('Transfer-Encoding');
         }
 
-        // response to HEAD and 1xx, 204 and 304 responses MUST NOT include a body
-        if ($request->getMethod() === 'HEAD' || ($code >= 100 && $code < 200) || $code === 204 || $code === 304) {
-            $response = $response->withBody(Psr7Implementation\stream_for(''));
+        // 101 response (Upgrade) should hold onto the body
+        if ($code !== 101) {
+            // response to HEAD and 1xx, 204 and 304 responses MUST NOT include a body
+            if ($request->getMethod() === 'HEAD' || ($code >= 100 && $code < 200) || $code === 204 || $code === 304) {
+                $response = $response->withBody(Psr7Implementation\stream_for(''));
+            }
         }
 
         $this->handleResponseBody($response, $connection);

From 96d8d26110bbbcade59d5055ea30ac19e85281a8 Mon Sep 17 00:00:00 2001
From: Matt Bonneau <matt@bonneau.net>
Date: Sun, 9 Apr 2017 15:49:05 -0400
Subject: [PATCH 2/3] Add tests and validation

---
 src/Server.php       |  86 +++++++++++++--
 tests/ServerTest.php | 242 +++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 318 insertions(+), 10 deletions(-)

diff --git a/src/Server.php b/src/Server.php
index 4d4f0a11..f1912357 100644
--- a/src/Server.php
+++ b/src/Server.php
@@ -242,6 +242,16 @@ public function handleRequest(ConnectionInterface $conn, RequestInterface $reque
             $stream = new LengthLimitedStream($stream, $contentLength);
         }
 
+        $upgradeRequest = false;
+        if ($request->getProtocolVersion() !== '1.0' && $request->hasHeader('Connection') && strtolower($request->getHeaderLine('Connection')) === "upgrade") {
+            if (!$request->hasHeader('Upgrade') || empty($request->getHeaderLine('Upgrade'))) {
+                // MUST have Upgrade options
+                $this->emit('error', array(new \InvalidArgumentException('Connection upgrade must specify upgrade protocol.')));
+                return $this->writeError($conn, 400, $request);
+            }
+            $upgradeRequest = true;
+        }
+
         $request = $request->withBody(new HttpBodyStream($stream, $contentLength));
 
         if ($request->getProtocolVersion() !== '1.0' && '100-continue' === strtolower($request->getHeaderLine('Expect'))) {
@@ -261,7 +271,7 @@ public function handleRequest(ConnectionInterface $conn, RequestInterface $reque
 
         $that = $this;
         $promise->then(
-            function ($response) use ($that, $conn, $request) {
+            function ($response) use ($that, $conn, $request, $contentLength, $stream, $upgradeRequest) {
                 if (!$response instanceof ResponseInterface) {
                     $message = 'The response callback is expected to resolve with an object implementing Psr\Http\Message\ResponseInterface, but resolved with "%s" instead.';
                     $message = sprintf($message, is_object($response) ? get_class($response) : gettype($response));
@@ -270,6 +280,71 @@ function ($response) use ($that, $conn, $request) {
                     $that->emit('error', array($exception));
                     return $that->writeError($conn, 500, $request);
                 }
+
+                if ($response->getStatusCode() === 426) {
+                    if (!$response->hasHeader('Upgrade') || $response->getHeaderLine('Upgrade') === '') {
+                        $message = 'HTTP 1.1 426 response requires `Upgrade` header.';
+                        $exception = new \RuntimeException($message);
+
+                        $that->emit('error', array($exception));
+                        return $that->writeError($conn, 500, $request);
+                    }
+                }
+
+                $upgradeConnection = false;
+                if ($response->getStatusCode() === 101) {
+                    if (!$upgradeRequest) {
+                        $message = 'HTTP status 101 is not valid when no upgrade was requested';
+                        $exception = new \RuntimeException($message);
+
+                        $that->emit('error', array($exception));
+                        return $that->writeError($conn, 500, $request);
+                    }
+
+                    if ($response->getProtocolVersion() === '1.0') {
+                        $message = 'HTTP status 101 is not valid with protocol version 1.0';
+                        $exception = new \RuntimeException($message);
+
+                        $that->emit('error', array($exception));
+                        return $that->writeError($conn, 500, $request);
+                    }
+
+                    if (!$response->hasHeader('Connection') || strtolower($response->getHeaderLine('Connection')) !== 'upgrade') {
+                        $message = 'HTTP 1.1 Upgrade requires `Connection: upgrade` header.';
+                        $exception = new \RuntimeException($message);
+
+                        $that->emit('error', array($exception));
+                        return $that->writeError($conn, 500, $request);
+                    }
+
+                    if (!$response->hasHeader('Upgrade') || $response->getHeaderLine('Upgrade') === '') {
+                        $message = 'HTTP 1.1 Upgrade requires `Upgrade` header with exactly one protocol specified.';
+                        $exception = new \RuntimeException($message);
+
+                        $that->emit('error', array($exception));
+                        return $that->writeError($conn, 500, $request);
+                    }
+
+                    $requestedProtocols = explode(',', preg_replace('/\s+/', '', $request->getHeaderLine('Upgrade')));
+
+                    if (!in_array(trim($response->getHeaderLine('Upgrade')), $requestedProtocols)) {
+                        $message = 'Upgrade requires response protocol to be one of the `Upgrade` protocols specified by the request.';
+                        $exception = new \RuntimeException($message);
+
+                        $that->emit('error', array($exception));
+                        return $that->writeError($conn, 500, $request);
+                    }
+
+                    $upgradeConnection = true;
+                }
+
+                if (!$upgradeConnection && $contentLength === 0) {
+                    // If Body is empty or Content-Length is 0 and won't emit further data,
+                    // 'data' events from other streams won't be called anymore
+                    $stream->emit('end');
+                    $stream->close();
+                }
+
                 $that->handleResponse($conn, $request, $response);
             },
             function ($error) use ($that, $conn, $request) {
@@ -281,15 +356,6 @@ function ($error) use ($that, $conn, $request) {
                 return $that->writeError($conn, 500, $request);
             }
         );
-
-        $upgradeConnection = $request->hasHeader('Connection') && $request->getHeaderLine('Connection') === 'Upgrade';
-
-        if (!$upgradeConnection && $contentLength === 0) {
-            // If Body is empty or Content-Length is 0 and won't emit further data,
-            // 'data' events from other streams won't be called anymore
-            $stream->emit('end');
-            $stream->close();
-        }
     }
 
     /** @internal */
diff --git a/tests/ServerTest.php b/tests/ServerTest.php
index b7ddac2f..62608cf8 100644
--- a/tests/ServerTest.php
+++ b/tests/ServerTest.php
@@ -2229,6 +2229,248 @@ function ($data) use (&$buffer) {
         $this->assertInstanceOf('RuntimeException', $exception);
     }
 
+    private function getUpgradeHeader()
+    {
+        $data = "GET / HTTP/1.1\r\n";
+        $data .= "Host: localhost\r\n";
+        $data .= "Connection: Upgrade\r\n";
+        $data .= "Upgrade: echo\r\n\r\n";
+
+        return $data;
+    }
+
+    public function testConnectionUpgradeEcho()
+    {
+        $server = new Server($this->socket, function (RequestInterface $request) {
+            $responseStream = new ReadableStream();
+            $request->getBody()->on('data', function ($data) use ($responseStream) {
+                $responseStream->emit('data', [$data]);
+            });
+
+            $this->assertEquals('Upgrade', $request->getHeaderLine('Connection'));
+            $this->assertEquals('echo', $request->getHeaderLine('Upgrade'));
+
+            $response = new Response(
+                101,
+                array(
+                    'Connection' => 'Upgrade',
+                    'Upgrade'    => 'echo'
+                ),
+                $responseStream);
+            return $response;
+        });
+
+        $buffer = '';
+        $this->connection
+            ->expects($this->any())
+            ->method('write')
+            ->will(
+                $this->returnCallback(
+                    function ($data) use (&$buffer) {
+                        $buffer .= $data;
+                    }
+                )
+            );
+
+        $this->socket->emit('connection', array($this->connection));
+
+        $this->connection->emit('data', array($this->getUpgradeHeader()));
+
+        $this->connection->emit('data', array('text to be echoed'));
+
+        $this->assertStringStartsWith("HTTP/1.1 101 Switching Protocols\r\n", $buffer);
+        $this->assertContains("\r\nConnection: Upgrade\r\n", $buffer);
+        $this->assertContains("\r\nUpgrade: echo\r\n", $buffer);
+        $this->assertStringEndsWith("\r\n\r\ntext to be echoed", $buffer);
+    }
+
+    public function testUpgradeWithNoProtocolRespondsWithError()
+    {
+        $server = new Server($this->socket, function (RequestInterface $request) {
+            $this->fail('Callback should not be called');
+        });
+
+        $exception = null;
+        $server->on('error', function (\Exception $ex) use (&$exception) {
+            $exception = $ex;
+        });
+
+        $buffer = '';
+        $this->connection
+            ->expects($this->any())
+            ->method('write')
+            ->will(
+                $this->returnCallback(
+                    function ($data) use (&$buffer) {
+                        $buffer .= $data;
+                    }
+                )
+            );
+
+        $this->socket->emit('connection', array($this->connection));
+
+        $data = "GET / HTTP/1.1\r\n";
+        $data .= "Host: localhost\r\n";
+        $data .= "Connection: Upgrade\r\n\r\n";
+
+        $this->connection->emit('data', array($this->getUpgradeHeader()));
+
+        $this->assertStringStartsWith("HTTP/1.1 500 Internal Server Error\r\n", $buffer);
+        $this->assertInstanceOf('RuntimeException', $exception);
+    }
+
+    public function testUpgrade101MustContainUpgradeHeaderWithNewProtocol()
+    {
+        $server = new Server($this->socket, function (RequestInterface $request) {
+            $responseStream = new ReadableStream();
+            $this->assertEquals('Upgrade', $request->getHeaderLine('Connection'));
+            $this->assertEquals('echo', $request->getHeaderLine('Upgrade'));
+
+            $response = new Response(
+                101,
+                array(
+                    'Connection' => 'Upgrade'
+                ),
+                $responseStream);
+            return $response;
+        });
+
+        $exception = null;
+        $server->on('error', function (\Exception $ex) use (&$exception) {
+            $exception = $ex;
+        });
+
+        $buffer = '';
+        $this->connection
+            ->expects($this->any())
+            ->method('write')
+            ->will(
+                $this->returnCallback(
+                    function ($data) use (&$buffer) {
+                        $buffer .= $data;
+                    }
+                )
+            );
+
+        $this->socket->emit('connection', array($this->connection));
+
+        $this->connection->emit('data', array($this->getUpgradeHeader()));
+
+        $this->assertStringStartsWith("HTTP/1.1 500 Internal Server Error\r\n", $buffer);
+        $this->assertInstanceOf('RuntimeException', $exception);
+    }
+
+    public function testUpgradeProtocolMustBeOneRequested()
+    {
+        $server = new Server($this->socket, function (RequestInterface $request) {
+            $responseStream = new ReadableStream();
+            $this->assertEquals('Upgrade', $request->getHeaderLine('Connection'));
+            $this->assertEquals('echo', $request->getHeaderLine('Upgrade'));
+
+            $response = new Response(
+                101,
+                array(
+                    'Connection' => 'Upgrade',
+                    'Upgrade'    => 'notecho'
+                ),
+                $responseStream);
+            return $response;
+        });
+
+        $exception = null;
+        $server->on('error', function (\Exception $ex) use (&$exception) {
+            $exception = $ex;
+        });
+
+        $buffer = '';
+        $this->connection
+            ->expects($this->any())
+            ->method('write')
+            ->will(
+                $this->returnCallback(
+                    function ($data) use (&$buffer) {
+                        $buffer .= $data;
+                    }
+                )
+            );
+
+        $this->socket->emit('connection', array($this->connection));
+
+        $this->connection->emit('data', array($this->getUpgradeHeader()));
+
+        $this->assertStringStartsWith("HTTP/1.1 500 Internal Server Error\r\n", $buffer);
+        $this->assertInstanceOf('RuntimeException', $exception);
+    }
+
+    public function testUpgrade426WithUpgradeHeader()
+    {
+        $server = new Server($this->socket, function (RequestInterface $request) {
+            $response = new Response(
+                426,
+                array(
+                    'Upgrade' => 'something'
+                ));
+            return $response;
+        });
+
+        $exception = null;
+        $server->on('error', function (\Exception $ex) use (&$exception) {
+            $exception = $ex;
+        });
+
+        $buffer = '';
+        $this->connection
+            ->expects($this->any())
+            ->method('write')
+            ->will(
+                $this->returnCallback(
+                    function ($data) use (&$buffer) {
+                        $buffer .= $data;
+                    }
+                )
+            );
+
+        $this->socket->emit('connection', array($this->connection));
+
+        $this->connection->emit('data', array($this->getUpgradeHeader()));
+
+        $this->assertStringStartsWith("HTTP/1.1 426 Upgrade Required\r\n", $buffer);
+    }
+
+    public function testUpgrade426MustContainUpgradeHeaderWithProtocol()
+    {
+        $server = new Server($this->socket, function (RequestInterface $request) {
+            $response = new Response(
+                426,
+                array());
+            return $response;
+        });
+
+        $exception = null;
+        $server->on('error', function (\Exception $ex) use (&$exception) {
+            $exception = $ex;
+        });
+
+        $buffer = '';
+        $this->connection
+            ->expects($this->any())
+            ->method('write')
+            ->will(
+                $this->returnCallback(
+                    function ($data) use (&$buffer) {
+                        $buffer .= $data;
+                    }
+                )
+            );
+
+        $this->socket->emit('connection', array($this->connection));
+
+        $this->connection->emit('data', array($this->getUpgradeHeader()));
+
+        $this->assertStringStartsWith("HTTP/1.1 500 Internal Server Error\r\n", $buffer);
+        $this->assertInstanceOf('RuntimeException', $exception);
+    }
+
     private function createGetRequest()
     {
         $data = "GET / HTTP/1.1\r\n";

From 079a20c61c9bc780d5139d3cefeb01f17e5c681a Mon Sep 17 00:00:00 2001
From: Matt Bonneau <matt@bonneau.net>
Date: Sun, 9 Apr 2017 16:01:23 -0400
Subject: [PATCH 3/3] Support PHP 5.3

---
 src/Server.php       |  2 +-
 tests/ServerTest.php | 28 ++++++++++++++++------------
 2 files changed, 17 insertions(+), 13 deletions(-)

diff --git a/src/Server.php b/src/Server.php
index f1912357..e24bf3a6 100644
--- a/src/Server.php
+++ b/src/Server.php
@@ -244,7 +244,7 @@ public function handleRequest(ConnectionInterface $conn, RequestInterface $reque
 
         $upgradeRequest = false;
         if ($request->getProtocolVersion() !== '1.0' && $request->hasHeader('Connection') && strtolower($request->getHeaderLine('Connection')) === "upgrade") {
-            if (!$request->hasHeader('Upgrade') || empty($request->getHeaderLine('Upgrade'))) {
+            if (!$request->hasHeader('Upgrade') || $request->getHeaderLine('Upgrade') === '') {
                 // MUST have Upgrade options
                 $this->emit('error', array(new \InvalidArgumentException('Connection upgrade must specify upgrade protocol.')));
                 return $this->writeError($conn, 400, $request);
diff --git a/tests/ServerTest.php b/tests/ServerTest.php
index 62608cf8..22a35457 100644
--- a/tests/ServerTest.php
+++ b/tests/ServerTest.php
@@ -2241,14 +2241,15 @@ private function getUpgradeHeader()
 
     public function testConnectionUpgradeEcho()
     {
-        $server = new Server($this->socket, function (RequestInterface $request) {
+        $that = $this;
+        $server = new Server($this->socket, function (RequestInterface $request) use ($that) {
             $responseStream = new ReadableStream();
             $request->getBody()->on('data', function ($data) use ($responseStream) {
-                $responseStream->emit('data', [$data]);
+                $responseStream->emit('data', array($data));
             });
 
-            $this->assertEquals('Upgrade', $request->getHeaderLine('Connection'));
-            $this->assertEquals('echo', $request->getHeaderLine('Upgrade'));
+            $that->assertEquals('Upgrade', $request->getHeaderLine('Connection'));
+            $that->assertEquals('echo', $request->getHeaderLine('Upgrade'));
 
             $response = new Response(
                 101,
@@ -2286,8 +2287,9 @@ function ($data) use (&$buffer) {
 
     public function testUpgradeWithNoProtocolRespondsWithError()
     {
-        $server = new Server($this->socket, function (RequestInterface $request) {
-            $this->fail('Callback should not be called');
+        $that = $this;
+        $server = new Server($this->socket, function (RequestInterface $request) use ($that) {
+            $that->fail('Callback should not be called');
         });
 
         $exception = null;
@@ -2321,10 +2323,11 @@ function ($data) use (&$buffer) {
 
     public function testUpgrade101MustContainUpgradeHeaderWithNewProtocol()
     {
-        $server = new Server($this->socket, function (RequestInterface $request) {
+        $that = $this;
+        $server = new Server($this->socket, function (RequestInterface $request) use ($that) {
             $responseStream = new ReadableStream();
-            $this->assertEquals('Upgrade', $request->getHeaderLine('Connection'));
-            $this->assertEquals('echo', $request->getHeaderLine('Upgrade'));
+            $that->assertEquals('Upgrade', $request->getHeaderLine('Connection'));
+            $that->assertEquals('echo', $request->getHeaderLine('Upgrade'));
 
             $response = new Response(
                 101,
@@ -2362,10 +2365,11 @@ function ($data) use (&$buffer) {
 
     public function testUpgradeProtocolMustBeOneRequested()
     {
-        $server = new Server($this->socket, function (RequestInterface $request) {
+        $that = $this;
+        $server = new Server($this->socket, function (RequestInterface $request) use ($that) {
             $responseStream = new ReadableStream();
-            $this->assertEquals('Upgrade', $request->getHeaderLine('Connection'));
-            $this->assertEquals('echo', $request->getHeaderLine('Upgrade'));
+            $that->assertEquals('Upgrade', $request->getHeaderLine('Connection'));
+            $that->assertEquals('echo', $request->getHeaderLine('Upgrade'));
 
             $response = new Response(
                 101,