├── .gitignore ├── .idea ├── .gitignore ├── laravel-grip.iml ├── laravel-plugin.xml ├── misc.xml ├── modules.xml ├── php-test-framework.xml ├── php.xml ├── runConfigurations.xml └── vcs.xml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json ├── composer.lock ├── config └── config.php ├── examples ├── http-stream │ └── README.md └── ws-over-http │ └── README.md ├── laravel-grip.iml ├── phpunit.xml ├── src ├── Errors │ ├── ConfigError.php │ ├── GripInstructAlreadyStartedError.php │ └── GripInstructNotAvailableError.php ├── Grip │ └── PrefixedPublisher.php ├── GripContext.php ├── Http │ └── Middleware │ │ ├── Facades │ │ ├── Grip.php │ │ ├── GripInstruct.php │ │ ├── GripPublisher.php │ │ └── GripWebSocket.php │ │ └── GripMiddleware.php └── LaravelGripServiceProvider.php └── tests ├── Feature └── GripMiddlewareInstallTest.php ├── TestCase.php └── Unit ├── GripMiddlewareTest.php ├── PrefixedPublisherTest.php └── ResolvePublisherTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | .phpunit.result.cache 3 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/laravel-grip.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /.idea/laravel-plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/php-test-framework.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.idea/php.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 123 | 124 | 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # laravel-grip Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [unreleased] 9 | 10 | ## [2.1.0] - 2023-12-16 11 | 12 | ### Added 13 | - Added support for verify-iss. 14 | - Updated with full support for Fastly Fanout. 15 | 16 | ## [2.0.0] - 2021-08-31 17 | 18 | ### Changed 19 | - Updated architecture 20 | - Added unit and feature tests 21 | - Minimum required Laravel version is 7. 22 | 23 | [unreleased]: https://github.com/fanout/laravel-grip/v2.1.0...HEAD 24 | [2.1.0]: https://github.com/fanout/laravel-grip/2.0.0...v2.1.0 25 | [2.0.0]: https://github.com/fanout/laravel-grip/releases/tag/2.0.0 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Fanout, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## laravel-grip 2 | 3 | GRIP library for [Laravel](https://laravel.com/), provided as a Laravel package. 4 | 5 | Minimum supported version of Laravel is 7.0, but it may work with older versions. 6 | 7 | Supported GRIP servers include: 8 | 9 | * [Pushpin](http://pushpin.org/) 10 | * [Fastly Fanout](https://docs.fastly.com/products/fanout) 11 | 12 | This library also supports legacy services hosted by [Fanout](https://fanout.io/) Cloud. 13 | 14 | Authors: Katsuyuki Omuro , Madeline Boby 15 | 16 | ### Introduction 17 | 18 | [GRIP](https://pushpin.org/docs/protocols/grip/) is a protocol that enables a web service to 19 | delegate realtime push behavior to a proxy component, using HTTP and headers. 20 | 21 | `laravel-grip` parses the `Grip-Sig` header in any requests to detect if they came 22 | through a GRIP proxy, and provides your route handler with tools to handle such requests. 23 | This includes access to information about whether the current request is proxied or is signed, 24 | as well as methods to issue any hold instructions to the GRIP proxy. 25 | 26 | Additionally, `laravel-grip` also handles 27 | [WebSocket-Over-HTTP processing](https://pushpin.org/docs/protocols/websocket-over-http/) so 28 | that WebSocket connections managed by the GRIP proxy can be controlled by your route handlers. 29 | 30 | ### Installation 31 | 32 | Install the library. 33 | 34 | ```sh 35 | composer require fanout/laravel-grip 36 | ``` 37 | 38 | This brings in the library, as well as installs the middleware into your Laravel application's stack 39 | by using the providers mechanism of Composer. 40 | 41 | #### Configuration 42 | 43 | `laravel-grip` can be configured by adding a file called `./config/grip.php` to your Laravel 44 | application. It should look like this: 45 | 46 | ```php 47 | /* string, array, or array of arrays */, 51 | 'prefix' => /* string. defaults to the empty string */, 52 | 'grip_proxy_required' => /* boolean, defaults to false */, 53 | ]; 54 | ``` 55 | 56 | Available options: 57 | | Key | Value | 58 | | --- | --- | 59 | | `grip` | A definition of GRIP proxies used to publish messages. See below for details. | 60 | | `prefix` | An optional string that will be prepended to the name of channels being published to. This can be used for namespacing. Defaults to `''`. | 61 | | `grip_proxy_required` | A boolean value representing whether all incoming requests should require that they be called behind a GRIP proxy. If this is true and a GRIP proxy is not detected, then a `501 Not Implemented` error will be issued. Defaults to `false`. | 62 | 63 | The `grip` parameter may be provided as any of the following: 64 | 65 | 1. An object with the following fields: 66 | 67 | | Key | Value | 68 | |---------------|---------------------------------------------------------------------------------| 69 | | `control_uri` | The Control URI of the GRIP client. | 70 | | `control_iss` | (optional) The Control ISS, if required by the GRIP client. | 71 | | `key` | (optional) The key to use with the Control ISS, if required by the GRIP client. | 72 | | `verify_iss` | (optional) The ISS to use when validating a GRIP signature. | 73 | | `verify_key` | (optional) The key to use when validating a GRIP signature. | 74 | 75 | 2. An array of such objects. 76 | 77 | 3. A GRIP URI, which is a string that encodes the above as a single string. 78 | 79 | ### Handling a route 80 | 81 | The middleware will automatically be installed before all of your routes. 82 | 83 | When your route runs, you will have access to the following facades: 84 | `Grip`, `GripInstruct`, `GripPublisher`, and `GripWebSocket`. 85 | 86 | While `Grip` will be available in all requests, the others will be available only when 87 | applicable based on configuration and the current request. 88 | 89 | The `Grip` facade provides the following functions: 90 | 91 | | Key | Description | 92 | | --- | --- | 93 | | `Grip::is_proxied` | A boolean value indicating whether the current request has been called via a GRIP proxy. | 94 | | `Grip::is_signed` | A boolean value indicating whether the current request is a signed request called via a GRIP proxy. | 95 | 96 | When the current request is proxied, then the `GripInstruct` facade is available and provides 97 | the same functions as `GripInstruct` in `fanout/grip`. 98 | 99 | When the current requiest is called over WebSocket-over-HTTP, then the `GripWebSocket` facade 100 | is available and provides the same functions as `WebSocketContext` in `fanout/grip`. 101 | 102 | To publish messages, use the `GripPublisher` facade. It provides the same functions as `Publisher` 103 | in `fanout/grip`. Use it to publish messages using the endpoints and prefix specified in the 104 | `./config/grip.php` file. 105 | 106 | ### Examples 107 | 108 | This repository contains examples to illustrate the use of `laravel-grip` found in the `examples` 109 | directory. For details on each example, please read the `README.md` files in the corresponding 110 | directories. 111 | 112 | 113 | ## Testing 114 | 115 | Run tests using the following command: 116 | 117 | ``` 118 | ./vendor/bin/phpunit 119 | ``` 120 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fanout/laravel-grip", 3 | "description": "GRIP for Laravel", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Katsuyuki Omuro", 9 | "email": "komuro@fastly.com" 10 | }, 11 | { 12 | "name": "Madeline Boby", 13 | "email": "madeline.boby@fastly.com" 14 | } 15 | ], 16 | "require": { 17 | "fanout/grip": "^1.1.0" 18 | }, 19 | "autoload": { 20 | "psr-4": { 21 | "Fanout\\LaravelGrip\\": "src" 22 | } 23 | }, 24 | "autoload-dev": { 25 | "psr-4": { 26 | "Fanout\\LaravelGrip\\Tests\\": "tests" 27 | } 28 | }, 29 | "extra": { 30 | "laravel": { 31 | "providers": [ 32 | "Fanout\\LaravelGrip\\LaravelGripServiceProvider" 33 | ] 34 | } 35 | }, 36 | "require-dev": { 37 | "orchestra/testbench": "^6.0", 38 | "phpunit/phpunit": "^9.5", 39 | "ramsey/uuid": "^4.2" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /config/config.php: -------------------------------------------------------------------------------- 1 | null, 5 | 'grip_proxy_required' => false, 6 | 'prefix' => '', 7 | ]; 8 | -------------------------------------------------------------------------------- /examples/http-stream/README.md: -------------------------------------------------------------------------------- 1 | ## HTTP Publish Example 2 | 3 | The following example shows how to set up laravel-grip for use in a Laravel 4 | project. 5 | 6 | The example assumes a new Laravel project, but adding to an existing project 7 | should possible in a similar fashion. 8 | 9 | Assume a brand new installation of Laravel in a directory called `~/sites/grip-demo`. 10 | This can be created using the `laravel` command described at 11 | `https://laravel.com/docs/8.x/installation`. 12 | 13 | ``` 14 | cd ~/sites 15 | laravel new grip-demo 16 | ``` 17 | 18 | ### Add laravel-grip 19 | 20 | ``` 21 | cd ~/sites/grip-demo 22 | composer require fanout/laravel-grip 23 | ``` 24 | 25 | Add `config/grip.php`: 26 | ```php 27 | return [ 28 | 'grip' => 'http://localhost:5561/' 29 | ]; 30 | ``` 31 | 32 | Add the following to `routes/api.php`: 33 | ```php 34 | use Fanout\LaravelGrip\Http\Middleware\Facades\GripInstruct; 35 | use Fanout\LaravelGrip\Http\Middleware\Facades\GripPublisher; 36 | 37 | /* ... */ 38 | 39 | Route::get('/http-stream/stream/{channel}', function( $channel ) { 40 | GripInstruct::add_channel( $channel ); 41 | GripInstruct::set_hold_stream(); 42 | return '[open stream]' . PHP_EOL; 43 | }); 44 | 45 | Route::get('/http-stream/publish/{channel}/{message}', function( $channel, $message ) { 46 | ob_start(); 47 | echo 'Channel: ' . $channel . PHP_EOL; 48 | echo 'Message: ' . $message . PHP_EOL; 49 | GripPublisher::publish_http_stream( $channel, $message . PHP_EOL ) 50 | ->then(function() { 51 | echo 'Publish Successful!' . PHP_EOL; 52 | }) 53 | ->otherwise(function( $error ) { 54 | echo 'Publish Fail!' . PHP_EOL; 55 | echo json_encode( $error ) . PHP_EOL; 56 | }) 57 | ->wait(); 58 | return ob_get_clean(); 59 | }); 60 | ``` 61 | 62 | ### Run Demo 63 | 64 | Set up Pushpin with a route: 65 | ``` 66 | * localhost:3000 67 | ``` 68 | 69 | In one Terminal window, run Pushpin 70 | ``` 71 | % pushpin 72 | ``` 73 | 74 | In another Terminal window, start Laravel: 75 | ``` 76 | % php artisan serve --port 3000 77 | ``` 78 | 79 | In another Terminal window, hit the *stream* endpoint with curl 80 | ``` 81 | % curl -i http://localhost:7999/api/http-stream/stream/test 82 | ``` 83 | 84 | You should see: 85 | ``` 86 | % curl -i http://localhost:7999/api/http-stream/stream/test 87 | HTTP/1.1 200 OK 88 | Host: localhost:7999 89 | Date: Sun, 29 Aug 2021 13:18:07 GMT 90 | X-Powered-By: PHP/8.0.10 91 | Content-Type: text/html; charset=UTF-8 92 | Cache-Control: no-cache, private 93 | Date: Sun, 29 Aug 2021 13:18:07 GMT 94 | X-RateLimit-Limit: 60 95 | X-RateLimit-Remaining: 59 96 | Access-Control-Allow-Origin: * 97 | Transfer-Encoding: chunked 98 | Connection: Transfer-Encoding 99 | 100 | [open stream] 101 | ``` 102 | 103 | Now, in yet another Terminal window, hit the *publish* endpoint with curl: 104 | ``` 105 | % curl -i http://localhost:7999/api/http-stream/publish/test/Hello 106 | ``` 107 | 108 | You should see: 109 | ``` 110 | HTTP/1.1 200 OK 111 | Host: localhost:7999 112 | Date: Sun, 29 Aug 2021 13:21:16 GMT 113 | X-Powered-By: PHP/8.0.10 114 | Content-Type: text/html; charset=UTF-8 115 | Cache-Control: no-cache, private 116 | Date: Sun, 29 Aug 2021 13:21:16 GMT 117 | X-RateLimit-Limit: 60 118 | X-RateLimit-Remaining: 59 119 | Access-Control-Allow-Origin: * 120 | Transfer-Encoding: chunked 121 | Connection: Transfer-Encoding 122 | 123 | Channel: test 124 | Message: Hello 125 | Publish Successful! 126 | ``` 127 | 128 | In the *stream* window, you should now see the following at the end of the output: 129 | ``` 130 | Hello 131 | ``` 132 | -------------------------------------------------------------------------------- /examples/ws-over-http/README.md: -------------------------------------------------------------------------------- 1 | ## WebSocket-over-HTTP Echo / Broadcast Example 2 | 3 | The following example shows how to set up laravel-grip for use in a Laravel 4 | project with the WebSocket-over-HTTP protocol. 5 | 6 | The example assumes a new Laravel project, but adding to an existing project 7 | should possible in a similar fashion. 8 | 9 | Assume a brand new installation of Laravel in a directory called `~/sites/grip-demo`. 10 | This can be created using the `laravel` command described at 11 | `https://laravel.com/docs/8.x/installation`. 12 | 13 | ``` 14 | cd ~/sites 15 | laravel new grip-demo 16 | ``` 17 | 18 | ### Add laravel-grip 19 | 20 | ``` 21 | cd ~/sites/grip-demo 22 | composer require fanout/laravel-grip 23 | ``` 24 | 25 | Add `config/grip.php`: 26 | ```php 27 | return [ 28 | 'grip' => 'http://localhost:5561/' 29 | ]; 30 | ``` 31 | 32 | Add the following to `routes/api.php`: 33 | ```php 34 | use Fanout\LaravelGrip\Http\Middleware\Facades\GripPublisher; 35 | use Fanout\LaravelGrip\Http\Middleware\Facades\GripWebSocket; 36 | 37 | /* ... */ 38 | 39 | Route::post('/ws/websocket/{channel}', function( Request $req, $channel ) { 40 | // Require WebSocket 41 | if( !GripWebSocket::is_valid() ) { 42 | return response( '[not a websocket request]' . PHP_EOL, 400 ); 43 | } 44 | 45 | if( GripWebSocket::is_opening() ) { 46 | // Open the WebSocket and subscribe to a channel: 47 | GripWebSocket::accept(); 48 | GripWebSocket::subscribe( $channel ); 49 | } 50 | 51 | while( GripWebSocket::can_recv() ) { 52 | $message = GripWebSocket::recv(); 53 | 54 | if ($message === null) { 55 | // If return value is undefined then connection is closed 56 | GripWebSocket::close(); 57 | break; 58 | } 59 | 60 | // Echo the message 61 | GripWebSocket::send( $message ); 62 | } 63 | 64 | return response( null, 204 ); 65 | }); 66 | 67 | Route::get('/ws/broadcast/{channel}/{message}', function( $channel, $message ) { 68 | ob_start(); 69 | echo 'Channel: ' . $channel . PHP_EOL; 70 | echo 'Message: ' . $message . PHP_EOL; 71 | GripPublisher::publish_websocket_message( $channel, $message . PHP_EOL ) 72 | ->then(function() { 73 | echo 'Publish Successful!' . PHP_EOL; 74 | }) 75 | ->otherwise(function($e) { 76 | echo 'Publish Fail!' . PHP_EOL; 77 | echo json_encode($e) . PHP_EOL; 78 | }) 79 | ->wait(); 80 | return ob_get_clean(); 81 | }); 82 | ``` 83 | 84 | ### Run Demo 85 | 86 | Set up Pushpin with a route: 87 | ``` 88 | * localhost:3000 89 | ``` 90 | 91 | In one Terminal window, run Pushpin 92 | ``` 93 | % pushpin 94 | ``` 95 | 96 | In another Terminal window, start Laravel: 97 | ``` 98 | % php artisan serve --port 3000 99 | ``` 100 | 101 | In another Terminal window, hit the *websocket* endpoint with wscat 102 | ``` 103 | % wscat --connect ws://localhost:7999/api/ws/websocket/test 104 | ``` 105 | 106 | You should see a prompt where you may enter a message. This application acts as an 107 | echo server, and any text you enter will be repeated back to you. 108 | 109 | Now, in yet another Terminal window, hit the *broadcast* endpoint with curl: 110 | ``` 111 | % curl -i http://localhost:7999/api/ws/broadcast/test/Hello 112 | ``` 113 | 114 | You should see: 115 | ``` 116 | HTTP/1.1 200 OK 117 | Host: localhost:7999 118 | Date: Sun, 29 Aug 2021 13:21:16 GMT 119 | X-Powered-By: PHP/8.0.10 120 | Content-Type: text/html; charset=UTF-8 121 | Cache-Control: no-cache, private 122 | Date: Sun, 29 Aug 2021 13:21:16 GMT 123 | X-RateLimit-Limit: 60 124 | X-RateLimit-Remaining: 59 125 | Access-Control-Allow-Origin: * 126 | Transfer-Encoding: chunked 127 | Connection: Transfer-Encoding 128 | 129 | Channel: test 130 | Message: Hello 131 | Publish Successful! 132 | ``` 133 | 134 | In the *websocket* window, you should now see the `Hello` message appear at the end, 135 | as an incoming message only. 136 | -------------------------------------------------------------------------------- /laravel-grip.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 17 | 18 | src/ 19 | 20 | 21 | 22 | 23 | ./tests/Unit 24 | 25 | 26 | ./tests/Feature 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/Errors/ConfigError.php: -------------------------------------------------------------------------------- 1 | prefix = $prefix; 15 | } 16 | 17 | public function publish( string $channel, Item $item ): PromiseInterface { 18 | return parent::publish( $this->prefix . $channel, $item ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/GripContext.php: -------------------------------------------------------------------------------- 1 | handled = false; 26 | $this->proxied = false; 27 | $this->signed = false; 28 | $this->grip_proxy_required = false; 29 | $this->grip_instruct = null; 30 | $this->ws_context = null; 31 | } 32 | 33 | public function is_handled(): bool { 34 | return $this->handled; 35 | } 36 | 37 | public function set_is_handled( $value = true ) { 38 | return $this->handled = $value; 39 | } 40 | 41 | public function is_proxied(): bool { 42 | return $this->proxied; 43 | } 44 | 45 | public function set_is_proxied( $value = true ) { 46 | return $this->proxied = $value; 47 | } 48 | 49 | public function is_signed(): bool { 50 | return $this->signed; 51 | } 52 | 53 | public function set_is_signed( $value = true ) { 54 | return $this->signed = $value; 55 | } 56 | 57 | public function is_grip_proxy_required(): bool { 58 | return $this->grip_proxy_required; 59 | } 60 | 61 | public function set_is_grip_proxy_required( $value = true ) { 62 | return $this->grip_proxy_required = $value; 63 | } 64 | 65 | public function has_instruct(): bool { 66 | return $this->grip_instruct !== null; 67 | } 68 | 69 | public function get_instruct(): GripInstruct { 70 | if ($this->grip_instruct !== null) { 71 | return $this->grip_instruct; 72 | } 73 | return $this->start_instruct(); 74 | } 75 | 76 | public function start_instruct(): GripInstruct { 77 | if (!$this->is_proxied()) { 78 | throw new GripInstructNotAvailableError(); 79 | } 80 | if ($this->grip_instruct !== null) { 81 | throw new GripInstructAlreadyStartedError(); 82 | } 83 | $this->grip_instruct = new GripInstruct(); 84 | return $this->grip_instruct; 85 | } 86 | 87 | public function get_ws_context(): ?WebSocketContext { 88 | return $this->ws_context; 89 | } 90 | 91 | public function set_ws_context( WebSocketContext $ws_context ) { 92 | $this->ws_context = $ws_context; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Http/Middleware/Facades/Grip.php: -------------------------------------------------------------------------------- 1 | meta = $new_meta; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Http/Middleware/Facades/GripPublisher.php: -------------------------------------------------------------------------------- 1 | clients; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Http/Middleware/Facades/GripWebSocket.php: -------------------------------------------------------------------------------- 1 | setup_grip( $request ); 43 | 44 | if( Grip::is_proxied() ) { 45 | Log::debug('Request is proxied'); 46 | } else { 47 | Log::debug('Request is not proxied'); 48 | } 49 | if( Grip::is_signed() ) { 50 | Log::debug('Request is signed'); 51 | } else { 52 | Log::debug('Request is not signed'); 53 | } 54 | 55 | if( Grip::is_grip_proxy_required() && !Grip::is_proxied() ) { 56 | // If we require a GRIP proxy but we detect there is 57 | // not one, we needs to fail now 58 | Log::error('ERROR - grip_proxy_required is true, but request not proxied.'); 59 | return response('Not Implemented.' . PHP_EOL, 501); 60 | } 61 | 62 | $ws_context = $this->setup_ws_context( $request ); 63 | if( $ws_context === 'connection-id-missing' ) { 64 | return response('WebSocket event missing connection-id header.' . PHP_EOL, 400); 65 | } 66 | if( $ws_context === 'websocket-decode-error' ) { 67 | return response( 'Error parsing WebSocket events.' . PHP_EOL, 400 ); 68 | } 69 | 70 | $response = $next($request); 71 | 72 | if( $ws_context !== null) { 73 | $this->apply_ws_context( $ws_context, $response ); 74 | } else { 75 | $this->apply_grip_instruct( $response ); 76 | } 77 | 78 | return $response; 79 | } 80 | 81 | function setup_grip(Request $request ) { 82 | 83 | try { 84 | Log::debug('ServeGrip#pre_middleware - start'); 85 | 86 | $grip_sig = $request->header( 'grip-sig' ); 87 | if( empty($grip_sig) ) { 88 | return; 89 | } 90 | Log::debug( 'grip_sig header exists.' ); 91 | 92 | $clients = GripPublisher::get_clients(); 93 | if( empty($clients) ) { 94 | Log::warning( 'no publisher clients configured.' ); 95 | return; 96 | } 97 | 98 | // If every client needs signing, then we mark as requires_signed; 99 | $requires_signed = true; 100 | foreach( $clients as $client ) { 101 | if( empty($client->get_verify_key()) ) { 102 | $requires_signed = false; 103 | break; 104 | } 105 | } 106 | 107 | // If all publishers have keys, then only consider this signed if 108 | // the grip sig has been signed by one of them 109 | $is_signed = false; 110 | if( $requires_signed ) { 111 | Log::debug( 'requires validating grip signature' ); 112 | foreach( $clients as $client ) { 113 | // At this point, all clients have a verify key 114 | Log::debug('validating: ' . $grip_sig . ' with ' . $client->get_verify_key() ); 115 | if( JwtAuth::validate_signature( $grip_sig, $client->get_verify_key(), $client->get_verify_iss() ) ) { 116 | Log::debug('validated' ); 117 | $is_signed = true; 118 | break; 119 | } 120 | Log::debug('not validated' ); 121 | } 122 | if (!$is_signed) { 123 | Log::debug( 'could not validate grip signature' ); 124 | // If we need to be signed but we got here without a signature, 125 | // we don't even consider this proxied. 126 | return; 127 | } 128 | } 129 | 130 | Grip::set_is_signed( $is_signed ); 131 | Grip::set_is_proxied(); 132 | 133 | } finally { 134 | Log::debug('ServeGrip#pre_middleware - end'); 135 | } 136 | } 137 | 138 | function setup_ws_context( Request $request ) { 139 | if( !WebSocketContext::is_ws_over_http() ) { 140 | Log::debug("is_ws_over_http false"); 141 | return null; 142 | } 143 | Log::debug("is_ws_over_http true"); 144 | 145 | try { 146 | WebSocketContext::set_input($request->getContent()); 147 | $ws_context = WebSocketContext::from_req(); 148 | } catch( ConnectionIdMissingError $ex ) { 149 | Log::error( 'ERROR - connection-id header needed' ); 150 | return 'connection-id-missing'; 151 | } catch( WebSocketDecodeEventError $ex ) { 152 | Log::error( 'ERROR - error parsing websocket events' ); 153 | return 'websocket-decode-error'; 154 | } 155 | 156 | Grip::set_ws_context( $ws_context ); 157 | return $ws_context; 158 | } 159 | 160 | function apply_grip_instruct( Response $response ) { 161 | if( !Grip::has_instruct() ) { 162 | return; 163 | } 164 | 165 | if( $response->getStatusCode() === 304 ) { 166 | // Code 304 only allows certain headers. 167 | // Some web servers strictly enforce this. 168 | // In that case we won't be able to use 169 | // Grip- headers to talk to the proxy. 170 | // Switch to code 200 and use Grip-Status 171 | // to specify intended status. 172 | Log::debug('Using gripInstruct setStatus header to handle 304'); 173 | $response->setStatusCode( 200 ); 174 | GripInstruct::set_status( 304 ); 175 | } 176 | 177 | // We can safely use Response#header() as the header values are always strings. 178 | foreach( GripInstruct::build_headers() as $header_name => $header_value ) { 179 | $response->header( $header_name, $header_value ); 180 | } 181 | } 182 | 183 | function apply_ws_context( WebSocketContext $ws_context, Response $response ) { 184 | if( $response->getStatusCode() === 200 || $response->getStatusCode() === 204 ) { 185 | // We can safely use Response#header() as the header values are always strings. 186 | foreach( $ws_context->to_headers() as $header_name => $header_value ) { 187 | $response->header( $header_name, $header_value ); 188 | } 189 | 190 | // Add outgoing events to response 191 | $out_events = $ws_context->get_outgoing_events(); 192 | $out_events_encoded = strval( WebSocketEvent::encode_events( $out_events ) ); 193 | if( !empty( $out_events_encoded ) ) { 194 | $response_content = $response->getContent(); 195 | $response_content .= $out_events_encoded; 196 | $response->setContent($response_content); 197 | $response->setStatusCode(200); 198 | } 199 | } 200 | } 201 | 202 | } 203 | -------------------------------------------------------------------------------- /src/LaravelGripServiceProvider.php: -------------------------------------------------------------------------------- 1 | mergeConfigFrom(__DIR__.'/../config/config.php', 'grip'); 17 | $this->app->scoped('fanout-gripcontext', function() { 18 | $grip_proxy_required = Config::get('grip.grip_proxy_required'); 19 | $serve_grip = new GripContext(); 20 | $serve_grip->set_is_grip_proxy_required( $grip_proxy_required ); 21 | return $serve_grip; 22 | }); 23 | $this->app->scoped( 'fanout-grippublisher', function() { 24 | $grip_config = Config::get('grip.grip'); 25 | $grip_prefix = Config::get('grip.prefix'); 26 | return $grip_config !== null ? new PrefixedPublisher( $grip_config, $grip_prefix ) : null; 27 | }); 28 | $this->app->scoped( 'fanout-gripinstruct', function() { 29 | return Grip::get_instruct(); 30 | }); 31 | $this->app->scoped( 'fanout-gripwebsocketcontext', function() { 32 | return Grip::get_ws_context(); 33 | }); 34 | } 35 | public function boot() { 36 | $kernel = $this->app->make(Kernel::class); 37 | $kernel->pushMiddleware(GripMiddleware::class); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/Feature/GripMiddlewareInstallTest.php: -------------------------------------------------------------------------------- 1 | setup_route(); 20 | 21 | $this->get( '/' ); 22 | $this->assertTrue( Grip::is_handled() ); 23 | 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | level, ECHO_LEVELS ) ) { 25 | return; 26 | } 27 | echo $logged->level . ": " . $logged->message . "\n"; 28 | }); 29 | } 30 | 31 | protected function getPackageProviders($app) { 32 | return [ 33 | LaravelGripServiceProvider::class, 34 | ]; 35 | } 36 | 37 | protected function getEnvironmentSetUp($app) { 38 | // perform environment setup 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/Unit/GripMiddlewareTest.php: -------------------------------------------------------------------------------- 1 | $clients, 43 | 'prefix' => '', 44 | 'grip_proxy_required' => $grip_proxy_required, 45 | ] ); 46 | } 47 | 48 | function create_request( $params = null ): Request { 49 | 50 | $grip_sig = $params['grip_sig'] ?? false; 51 | $is_websocket = $params['is_websocket'] ?? false; 52 | $has_connection_id = $params['has_connection_id'] ?? false; 53 | $body = $params['body'] ?? null; 54 | 55 | $server = []; 56 | if($grip_sig) { 57 | $exp = time() + 60 * 60; // 1 hour ago from now 58 | if ($grip_sig === 'expired') { 59 | $exp = time() - 60 * 60; // 1 hour ago 60 | } 61 | $sig = JWT::encode([ 62 | 'iss' => 'realm', 63 | 'exp' => $exp, 64 | ], self::SAMPLE_KEY ); 65 | $server['HTTP_GRIP_SIG'] = $sig; 66 | } 67 | if($is_websocket) { 68 | $_SERVER[ 'HTTP_CONTENT_TYPE' ] = 'application/websocket-events'; 69 | $_SERVER[ 'REQUEST_METHOD' ] = 'POST'; 70 | } else { 71 | unset( $_SERVER[ 'HTTP_CONTENT_TYPE' ] ); 72 | unset( $_SERVER[ 'REQUEST_METHOD' ] ); 73 | } 74 | if($has_connection_id) { 75 | $uuid = Uuid::uuid4(); 76 | $_SERVER[ 'HTTP_CONNECTION_ID' ] = $uuid->toString(); 77 | } else { 78 | unset( $_SERVER[ 'HTTP_CONNECTION_ID' ] ); 79 | } 80 | return new Request( [], [], [], [], [], $server, $body ); 81 | } 82 | 83 | /** @test */ 84 | function grip_middleware_throws_500_if_not_configured() { 85 | $req = $this->create_request(); 86 | 87 | $grip_middleware = new GripMiddleware(); 88 | 89 | $response = $grip_middleware->handle( $req, function() {}); 90 | 91 | $this->assertEquals( 500, $response->getStatusCode() ); 92 | $this->assertEquals( "No GRIP configuration provided.\n", $response->content() ); 93 | } 94 | 95 | /** @test */ 96 | function grip_middleware_is_installed() { 97 | 98 | $this->config_grip(); 99 | $req = $this->create_request(); 100 | 101 | $grip_middleware = new GripMiddleware(); 102 | $grip_middleware->handle( $req, function() { 103 | $this->assertTrue( Grip::is_handled() ); 104 | return response( null, 200 ); 105 | }); 106 | 107 | } 108 | 109 | /** @test */ 110 | function grip_middleware_detects_when_not_proxied() { 111 | 112 | $this->config_grip(); 113 | $req = $this->create_request(); 114 | 115 | $grip_middleware = new GripMiddleware(); 116 | $grip_middleware->handle( $req, function() { 117 | $this->assertFalse( Grip::is_proxied() ); 118 | return response( null, 200 ); 119 | }); 120 | 121 | } 122 | 123 | /** @test */ 124 | function grip_middleware_assumes_no_proxy_when_no_clients() { 125 | $this->config_grip(['clients' => []]); 126 | $req = $this->create_request(['grip_sig' => true]); 127 | 128 | $grip_middleware = new GripMiddleware(); 129 | $grip_middleware->handle( $req, function() { 130 | $this->assertFalse( Grip::is_proxied() ); 131 | return response( null, 200 ); 132 | }); 133 | } 134 | 135 | /** @test */ 136 | function grip_middleware_detects_proxy_requires_no_sig() { 137 | $this->config_grip(); 138 | $req = $this->create_request(['grip_sig' => true]); 139 | 140 | $grip_middleware = new GripMiddleware(); 141 | $grip_middleware->handle( $req, function() { 142 | $this->assertTrue( Grip::is_proxied() ); 143 | return response( null, 200 ); 144 | }); 145 | } 146 | 147 | /** @test */ 148 | function grip_middleware_detects_proxy_requires_and_has_sig() { 149 | $this->config_grip(['use_sample_key' => true]); 150 | $req = $this->create_request(['grip_sig' => true]); 151 | 152 | $grip_middleware = new GripMiddleware(); 153 | $grip_middleware->handle( $req, function() { 154 | $this->assertTrue( Grip::is_proxied() ); 155 | return response( null, 200 ); 156 | }); 157 | } 158 | 159 | /** @test */ 160 | function grip_middleware_detects_no_proxy_when_requires_and_has_expired_sig() { 161 | $this->config_grip(['use_sample_key' => true]); 162 | $req = $this->create_request(['grip_sig' => 'expired']); 163 | 164 | $grip_middleware = new GripMiddleware(); 165 | $grip_middleware->handle( $req, function() { 166 | $this->assertFalse( Grip::is_proxied() ); 167 | return response( null, 200 ); 168 | }); 169 | } 170 | 171 | /** @test */ 172 | function grip_middleware_detects_no_proxy_when_requires_and_has_invalid_sig() { 173 | $this->config_grip(['use_key' => 'foo']); 174 | $req = $this->create_request(['grip_sig' => true]); 175 | 176 | $grip_middleware = new GripMiddleware(); 177 | $grip_middleware->handle( $req, function() { 178 | $this->assertFalse( Grip::is_proxied() ); 179 | return response( null, 200 ); 180 | }); 181 | } 182 | 183 | /** @test */ 184 | function grip_middleware_detects_not_signed_when_requires_no_sig() { 185 | $this->config_grip(); 186 | $req = $this->create_request(['grip_sig' => true]); 187 | 188 | $grip_middleware = new GripMiddleware(); 189 | $grip_middleware->handle( $req, function() { 190 | $this->assertFalse( Grip::is_signed() ); 191 | return response( null, 200 ); 192 | }); 193 | } 194 | 195 | /** @test */ 196 | function grip_middleware_detects_signed_when_requires_and_has_sig() { 197 | $this->config_grip(['use_sample_key' => true]); 198 | $req = $this->create_request(['grip_sig' => true]); 199 | 200 | $grip_middleware = new GripMiddleware(); 201 | $grip_middleware->handle( $req, function() { 202 | $this->assertTrue( Grip::is_signed() ); 203 | return response( null, 200 ); 204 | }); 205 | } 206 | 207 | /** @test */ 208 | function grip_middleware_throws_501_when_requires_proxy_but_not_proxied() { 209 | $this->config_grip(['grip_proxy_required' => true]); 210 | $req = $this->create_request(); 211 | 212 | $grip_middleware = new GripMiddleware(); 213 | 214 | /** @var Response $response */ 215 | $response = $grip_middleware->handle( $req, function() {} ); 216 | 217 | $this->assertEquals( 501, $response->getStatusCode() ); 218 | $this->assertEquals( "Not Implemented.\n", $response->content() ); 219 | } 220 | 221 | /** @test */ 222 | function grip_middleware_allows_start_grip_instruct_when_proxied() { 223 | $this->config_grip(['use_sample_key' => true]); 224 | $req = $this->create_request(['grip_sig' => true]); 225 | 226 | $grip_middleware = new GripMiddleware(); 227 | $grip_middleware->handle( $req, function() { 228 | $grip_instruct = Grip::start_instruct(); 229 | $this->assertNotNull( $grip_instruct ); 230 | return response( null, 200 ); 231 | }); 232 | } 233 | 234 | /** @test */ 235 | function grip_middleware_does_not_allow_start_grip_instruct_when_not_proxied() { 236 | $this->config_grip(); 237 | $req = $this->create_request(); 238 | 239 | $grip_middleware = new GripMiddleware(); 240 | $grip_middleware->handle( $req, function() { 241 | $this->expectException( GripInstructNotAvailableError::class ); 242 | Grip::start_instruct(); 243 | }); 244 | } 245 | 246 | /** @test */ 247 | function grip_middleware_does_not_allow_start_grip_instruct_multiple_times() { 248 | $this->config_grip(['use_sample_key' => true]); 249 | $req = $this->create_request(['grip_sig' => true]); 250 | 251 | $grip_middleware = new GripMiddleware(); 252 | $grip_middleware->handle( $req, function() { 253 | $this->expectException( GripInstructAlreadyStartedError::class ); 254 | Grip::start_instruct(); 255 | Grip::start_instruct(); 256 | }); 257 | } 258 | 259 | /** @test */ 260 | function grip_middleware_allows_grip_instruct_facade_when_proxied() { 261 | $this->config_grip(['use_sample_key' => true]); 262 | $req = $this->create_request(['grip_sig' => true]); 263 | 264 | $grip_middleware = new GripMiddleware(); 265 | $grip_middleware->handle( $req, function() { 266 | $this->assertTrue( GripInstruct::is_valid() ); 267 | return response( null, 200 ); 268 | }); 269 | } 270 | 271 | /** @test */ 272 | function grip_middleware_does_not_allow_grip_instruct_facade_when_not_proxied() { 273 | $this->config_grip(); 274 | $req = $this->create_request(); 275 | 276 | $grip_middleware = new GripMiddleware(); 277 | $grip_middleware->handle( $req, function() { 278 | $this->assertFalse( GripInstruct::is_valid() ); 279 | return response( null, 200 ); 280 | }); 281 | } 282 | 283 | /** @test */ 284 | function grip_middleware_allows_grip_instruct_facade_multiple_times() { 285 | $this->config_grip(['use_sample_key' => true]); 286 | $req = $this->create_request(['grip_sig' => true]); 287 | 288 | $grip_middleware = new GripMiddleware(); 289 | $grip_middleware->handle( $req, function() { 290 | GripInstruct::is_valid(); 291 | $this->assertTrue( GripInstruct::is_valid() ); 292 | return response( null, 200 ); 293 | }); 294 | } 295 | 296 | /** @test */ 297 | function grip_middleware_does_not_prevent_grip_instruct_facade_after_start_instruct() { 298 | $this->config_grip(['use_sample_key' => true]); 299 | $req = $this->create_request(['grip_sig' => true]); 300 | 301 | $grip_middleware = new GripMiddleware(); 302 | $grip_middleware->handle( $req, function() { 303 | Grip::start_instruct(); 304 | $this->assertTrue( GripInstruct::is_valid() ); 305 | return response( null, 200 ); 306 | }); 307 | } 308 | 309 | /** @test */ 310 | function grip_middleware_prevents_start_instruct_after_grip_instruct_facade() { 311 | $this->config_grip(['use_sample_key' => true]); 312 | $req = $this->create_request(['grip_sig' => true]); 313 | 314 | $grip_middleware = new GripMiddleware(); 315 | $grip_middleware->handle( $req, function() { 316 | GripInstruct::is_valid(); 317 | 318 | $this->expectException( GripInstructAlreadyStartedError::class ); 319 | Grip::start_instruct(); 320 | return response( null, 200 ); 321 | }); 322 | } 323 | 324 | /** @test */ 325 | function grip_middleware_adds_headers_simple() { 326 | $this->config_grip(['use_sample_key' => true]); 327 | $req = $this->create_request(['grip_sig' => true]); 328 | 329 | $grip_middleware = new GripMiddleware(); 330 | $response = $grip_middleware->handle( $req, function() { 331 | GripInstruct::add_channel('foo'); 332 | return response( null, 200 ); 333 | }); 334 | $this->assertEquals( 'foo', $response->headers->get( 'Grip-Channel' ) ); 335 | } 336 | 337 | /** @test */ 338 | function grip_middleware_doesnt_add_headers_unless_instruct_used() { 339 | $this->config_grip(['use_sample_key' => true]); 340 | $req = $this->create_request(['grip_sig' => true]); 341 | 342 | $grip_middleware = new GripMiddleware(); 343 | $response = $grip_middleware->handle( $req, function() { 344 | return response( null, 200 ); 345 | }); 346 | $this->assertEmpty( $response->headers->get( 'Grip-Channel' ) ); 347 | } 348 | 349 | /** @test */ 350 | function grip_middleware_handle_304_status() { 351 | $this->config_grip(['use_sample_key' => true]); 352 | $req = $this->create_request(['grip_sig' => true]); 353 | 354 | $grip_middleware = new GripMiddleware(); 355 | $response = $grip_middleware->handle( $req, function() { 356 | GripInstruct::add_channel('foo'); 357 | return response( null, 304 ); 358 | }); 359 | $this->assertEquals( 200, $response->getStatusCode() ); 360 | $this->assertEquals( '304', $response->headers->get( 'Grip-Status' ) ); 361 | } 362 | 363 | /** @test */ 364 | function grip_middleware_throws_400_when_is_websocket_but_no_connection_id() { 365 | $this->config_grip(); 366 | $req = $this->create_request(['grip_sig' => true, 'is_websocket' => true]); 367 | 368 | $grip_middleware = new GripMiddleware(); 369 | $response = $grip_middleware->handle( $req, function() { 370 | return response(null, 200); 371 | }); 372 | $this->assertEquals( 400, $response->getStatusCode() ); 373 | $this->assertEquals( 'WebSocket event missing connection-id header.' . PHP_EOL, $response->content() ); 374 | } 375 | 376 | /** @test */ 377 | function grip_middleware_makes_ws_context_when_is_websocket_and_connection_id_present() { 378 | $this->config_grip(); 379 | $req = $this->create_request(['grip_sig' => true, 'is_websocket' => true, 'has_connection_id' => true]); 380 | 381 | $grip_middleware = new GripMiddleware(); 382 | $grip_middleware->handle( $req, function() { 383 | $this->assertTrue( GripWebSocket::is_valid() ); 384 | return response(null, 200); 385 | }); 386 | } 387 | 388 | /** @test */ 389 | function grip_middleware_makes_ws_context_and_decodes_itwhen_is_websocket_and_connection_id_present_with_valid_event() { 390 | $this->config_grip(); 391 | $req = $this->create_request(['grip_sig' => true, 'is_websocket' => true, 'has_connection_id' => 'true', 'body' => "TEXT 5\r\nHello\r\n"]); 392 | 393 | $grip_middleware = new GripMiddleware(); 394 | $grip_middleware->handle( $req, function() { 395 | $data = GripWebSocket::recv(); 396 | $this->assertEquals( 'Hello', $data ); 397 | return response(null, 200); 398 | }); 399 | } 400 | 401 | /** @test */ 402 | function grip_middleware_throws_400_when_is_websocket_and_connection_id_but_event_is_malformed() { 403 | $this->config_grip(); 404 | $req = $this->create_request(['grip_sig' => true, 'is_websocket' => true, 'has_connection_id' => 'true', 'body' => "TEXT 5\r\n"]); 405 | 406 | $grip_middleware = new GripMiddleware(); 407 | $response = $grip_middleware->handle( $req, function() { 408 | 409 | }); 410 | $this->assertEquals( 400, $response->getStatusCode() ); 411 | $this->assertEquals( 'Error parsing WebSocket events.' . PHP_EOL, $response->content() ); 412 | } 413 | 414 | /** @test */ 415 | function grip_middleware_makes_no_ws_context_when_not_websocket() { 416 | $this->config_grip(); 417 | $req = $this->create_request(['grip_sig' => true]); 418 | 419 | $grip_middleware = new GripMiddleware(); 420 | $grip_middleware->handle( $req, function() { 421 | $this->assertFalse( GripWebSocket::is_valid() ); 422 | return response( null, 200 ); 423 | }); 424 | } 425 | 426 | /** @test */ 427 | function grip_middleware_outputs_no_ws_headers_when_not_websocket() { 428 | $this->config_grip(); 429 | $req = $this->create_request(['grip_sig' => true]); 430 | 431 | $grip_middleware = new GripMiddleware(); 432 | $response = $grip_middleware->handle( $req, function() { 433 | return response( null, 200 ); 434 | }); 435 | $this->assertEquals( '', $response->headers->get( 'Content-Type' ) ); 436 | } 437 | 438 | /** @test */ 439 | function grip_middleware_outputs_ws_headers_when_is_websocket_and_connection_id_present() { 440 | $this->config_grip(); 441 | $req = $this->create_request(['grip_sig' => true, 'is_websocket' => true, 'has_connection_id' => true]); 442 | 443 | $grip_middleware = new GripMiddleware(); 444 | $response = $grip_middleware->handle( $req, function() { 445 | GripWebSocket::accept(); 446 | return response(null, 200); 447 | }); 448 | $this->assertEquals( 'application/websocket-events', $response->headers->get( 'Content-Type' ) ); 449 | $this->assertEquals( 'grip', $response->headers->get( 'Sec-WebSocket-Extensions' ) ); 450 | } 451 | 452 | /** @test */ 453 | function grip_middleware_does_not_output_ws_headers_when_is_websocket_and_connection_id_present_but_code_not_200() { 454 | $this->config_grip(); 455 | $req = $this->create_request(['grip_sig' => true, 'is_websocket' => true, 'has_connection_id' => true]); 456 | 457 | $grip_middleware = new GripMiddleware(); 458 | $response = $grip_middleware->handle( $req, function() { 459 | return response(null, 500); 460 | }); 461 | $this->assertEquals( '', $response->headers->get( 'Content-Type' ) ); 462 | } 463 | 464 | /** @test */ 465 | function grip_middleware_outputs_ws_events_when_is_websocket_and_connection_id_present_and_events_are_sent() { 466 | $this->config_grip(); 467 | $req = $this->create_request(['grip_sig' => true, 'is_websocket' => true, 'has_connection_id' => true]); 468 | 469 | $grip_middleware = new GripMiddleware(); 470 | $response = $grip_middleware->handle( $req, function() { 471 | GripWebSocket::send( 'foo' ); 472 | return response(null, 200); 473 | }); 474 | 475 | $this->assertEquals( "TEXT 5\r\nm:foo\r\n", $response->getContent() ); 476 | } 477 | 478 | /** @test */ 479 | function grip_middleware_outputs_changes_204_to_200_when_is_websocket_and_connection_id_present_and_events_are_sent() { 480 | $this->config_grip(); 481 | $req = $this->create_request(['grip_sig' => true, 'is_websocket' => true, 'has_connection_id' => true]); 482 | 483 | $grip_middleware = new GripMiddleware(); 484 | $response = $grip_middleware->handle( $req, function() { 485 | GripWebSocket::send( 'foo' ); 486 | return response(null, 204); 487 | }); 488 | 489 | $this->assertEquals( 200, $response->getStatusCode() ); 490 | } 491 | 492 | /** @test */ 493 | function grip_middleware_outputs_no_ws_events_when_is_websocket_and_connection_id_present_and_events_are_not_sent() { 494 | $this->config_grip(); 495 | $req = $this->create_request(['grip_sig' => true, 'is_websocket' => true, 'has_connection_id' => true]); 496 | 497 | $grip_middleware = new GripMiddleware(); 498 | $response = $grip_middleware->handle( $req, function() { 499 | return response(null, 200); 500 | }); 501 | 502 | $this->assertEmpty( $response->getContent() ); 503 | } 504 | 505 | /** @test */ 506 | function grip_middleware_keeps_204_when_is_websocket_and_connection_id_present_and_events_are_not_sent_and_code_is_204() { 507 | $this->config_grip(); 508 | $req = $this->create_request(['grip_sig' => true, 'is_websocket' => true, 'has_connection_id' => true]); 509 | 510 | $grip_middleware = new GripMiddleware(); 511 | $response = $grip_middleware->handle( $req, function() { 512 | return response(null, 204); 513 | }); 514 | 515 | $this->assertEquals( 204, $response->getStatusCode() ); 516 | } 517 | } 518 | -------------------------------------------------------------------------------- /tests/Unit/PrefixedPublisherTest.php: -------------------------------------------------------------------------------- 1 | getMockBuilder( PublisherClient::class ) 19 | ->disableOriginalConstructor() 20 | ->getMock(); 21 | 22 | $mock_publisher_client->expects($this->once()) 23 | ->method( 'publish' ) 24 | ->willReturn(new FulfilledPromise(true)) 25 | ->with( 'prefixchan', $item ); 26 | 27 | $publisher = new PrefixedPublisher([], 'prefix'); 28 | $publisher->add_client($mock_publisher_client); 29 | 30 | $publisher->publish( 'chan', $item ) 31 | ->wait(); 32 | 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /tests/Unit/ResolvePublisherTest.php: -------------------------------------------------------------------------------- 1 | assertFalse( GripPublisher::is_valid() ); 14 | } 15 | 16 | /** @test */ 17 | function publisher_is_valid_with_config() { 18 | Config::set( 'grip.grip', 'https://api.fanout.io/realm/realm?iss=realm&key=base64:geag121321=' ); 19 | 20 | $this->assertTrue( GripPublisher::is_valid() ); 21 | } 22 | 23 | } 24 | --------------------------------------------------------------------------------