Skip to content

Commit 20a2332

Browse files
committed
Add Trusted Clients HTTP middleware
1 parent 78a0878 commit 20a2332

File tree

7 files changed

+171
-9
lines changed

7 files changed

+171
-9
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
- Rename Rank and RankSample to Score and ScoreSample
66
- Move Verbose interface to Server
77
- Abstracted Router and Command Bus instantiation
8+
- Implemented Trusted Clients HTTP middleware
89

910
- 0.0.2-beta
1011
- Changed name of Binary serializer to Igbinary

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ $ composer require rubix/server
2525
- [RPC Client](#rpc-client)
2626
- [Http Middleware](#http-middeware)
2727
- [Shared Token Authenticator](#shared-token-authenticator)
28+
- [Trusted Clients](#trusted-clients)
2829
- [Messages](#messages)
2930
- [Commands](#commands)
3031
- [Predict](#predict)
@@ -224,6 +225,25 @@ use Rubix\Server\Http\Middleware\SharedTokenAuthenticator;
224225
$middleware = new SharedTokenAuthenticator('secret');
225226
```
226227

228+
### Trusted Clients
229+
A whitelist of trust clients - all other clients will be dropped.
230+
231+
**Parameters:**
232+
233+
| # | Param | Default | Type | Description |
234+
|--|--|--|--|--|
235+
| 1 | ips | ['127.0.0.1'] | array | An array of trusted client ip addresses. |
236+
237+
**Example:**
238+
239+
```php
240+
use Rubix\Server\Http\Middleware\TrustedClients;
241+
242+
$middleware = new TrustedClients([
243+
'127.0.0.1', '192.168.4.1', '45.63.67.15',
244+
]);
245+
```
246+
227247
---
228248
### Messages
229249
Messages are containers for the data that flow across the network between clients and model servers. They provide an object oriented interface to making requests and receiving responses through client/server interaction. There are two types of messages to consider in Rubix Server - [Commands](#commands) and [Responses](#responses). Commands signal an action to be performed by the server and are instantiated by the user and sent by the client API. Responses are returned by the server and contain the data that was sent back as a result of a command.

examples/RPC/client.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@
1111
use Rubix\Server\Commands\QueryModel;
1212
use Rubix\Server\Commands\ServerStatus;
1313

14-
$client = new RPCClient('127.0.0.1', 8888);
14+
const SHARED_SECRET = '2e2c47bbda4e531c585d796c0c8a4ac9';
15+
16+
$client = new RPCClient('127.0.0.1', 8888, false, [
17+
'Authorization' => SHARED_SECRET,
18+
]);
1519

1620
$dataset = new Unlabeled([
1721
[228, 28, 138],

examples/RPC/server.php

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,31 @@
33
include __DIR__ . '../../../vendor/autoload.php';
44

55
use Rubix\Server\RPCServer;
6+
use Rubix\Server\Http\Middleware\TrustedClients;
7+
use Rubix\Server\Http\Middleware\SharedTokenAuthenticator;
68
use Rubix\ML\Datasets\Generators\Blob;
79
use Rubix\ML\Datasets\Generators\Agglomerate;
810
use Rubix\ML\Classifiers\KNearestNeighbors;
911
use Rubix\ML\Other\Loggers\Screen;
1012

13+
const SHARED_SECRET = '2e2c47bbda4e531c585d796c0c8a4ac9';
14+
1115
$generator = new Agglomerate([
12-
'red' => new Blob([255, 0, 0], 10.),
13-
'green' => new Blob([0, 128, 0], 10.),
14-
'blue' => new Blob([0, 0, 255], 10.),
16+
'red' => new Blob([255, 0, 0], 10.0),
17+
'green' => new Blob([0, 128, 0], 10.0),
18+
'blue' => new Blob([0, 0, 255], 10.0),
1519
]);
1620

21+
$dataset = $generator->generate(1000);
22+
1723
$estimator = new KNearestNeighbors(3);
1824

19-
$estimator->train($generator->generate(500));
25+
$estimator->train($dataset);
2026

21-
$server = new RPCServer('127.0.0.1', 8888);
27+
$server = new RPCServer('127.0.0.1', 8888, null, [
28+
new TrustedClients(['127.0.0.1']),
29+
new SharedTokenAuthenticator(SHARED_SECRET),
30+
]);
2231

2332
$server->setLogger(new Screen('server'));
2433

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
namespace Rubix\Server\Http\Middleware;
4+
5+
use React\Http\Message\Response as ReactResponse;
6+
use Psr\Http\Message\ServerRequestInterface as Request;
7+
use Psr\Http\Message\ResponseInterface as Response;
8+
use InvalidArgumentException;
9+
10+
/**
11+
* Trusted Clients
12+
*
13+
* A whitelist of trust clients - all other clients will be dropped.
14+
*
15+
* @category Machine Learning
16+
* @package Rubix/Server
17+
* @author Andrew DalPino
18+
*/
19+
class TrustedClients extends Middleware
20+
{
21+
protected const UNAUTHORIZED = 401;
22+
23+
protected const FLAGS = FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6;
24+
25+
/**
26+
* An array of trusted client ip addresses.
27+
*
28+
* @var string[]
29+
*/
30+
protected $ips;
31+
32+
/**
33+
* @param string[] $ips
34+
* @throws \InvalidArgumentException
35+
*/
36+
public function __construct(array $ips = ['127.0.0.1'])
37+
{
38+
if (empty($ips)) {
39+
throw new InvalidArgumentException('At least 1 trusted client is required.');
40+
}
41+
42+
foreach ($ips as $i => $ip) {
43+
if (filter_var($ip, FILTER_VALIDATE_IP, self::FLAGS) === false) {
44+
throw new InvalidArgumentException("Invalid IP address at position $i.");
45+
}
46+
}
47+
48+
$this->ips = $ips;
49+
}
50+
51+
/**
52+
* Run the middleware over the request.
53+
*
54+
* @param Request $request
55+
* @param callable $next
56+
* @return Response
57+
*/
58+
public function handle(Request $request, callable $next) : Response
59+
{
60+
$params = $request->getServerParams();
61+
62+
if (isset($params['REMOTE_ADDR'])) {
63+
$ip = (string) explode(':', $params['REMOTE_ADDR'])[0];
64+
65+
if (in_array($ip, $this->ips)) {
66+
return $next($request);
67+
}
68+
}
69+
70+
return new ReactResponse(self::UNAUTHORIZED);
71+
}
72+
}

src/RPCClient.php

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,6 @@ public function __construct(
141141
* Send a command to the server and return the results.
142142
*
143143
* @param \Rubix\Server\Commands\Command $command
144-
* @throws \InvalidArgumentException
145144
* @throws \RuntimeException
146145
* @return \Rubix\Server\Responses\Response
147146
*/
@@ -151,6 +150,8 @@ public function send(Command $command) : Response
151150

152151
$delay = $this->delay;
153152

153+
$lastException = null;
154+
154155
for ($tries = 1 + $this->retries; $tries > 0; --$tries) {
155156
try {
156157
$payload = $this->client->request('POST', '/', [
@@ -165,12 +166,16 @@ public function send(Command $command) : Response
165166
if ($delay < self::MAX_DELAY) {
166167
$delay *= 2;
167168
}
169+
170+
$lastException = $e;
168171
}
169172
}
170173

171174
if (empty($payload)) {
172-
throw new RuntimeException('There was a problem'
173-
. ' communicating with the server.');
175+
$message = $lastException ? $lastException->getMessage() : '';
176+
177+
throw new RuntimeException('There was a problem communicating'
178+
. " with the server. $message");
174179
}
175180

176181
$response = $this->serializer->unserialize($payload);
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
namespace Rubix\Server\Tests\Http\Middleware;
4+
5+
use Rubix\Server\Http\Middleware\Middleware;
6+
use Rubix\Server\Http\Middleware\TrustedClients;
7+
use PHPUnit\Framework\TestCase;
8+
use InvalidArgumentException;
9+
10+
/**
11+
* @group Middleware
12+
* @covers \Rubix\Server\Http\Middleware\TrustedClients
13+
*/
14+
class TrustedClientsTest extends TestCase
15+
{
16+
/**
17+
* @var \Rubix\Server\Http\Middleware\TrustedClients
18+
*/
19+
protected $middleware;
20+
21+
/**
22+
* @before
23+
*/
24+
protected function setUp() : void
25+
{
26+
$this->middleware = new TrustedClients([
27+
'127.0.0.1', '192.168.4.1', '45.63.67.15',
28+
]);
29+
}
30+
31+
/**
32+
* @test
33+
*/
34+
public function build() : void
35+
{
36+
$this->assertInstanceOf(TrustedClients::class, $this->middleware);
37+
$this->assertInstanceOf(Middleware::class, $this->middleware);
38+
}
39+
40+
/**
41+
* @test
42+
*/
43+
public function badIp() : void
44+
{
45+
$this->expectException(InvalidArgumentException::class);
46+
47+
$middleware = new TrustedClients([
48+
'127.0.0.1', 'bad', '45.63.67.15',
49+
]);
50+
}
51+
}

0 commit comments

Comments
 (0)