Skip to content

Commit c602a0f

Browse files
authored
Merge pull request #84 from clue-labs/server-auth
Replace Server::setAuth() with optional `Server` constructor parameter
2 parents ace3394 + 6231abc commit c602a0f

5 files changed

Lines changed: 193 additions & 98 deletions

File tree

README.md

Lines changed: 39 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,10 @@ $server->listen($socket);
628628
$loop->run();
629629
```
630630

631+
Additionally, the `Server` constructor accepts optional parameters to explicitly
632+
configure the [connector](#server-connector) to use and to require
633+
[authentication](#server-authentication). For more details, read on...
634+
631635
#### Server connector
632636

633637
The `Server` uses an instance of ReactPHP's
@@ -674,42 +678,52 @@ If a client tries to use any other protocol version, does not send along
674678
authentication details or if authentication details can not be verified,
675679
the connection will be rejected.
676680

677-
Because your authentication mechanism might take some time to actually check
678-
the provided authentication credentials (like querying a remote database or webservice),
679-
the server side uses a [Promise](https://github.com/reactphp/promise) based interface.
680-
While this might seem complex at first, it actually provides a very simple way
681-
to handle simultanous connections in a non-blocking fashion and increases overall performance.
681+
If you only want to accept static authentication details, you can simply pass an
682+
additional assoc array with your authentication details to the `Server` like this:
682683

683-
```PHP
684-
$server->setAuth(function ($username, $password, $remote) {
685-
// either return a boolean success value right away
686-
// or use promises for delayed authentication
684+
```php
685+
$server = new Clue\React\Socks\Server($loop, null, array(
686+
'tom' => 'password',
687+
'admin' => 'root'
688+
));
689+
```
690+
691+
See also [example #12](examples).
687692

693+
If you want more control over authentication, you can pass an authenticator
694+
function that should return a `bool` value like this synchronous example:
695+
696+
```php
697+
$server = new Clue\React\Socks\Server($loop, null, function ($user, $pass, $remote) {
688698
// $remote is a full URI à la socks5://user:pass@192.168.1.1:1234
689699
// or socks5s://user:pass@192.168.1.1:1234 for SOCKS over TLS
690700
// useful for logging or extracting parts, such as the remote IP
691701
$ip = parse_url($remote, PHP_URL_HOST);
692702

693-
return ($username === 'root' && $ip === '127.0.0.1');
703+
return ($user === 'root' && $pass === 'secret' && $ip === '127.0.0.1');
694704
});
695705
```
696706

697-
Or if you only accept static authentication details, you can use the simple
698-
array-based authentication method as a shortcut:
707+
Because your authentication mechanism might take some time to actually check the
708+
provided authentication credentials (like querying a remote database or webservice),
709+
the server also supports a [Promise](https://github.com/reactphp/promise)-based
710+
interface. While this might seem more complex at first, it actually provides a
711+
very powerful way of handling a large number of connections concurrently without
712+
ever blocking any connections. You can return a [Promise](https://github.com/reactphp/promise)
713+
from the authenticator function that will fulfill with a `bool` value like this
714+
async example:
699715

700-
```PHP
701-
$server->setAuthArray(array(
702-
'tom' => 'password',
703-
'admin' => 'root'
704-
));
705-
```
706-
707-
See also [example #12](examples).
708-
709-
If you do not want to use authentication anymore:
710-
711-
```PHP
712-
$server->unsetAuth();
716+
```php
717+
$server = new Clue\React\Socks\Server($loop, null, function ($user, $pass) use ($db) {
718+
// pseudo-code: query database for given authentication details
719+
return $db->query(
720+
'SELECT 1 FROM users WHERE name = ? AND password = ?',
721+
array($username, $password)
722+
)->then(function (QueryResult $result) {
723+
// ensure we find exactly one match in the database
724+
return count($result->resultRows) === 1;
725+
});
726+
});
713727
```
714728

715729
#### Server proxy chaining

examples/12-server-with-password.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@
99

1010
// start a new SOCKS proxy server
1111
// require authentication and hence make this a SOCKS5-only server
12-
$server = new Server($loop);
13-
$server->setAuthArray(array(
12+
$server = new Server($loop, null, array(
1413
'tom' => 'god',
1514
'user' => 'p@ssw0rd'
1615
));

src/Server.php

Lines changed: 42 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
namespace Clue\React\Socks;
44

55
use React\Socket\ServerInterface;
6-
use React\Promise;
7-
use React\Promise\Deferred;
86
use React\Promise\PromiseInterface;
97
use React\Socket\ConnectorInterface;
108
use React\Socket\Connector;
@@ -39,14 +37,40 @@ final class Server
3937

4038
private $connector;
4139

42-
private $auth = null;
40+
/**
41+
* @var null|callable
42+
*/
43+
private $auth;
4344

44-
public function __construct(LoopInterface $loop, ConnectorInterface $connector = null)
45+
/**
46+
* @param LoopInterface $loop
47+
* @param null|ConnectorInterface $connector
48+
* @param null|array|callable $auth
49+
*/
50+
public function __construct(LoopInterface $loop, ConnectorInterface $connector = null, $auth = null)
4551
{
4652
if ($connector === null) {
4753
$connector = new Connector($loop);
4854
}
4955

56+
if (\is_array($auth)) {
57+
// wrap authentication array in authentication callback
58+
$this->auth = function ($username, $password) use ($auth) {
59+
return \React\Promise\resolve(
60+
isset($auth[$username]) && (string)$auth[$username] === $password
61+
);
62+
};
63+
} elseif (\is_callable($auth)) {
64+
// wrap authentication callback in order to cast its return value to a promise
65+
$this->auth = function($username, $password, $remote) use ($auth) {
66+
return \React\Promise\resolve(
67+
\call_user_func($auth, $username, $password, $remote)
68+
);
69+
};
70+
} elseif ($auth !== null) {
71+
throw new \InvalidArgumentException('Invalid authenticator given');
72+
}
73+
5074
$this->loop = $loop;
5175
$this->connector = $connector;
5276
}
@@ -63,36 +87,6 @@ public function listen(ServerInterface $socket)
6387
});
6488
}
6589

66-
public function setAuth($auth)
67-
{
68-
if (!is_callable($auth)) {
69-
throw new InvalidArgumentException('Given authenticator is not a valid callable');
70-
}
71-
72-
// wrap authentication callback in order to cast its return value to a promise
73-
$this->auth = function($username, $password, $remote) use ($auth) {
74-
$ret = call_user_func($auth, $username, $password, $remote);
75-
if ($ret instanceof PromiseInterface) {
76-
return $ret;
77-
}
78-
$deferred = new Deferred();
79-
$ret ? $deferred->resolve() : $deferred->reject();
80-
return $deferred->promise();
81-
};
82-
}
83-
84-
public function setAuthArray(array $login)
85-
{
86-
$this->setAuth(function ($username, $password) use ($login) {
87-
return (isset($login[$username]) && (string)$login[$username] === $password);
88-
});
89-
}
90-
91-
public function unsetAuth()
92-
{
93-
$this->auth = null;
94-
}
95-
9690
/** @internal */
9791
public function onConnection(ConnectionInterface $connection)
9892
{
@@ -215,7 +209,7 @@ public function handleSocks4(ConnectionInterface $stream, StreamReader $reader)
215209
}
216210

217211
/** @internal */
218-
public function handleSocks5(ConnectionInterface $stream, $auth=null, StreamReader $reader)
212+
public function handleSocks5(ConnectionInterface $stream, $auth, StreamReader $reader)
219213
{
220214
$remote = $stream->getRemoteAddress();
221215
if ($remote !== null) {
@@ -255,13 +249,19 @@ public function handleSocks5(ConnectionInterface $stream, $auth=null, StreamRead
255249
$remote = str_replace('://', '://' . rawurlencode($username) . ':' . rawurlencode($password) . '@', $remote);
256250
}
257251

258-
return $auth($username, $password, $remote)->then(function () use ($stream) {
259-
// accept
260-
$stream->write(pack('C2', 0x01, 0x00));
261-
}, function() use ($stream) {
262-
// reject => send any code but 0x00
252+
return $auth($username, $password, $remote)->then(function ($authenticated) use ($stream) {
253+
if ($authenticated) {
254+
// accept auth
255+
$stream->write(pack('C2', 0x01, 0x00));
256+
} else {
257+
// reject auth => send any code but 0x00
258+
$stream->end(pack('C2', 0x01, 0xFF));
259+
throw new UnexpectedValueException('Authentication denied');
260+
}
261+
}, function ($e) use ($stream) {
262+
// reject failed authentication => send any code but 0x00
263263
$stream->end(pack('C2', 0x01, 0xFF));
264-
throw new UnexpectedValueException('Unable to authenticate');
264+
throw new UnexpectedValueException('Authentication error', 0, $e);
265265
});
266266
});
267267
});
@@ -336,7 +336,7 @@ public function connectTarget(ConnectionInterface $stream, array $target)
336336
// validate URI so a string hostname can not pass excessive URI parts
337337
$parts = parse_url('tcp://' . $uri);
338338
if (!$parts || !isset($parts['scheme'], $parts['host'], $parts['port']) || count($parts) !== 3) {
339-
return Promise\reject(new InvalidArgumentException('Invalid target URI given'));
339+
return \React\Promise\reject(new InvalidArgumentException('Invalid target URI given'));
340340
}
341341

342342
if (isset($target[2])) {

0 commit comments

Comments
 (0)