├── .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 |
5 |
6 |
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 |
122 |
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 |
--------------------------------------------------------------------------------