Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 39 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,10 @@ $server->listen($socket);
$loop->run();
```

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

#### Server connector

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

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

```PHP
$server->setAuth(function ($username, $password, $remote) {
// either return a boolean success value right away
// or use promises for delayed authentication
```php
$server = new Clue\React\Socks\Server($loop, null, array(
'tom' => 'password',
'admin' => 'root'
));
```

See also [example #12](examples).

If you want more control over authentication, you can pass an authenticator
function that should return a `bool` value like this synchronous example:

```php
$server = new Clue\React\Socks\Server($loop, null, function ($user, $pass, $remote) {
// $remote is a full URI à la socks5://user:pass@192.168.1.1:1234
// or socks5s://user:pass@192.168.1.1:1234 for SOCKS over TLS
// useful for logging or extracting parts, such as the remote IP
$ip = parse_url($remote, PHP_URL_HOST);

return ($username === 'root' && $ip === '127.0.0.1');
return ($user === 'root' && $pass === 'secret' && $ip === '127.0.0.1');
});
```

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

```PHP
$server->setAuthArray(array(
'tom' => 'password',
'admin' => 'root'
));
```

See also [example #12](examples).

If you do not want to use authentication anymore:

```PHP
$server->unsetAuth();
```php
$server = new Clue\React\Socks\Server($loop, null, function ($user, $pass) use ($db) {
// pseudo-code: query database for given authentication details
return $db->query(
'SELECT 1 FROM users WHERE name = ? AND password = ?',
array($username, $password)
)->then(function (QueryResult $result) {
// ensure we find exactly one match in the database
return count($result->resultRows) === 1;
});
});
```

#### Server proxy chaining
Expand Down
3 changes: 1 addition & 2 deletions examples/12-server-with-password.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@

// start a new SOCKS proxy server
// require authentication and hence make this a SOCKS5-only server
$server = new Server($loop);
$server->setAuthArray(array(
$server = new Server($loop, null, array(
'tom' => 'god',
'user' => 'p@ssw0rd'
));
Expand Down
84 changes: 42 additions & 42 deletions src/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
namespace Clue\React\Socks;

use React\Socket\ServerInterface;
use React\Promise;
use React\Promise\Deferred;
use React\Promise\PromiseInterface;
use React\Socket\ConnectorInterface;
use React\Socket\Connector;
Expand Down Expand Up @@ -39,14 +37,40 @@ final class Server

private $connector;

private $auth = null;
/**
* @var null|callable
*/
private $auth;

public function __construct(LoopInterface $loop, ConnectorInterface $connector = null)
/**
* @param LoopInterface $loop
* @param null|ConnectorInterface $connector
* @param null|array|callable $auth
*/
public function __construct(LoopInterface $loop, ConnectorInterface $connector = null, $auth = null)
{
if ($connector === null) {
$connector = new Connector($loop);
}

if (\is_array($auth)) {
// wrap authentication array in authentication callback
$this->auth = function ($username, $password) use ($auth) {
return \React\Promise\resolve(
isset($auth[$username]) && (string)$auth[$username] === $password
);
};
} elseif (\is_callable($auth)) {
// wrap authentication callback in order to cast its return value to a promise
$this->auth = function($username, $password, $remote) use ($auth) {
return \React\Promise\resolve(
\call_user_func($auth, $username, $password, $remote)
);
};
} elseif ($auth !== null) {
throw new \InvalidArgumentException('Invalid authenticator given');
}

$this->loop = $loop;
$this->connector = $connector;
}
Expand All @@ -63,36 +87,6 @@ public function listen(ServerInterface $socket)
});
}

public function setAuth($auth)
{
if (!is_callable($auth)) {
throw new InvalidArgumentException('Given authenticator is not a valid callable');
}

// wrap authentication callback in order to cast its return value to a promise
$this->auth = function($username, $password, $remote) use ($auth) {
$ret = call_user_func($auth, $username, $password, $remote);
if ($ret instanceof PromiseInterface) {
return $ret;
}
$deferred = new Deferred();
$ret ? $deferred->resolve() : $deferred->reject();
return $deferred->promise();
};
}

public function setAuthArray(array $login)
{
$this->setAuth(function ($username, $password) use ($login) {
return (isset($login[$username]) && (string)$login[$username] === $password);
});
}

public function unsetAuth()
{
$this->auth = null;
}

/** @internal */
public function onConnection(ConnectionInterface $connection)
{
Expand Down Expand Up @@ -215,7 +209,7 @@ public function handleSocks4(ConnectionInterface $stream, StreamReader $reader)
}

/** @internal */
public function handleSocks5(ConnectionInterface $stream, $auth=null, StreamReader $reader)
public function handleSocks5(ConnectionInterface $stream, $auth, StreamReader $reader)
{
$remote = $stream->getRemoteAddress();
if ($remote !== null) {
Expand Down Expand Up @@ -255,13 +249,19 @@ public function handleSocks5(ConnectionInterface $stream, $auth=null, StreamRead
$remote = str_replace('://', '://' . rawurlencode($username) . ':' . rawurlencode($password) . '@', $remote);
}

return $auth($username, $password, $remote)->then(function () use ($stream) {
// accept
$stream->write(pack('C2', 0x01, 0x00));
}, function() use ($stream) {
// reject => send any code but 0x00
return $auth($username, $password, $remote)->then(function ($authenticated) use ($stream) {
if ($authenticated) {
// accept auth
$stream->write(pack('C2', 0x01, 0x00));
} else {
// reject auth => send any code but 0x00
$stream->end(pack('C2', 0x01, 0xFF));
throw new UnexpectedValueException('Authentication denied');
}
}, function ($e) use ($stream) {
// reject failed authentication => send any code but 0x00
$stream->end(pack('C2', 0x01, 0xFF));
throw new UnexpectedValueException('Unable to authenticate');
throw new UnexpectedValueException('Authentication error', 0, $e);
});
});
});
Expand Down Expand Up @@ -336,7 +336,7 @@ public function connectTarget(ConnectionInterface $stream, array $target)
// validate URI so a string hostname can not pass excessive URI parts
$parts = parse_url('tcp://' . $uri);
if (!$parts || !isset($parts['scheme'], $parts['host'], $parts['port']) || count($parts) !== 3) {
return Promise\reject(new InvalidArgumentException('Invalid target URI given'));
return \React\Promise\reject(new InvalidArgumentException('Invalid target URI given'));
}

if (isset($target[2])) {
Expand Down
Loading