From b0d05928dd239c90b1ee57241d724182bba9b6e2 Mon Sep 17 00:00:00 2001 From: Joe Dixon Date: Wed, 24 Apr 2024 21:43:37 +0100 Subject: [PATCH] [1.x] Sends content length to address OpenSSL issue (#167) * send content length * test content-length --- .../Http/Controllers/ChannelController.php | 4 +-- .../Controllers/ChannelUsersController.php | 9 +++--- .../Http/Controllers/ChannelsController.php | 4 +-- .../Controllers/ConnectionsController.php | 4 +-- .../Controllers/EventsBatchController.php | 9 +++--- .../Http/Controllers/EventsController.php | 9 +++--- .../Controllers/UsersTerminateController.php | 7 ++--- src/Servers/Reverb/Http/Response.php | 18 ++++++++++++ .../Pusher/Reverb/ChannelControllerTest.php | 20 +++++++++++++ .../Reverb/ChannelUsersControllerTest.php | 28 +++++++++++++++++++ .../Pusher/Reverb/ChannelsControllerTest.php | 20 +++++++++++++ .../Reverb/ConnectionsControllerTest.php | 20 +++++++++++++ .../Reverb/EventsBatchControllerTest.php | 26 +++++++++++++++++ .../Pusher/Reverb/EventsControllerTest.php | 10 +++++++ .../Reverb/UsersTerminateControllerTest.php | 2 ++ 15 files changed, 165 insertions(+), 25 deletions(-) create mode 100644 src/Servers/Reverb/Http/Response.php diff --git a/src/Protocols/Pusher/Http/Controllers/ChannelController.php b/src/Protocols/Pusher/Http/Controllers/ChannelController.php index bbc3b15a..a4101936 100644 --- a/src/Protocols/Pusher/Http/Controllers/ChannelController.php +++ b/src/Protocols/Pusher/Http/Controllers/ChannelController.php @@ -4,9 +4,9 @@ use Laravel\Reverb\Protocols\Pusher\MetricsHandler; use Laravel\Reverb\Servers\Reverb\Http\Connection; +use Laravel\Reverb\Servers\Reverb\Http\Response; use Psr\Http\Message\RequestInterface; use React\Promise\PromiseInterface; -use Symfony\Component\HttpFoundation\JsonResponse; class ChannelController extends Controller { @@ -20,6 +20,6 @@ public function __invoke(RequestInterface $request, Connection $connection, stri return app(MetricsHandler::class)->gather($this->application, 'channel', [ 'channel' => $channel, 'info' => ($info = $this->query['info']) ? $info.',occupied' : 'occupied', - ])->then(fn ($channel) => new JsonResponse((object) $channel)); + ])->then(fn ($channel) => new Response((object) $channel)); } } diff --git a/src/Protocols/Pusher/Http/Controllers/ChannelUsersController.php b/src/Protocols/Pusher/Http/Controllers/ChannelUsersController.php index 8b979c9c..37999310 100644 --- a/src/Protocols/Pusher/Http/Controllers/ChannelUsersController.php +++ b/src/Protocols/Pusher/Http/Controllers/ChannelUsersController.php @@ -5,10 +5,9 @@ use Laravel\Reverb\Protocols\Pusher\Concerns\InteractsWithChannelInformation; use Laravel\Reverb\Protocols\Pusher\MetricsHandler; use Laravel\Reverb\Servers\Reverb\Http\Connection; +use Laravel\Reverb\Servers\Reverb\Http\Response; use Psr\Http\Message\RequestInterface; use React\Promise\PromiseInterface; -use Symfony\Component\HttpFoundation\JsonResponse; -use Symfony\Component\HttpFoundation\Response; class ChannelUsersController extends Controller { @@ -24,15 +23,15 @@ public function __invoke(RequestInterface $request, Connection $connection, stri $channel = $this->channels->find($channel); if (! $channel) { - return new JsonResponse((object) [], 404); + return new Response((object) [], 404); } if (! $this->isPresenceChannel($channel)) { - return new JsonResponse((object) [], 400); + return new Response((object) [], 400); } return app(MetricsHandler::class) ->gather($this->application, 'channel_users', ['channel' => $channel->name()]) - ->then(fn ($connections) => new JsonResponse(['users' => $connections])); + ->then(fn ($connections) => new Response(['users' => $connections])); } } diff --git a/src/Protocols/Pusher/Http/Controllers/ChannelsController.php b/src/Protocols/Pusher/Http/Controllers/ChannelsController.php index 80f3eac1..605291b5 100644 --- a/src/Protocols/Pusher/Http/Controllers/ChannelsController.php +++ b/src/Protocols/Pusher/Http/Controllers/ChannelsController.php @@ -4,9 +4,9 @@ use Laravel\Reverb\Protocols\Pusher\MetricsHandler; use Laravel\Reverb\Servers\Reverb\Http\Connection; +use Laravel\Reverb\Servers\Reverb\Http\Response; use Psr\Http\Message\RequestInterface; use React\Promise\PromiseInterface; -use Symfony\Component\HttpFoundation\JsonResponse; class ChannelsController extends Controller { @@ -20,6 +20,6 @@ public function __invoke(RequestInterface $request, Connection $connection, stri return app(MetricsHandler::class)->gather($this->application, 'channels', [ 'filter' => $this->query['filter_by_prefix'] ?? null, 'info' => $this->query['info'] ?? null, - ])->then(fn ($channels) => new JsonResponse(['channels' => array_map(fn ($item) => (object) $item, $channels)])); + ])->then(fn ($channels) => new Response(['channels' => array_map(fn ($item) => (object) $item, $channels)])); } } diff --git a/src/Protocols/Pusher/Http/Controllers/ConnectionsController.php b/src/Protocols/Pusher/Http/Controllers/ConnectionsController.php index 8def95e7..f547de51 100644 --- a/src/Protocols/Pusher/Http/Controllers/ConnectionsController.php +++ b/src/Protocols/Pusher/Http/Controllers/ConnectionsController.php @@ -4,9 +4,9 @@ use Laravel\Reverb\Protocols\Pusher\MetricsHandler; use Laravel\Reverb\Servers\Reverb\Http\Connection; +use Laravel\Reverb\Servers\Reverb\Http\Response; use Psr\Http\Message\RequestInterface; use React\Promise\PromiseInterface; -use Symfony\Component\HttpFoundation\JsonResponse; class ConnectionsController extends Controller { @@ -18,6 +18,6 @@ public function __invoke(RequestInterface $request, Connection $connection, stri $this->verify($request, $connection, $appId); return app(MetricsHandler::class)->gather($this->application, 'connections') - ->then(fn ($connections) => new JsonResponse(['connections' => count($connections)])); + ->then(fn ($connections) => new Response(['connections' => count($connections)])); } } diff --git a/src/Protocols/Pusher/Http/Controllers/EventsBatchController.php b/src/Protocols/Pusher/Http/Controllers/EventsBatchController.php index cb18dc42..1264e591 100644 --- a/src/Protocols/Pusher/Http/Controllers/EventsBatchController.php +++ b/src/Protocols/Pusher/Http/Controllers/EventsBatchController.php @@ -8,10 +8,9 @@ use Laravel\Reverb\Protocols\Pusher\EventDispatcher; use Laravel\Reverb\Protocols\Pusher\MetricsHandler; use Laravel\Reverb\Servers\Reverb\Http\Connection; +use Laravel\Reverb\Servers\Reverb\Http\Response; use Psr\Http\Message\RequestInterface; use React\Promise\PromiseInterface; -use Symfony\Component\HttpFoundation\JsonResponse; -use Symfony\Component\HttpFoundation\Response; use function React\Promise\all; @@ -31,7 +30,7 @@ public function __invoke(RequestInterface $request, Connection $connection, stri $validator = $this->validator($payload); if ($validator->fails()) { - return new JsonResponse($validator->errors(), 422); + return new Response($validator->errors(), 422); } $items = collect($payload['batch']); @@ -56,11 +55,11 @@ public function __invoke(RequestInterface $request, Connection $connection, stri if ($items->contains(fn ($item) => ! empty($item))) { return all($items)->then(function ($items) { - return new JsonResponse(['batch' => array_map(fn ($item) => (object) $item, $items)]); + return new Response(['batch' => array_map(fn ($item) => (object) $item, $items)]); }); } - return new JsonResponse(['batch' => (object) []]); + return new Response(['batch' => (object) []]); } /** diff --git a/src/Protocols/Pusher/Http/Controllers/EventsController.php b/src/Protocols/Pusher/Http/Controllers/EventsController.php index 285f42ca..cefa1747 100644 --- a/src/Protocols/Pusher/Http/Controllers/EventsController.php +++ b/src/Protocols/Pusher/Http/Controllers/EventsController.php @@ -9,10 +9,9 @@ use Laravel\Reverb\Protocols\Pusher\EventDispatcher; use Laravel\Reverb\Protocols\Pusher\MetricsHandler; use Laravel\Reverb\Servers\Reverb\Http\Connection; +use Laravel\Reverb\Servers\Reverb\Http\Response; use Psr\Http\Message\RequestInterface; use React\Promise\PromiseInterface; -use Symfony\Component\HttpFoundation\JsonResponse; -use Symfony\Component\HttpFoundation\Response; class EventsController extends Controller { @@ -30,7 +29,7 @@ public function __invoke(RequestInterface $request, Connection $connection, stri $validator = $this->validator($payload); if ($validator->fails()) { - return new JsonResponse($validator->errors(), 422); + return new Response($validator->errors(), 422); } $channels = Arr::wrap($payload['channels'] ?? $payload['channel'] ?? []); @@ -51,10 +50,10 @@ public function __invoke(RequestInterface $request, Connection $connection, stri if (isset($payload['info'])) { return app(MetricsHandler::class) ->gather($this->application, 'channels', ['info' => $payload['info'], 'channels' => $channels]) - ->then(fn ($channels) => new JsonResponse(['channels' => array_map(fn ($channel) => (object) $channel, $channels)])); + ->then(fn ($channels) => new Response(['channels' => array_map(fn ($channel) => (object) $channel, $channels)])); } - return new JsonResponse((object) []); + return new Response((object) []); } /** diff --git a/src/Protocols/Pusher/Http/Controllers/UsersTerminateController.php b/src/Protocols/Pusher/Http/Controllers/UsersTerminateController.php index 8a03b382..12e6cd4c 100644 --- a/src/Protocols/Pusher/Http/Controllers/UsersTerminateController.php +++ b/src/Protocols/Pusher/Http/Controllers/UsersTerminateController.php @@ -5,10 +5,9 @@ use Laravel\Reverb\ServerProviderManager; use Laravel\Reverb\Servers\Reverb\Contracts\PubSubProvider; use Laravel\Reverb\Servers\Reverb\Http\Connection; +use Laravel\Reverb\Servers\Reverb\Http\Response; use Psr\Http\Message\RequestInterface; use React\Promise\PromiseInterface; -use Symfony\Component\HttpFoundation\JsonResponse; -use Symfony\Component\HttpFoundation\Response; class UsersTerminateController extends Controller { @@ -24,7 +23,7 @@ public function __invoke(RequestInterface $request, Connection $connection, stri 'type' => 'terminate', 'application' => serialize($this->application), 'payload' => ['user_id' => $userId], - ])->then(fn () => new JsonResponse((object) [])); + ])->then(fn () => new Response((object) [])); } $connections = collect($this->channels->connections()); @@ -35,6 +34,6 @@ public function __invoke(RequestInterface $request, Connection $connection, stri } }); - return new JsonResponse((object) []); + return new Response((object) []); } } diff --git a/src/Servers/Reverb/Http/Response.php b/src/Servers/Reverb/Http/Response.php new file mode 100644 index 00000000..5604910e --- /dev/null +++ b/src/Servers/Reverb/Http/Response.php @@ -0,0 +1,18 @@ +headers->set('Content-Length', strlen($this->content)); + } +} \ No newline at end of file diff --git a/tests/Feature/Protocols/Pusher/Reverb/ChannelControllerTest.php b/tests/Feature/Protocols/Pusher/Reverb/ChannelControllerTest.php index ca397ec0..ea810525 100644 --- a/tests/Feature/Protocols/Pusher/Reverb/ChannelControllerTest.php +++ b/tests/Feature/Protocols/Pusher/Reverb/ChannelControllerTest.php @@ -59,6 +59,15 @@ expect($response->getBody()->getContents())->toBe('{"occupied":true,"subscription_count":1}'); }); +it('can send the content-length header', function () { + subscribe('test-channel-one'); + subscribe('test-channel-one'); + + $response = await($this->signedRequest('channels/test-channel-one?info=user_count,subscription_count,cache')); + + expect($response->getHeader('Content-Length'))->toBe(['40']); +}); + it('can gather data for a single channel', function () { $this->usingRedis(); @@ -121,3 +130,14 @@ expect($response->getStatusCode())->toBe(200); expect($response->getBody()->getContents())->toBe('{"occupied":true,"subscription_count":1}'); }); + +it('can send the content-length header when gathering results', function () { + $this->usingRedis(); + + subscribe('test-channel-one'); + subscribe('test-channel-one'); + + $response = await($this->signedRequest('channels/test-channel-one?info=user_count,subscription_count,cache')); + + expect($response->getHeader('Content-Length'))->toBe(['40']); +}); diff --git a/tests/Feature/Protocols/Pusher/Reverb/ChannelUsersControllerTest.php b/tests/Feature/Protocols/Pusher/Reverb/ChannelUsersControllerTest.php index 7a9d31c9..e8ebcb9a 100644 --- a/tests/Feature/Protocols/Pusher/Reverb/ChannelUsersControllerTest.php +++ b/tests/Feature/Protocols/Pusher/Reverb/ChannelUsersControllerTest.php @@ -61,6 +61,19 @@ await($this->signedRequest('channels/presence-test-channel/users')); })->throws(ResponseException::class); +it('can send the content-length header', function () { + $channel = app(ChannelManager::class) + ->for(app()->make(ApplicationProvider::class)->findByKey('reverb-key')) + ->findOrCreate('presence-test-channel'); + $channel->subscribe($connection = new FakeConnection('test-connection-one'), validAuth($connection->id(), 'presence-test-channel', $data = json_encode(['user_id' => 1, 'user_info' => ['name' => 'Taylor']])), $data); + $channel->subscribe($connection = new FakeConnection('test-connection-two'), validAuth($connection->id(), 'presence-test-channel', $data = json_encode(['user_id' => 2, 'user_info' => ['name' => 'Joe']])), $data); + $channel->subscribe($connection = new FakeConnection('test-connection-three'), validAuth($connection->id(), 'presence-test-channel', $data = json_encode(['user_id' => 3, 'user_info' => ['name' => 'Jess']])), $data); + + $response = await($this->signedRequest('channels/presence-test-channel/users')); + + expect($response->getHeader('Content-Length'))->toBe(['38']); +}); + it('gathers the user data', function () { $this->usingRedis(); @@ -92,3 +105,18 @@ expect($response->getStatusCode())->toBe(200); expect($response->getBody()->getContents())->toBe('{"users":[{"id":2},{"id":3}]}'); }); + +it('can send the content-length header when gathering results', function () { + $this->usingRedis(); + + $channel = app(ChannelManager::class) + ->for(app()->make(ApplicationProvider::class)->findByKey('reverb-key')) + ->findOrCreate('presence-test-channel'); + $channel->subscribe($connection = new FakeConnection('test-connection-one'), validAuth($connection->id(), 'presence-test-channel', $data = json_encode(['user_id' => 1, 'user_info' => ['name' => 'Taylor']])), $data); + $channel->subscribe($connection = new FakeConnection('test-connection-two'), validAuth($connection->id(), 'presence-test-channel', $data = json_encode(['user_id' => 2, 'user_info' => ['name' => 'Joe']])), $data); + $channel->subscribe($connection = new FakeConnection('test-connection-three'), validAuth($connection->id(), 'presence-test-channel', $data = json_encode(['user_id' => 3, 'user_info' => ['name' => 'Jess']])), $data); + + $response = await($this->signedRequest('channels/presence-test-channel/users')); + + expect($response->getHeader('Content-Length'))->toBe(['38']); +}); diff --git a/tests/Feature/Protocols/Pusher/Reverb/ChannelsControllerTest.php b/tests/Feature/Protocols/Pusher/Reverb/ChannelsControllerTest.php index 7759d5b1..232115ed 100644 --- a/tests/Feature/Protocols/Pusher/Reverb/ChannelsControllerTest.php +++ b/tests/Feature/Protocols/Pusher/Reverb/ChannelsControllerTest.php @@ -51,6 +51,15 @@ expect($response->getBody()->getContents())->toBe('{"channels":{"test-channel-two":{}}}'); }); +it('can send the content-length header', function () { + subscribe('test-channel-one'); + subscribe('presence-test-channel-two'); + + $response = await($this->signedRequest('channels?info=user_count')); + + expect($response->getHeader('Content-Length'))->toBe(['81']); +}); + it('can gather all channel information', function () { $this->usingRedis(); @@ -102,3 +111,14 @@ expect($response->getStatusCode())->toBe(200); expect($response->getBody()->getContents())->toBe('{"channels":{"test-channel-two":{}}}'); }); + +it('can send the content-length header when gathering results', function () { + $this->usingRedis(); + + subscribe('test-channel-one'); + subscribe('presence-test-channel-two'); + + $response = await($this->signedRequest('channels?info=user_count')); + + expect($response->getHeader('Content-Length'))->toBe(['81']); +}); diff --git a/tests/Feature/Protocols/Pusher/Reverb/ConnectionsControllerTest.php b/tests/Feature/Protocols/Pusher/Reverb/ConnectionsControllerTest.php index 345cfa96..b08afc8a 100644 --- a/tests/Feature/Protocols/Pusher/Reverb/ConnectionsControllerTest.php +++ b/tests/Feature/Protocols/Pusher/Reverb/ConnectionsControllerTest.php @@ -27,6 +27,15 @@ expect($response->getBody()->getContents())->toBe('{"connections":1}'); }); +it('can send the content-length header', function () { + subscribe('test-channel-one'); + subscribe('presence-test-channel-two'); + + $response = await($this->signedRequest('connections')); + + expect($response->getHeader('Content-Length'))->toBe(['17']); +}); + it('can gather a connection count', function () { $this->usingRedis(); @@ -51,3 +60,14 @@ expect($response->getStatusCode())->toBe(200); expect($response->getBody()->getContents())->toBe('{"connections":1}'); }); + +it('can send the content-length header when gathering results', function () { + $this->usingRedis(); + + subscribe('test-channel-one'); + subscribe('presence-test-channel-two'); + + $response = await($this->signedRequest('connections')); + + expect($response->getHeader('Content-Length'))->toBe(['17']); +}); \ No newline at end of file diff --git a/tests/Feature/Protocols/Pusher/Reverb/EventsBatchControllerTest.php b/tests/Feature/Protocols/Pusher/Reverb/EventsBatchControllerTest.php index 2aadf7dc..efa6c631 100644 --- a/tests/Feature/Protocols/Pusher/Reverb/EventsBatchControllerTest.php +++ b/tests/Feature/Protocols/Pusher/Reverb/EventsBatchControllerTest.php @@ -87,6 +87,18 @@ expect($response->getBody()->getContents())->toBe('{"batch":[{"user_count":1},{}]}'); }); +it('can send the content-length header', function () { + $response = await($this->signedPostRequest('batch_events', ['batch' => [ + [ + 'name' => 'NewEvent', + 'channel' => 'test-channel', + 'data' => json_encode(['some' => 'data']), + ], + ]])); + + expect($response->getHeader('Content-Length'))->toBe(['12']); +}); + it('can receive an event batch trigger with multiple events and gather info for each', function () { $this->usingRedis(); @@ -145,3 +157,17 @@ expect($response->getStatusCode())->toBe(500); })->throws(ResponseException::class, exceptionCode: 500); + +it('can send the content-length header when gathering results', function () { + $this->usingRedis(); + + $response = await($this->signedPostRequest('batch_events', ['batch' => [ + [ + 'name' => 'NewEvent', + 'channel' => 'test-channel', + 'data' => json_encode(['some' => 'data']), + ], + ]])); + + expect($response->getHeader('Content-Length'))->toBe(['12']); +}); diff --git a/tests/Feature/Protocols/Pusher/Reverb/EventsControllerTest.php b/tests/Feature/Protocols/Pusher/Reverb/EventsControllerTest.php index f6df3122..a34487c0 100644 --- a/tests/Feature/Protocols/Pusher/Reverb/EventsControllerTest.php +++ b/tests/Feature/Protocols/Pusher/Reverb/EventsControllerTest.php @@ -197,3 +197,13 @@ it('fails when app cannot be found', function () { await($this->signedPostRequest('events', appId: 'invalid-app-id')); })->throws(ResponseException::class, exceptionCode: 404); + +it('can send the content-length header', function () { + $response = await($this->signedPostRequest('events', [ + 'name' => 'NewEvent', + 'channel' => 'test-channel', + 'data' => json_encode(['some' => 'data']), + ])); + + expect($response->getHeader('Content-Length'))->toBe(['2']); +}); diff --git a/tests/Feature/Protocols/Pusher/Reverb/UsersTerminateControllerTest.php b/tests/Feature/Protocols/Pusher/Reverb/UsersTerminateControllerTest.php index 9dd3ec18..99851799 100644 --- a/tests/Feature/Protocols/Pusher/Reverb/UsersTerminateControllerTest.php +++ b/tests/Feature/Protocols/Pusher/Reverb/UsersTerminateControllerTest.php @@ -29,6 +29,7 @@ expect($response->getBody()->getContents())->toBe('{}'); expect(collect(channels()->all())->get('presence-test-channel-one')->connections())->toHaveCount(1); expect(collect(channels()->all())->get('test-channel-two')->connections())->toHaveCount(1); + expect($response->getHeader('Content-Length'))->toBe(['2']); }); it('unsubscribes from all channels across all servers and terminates a user', function () { @@ -51,4 +52,5 @@ expect($response->getBody()->getContents())->toBe('{}'); expect(collect(channels()->all())->get('presence-test-channel-one')->connections())->toHaveCount(1); expect(collect(channels()->all())->get('test-channel-two')->connections())->toHaveCount(1); + expect($response->getHeader('Content-Length'))->toBe(['2']); });