Skip to content

Commit

Permalink
Add Socks5SocketConnector from amphp/http-tunnel
Browse files Browse the repository at this point in the history
  • Loading branch information
kelunik committed Mar 19, 2024
1 parent 4223324 commit bf9cfb7
Showing 1 changed file with 179 additions and 0 deletions.
179 changes: 179 additions & 0 deletions src/Socks5SocketConnector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
<?php declare(strict_types=1);

namespace Amp\Socket;

use Amp\ByteStream\StreamException;
use Amp\Cancellation;
use Amp\ForbidCloning;
use Amp\ForbidSerialization;
use League\Uri\Uri;

/** @api */
final class Socks5SocketConnector implements SocketConnector
{
private const REPLIES = [
0 => 'succeeded',
1 => 'general SOCKS server failure',
2 => 'connection not allowed by ruleset',
3 => 'Network unreachable',
4 => 'Host unreachable',
5 => 'Connection refused',
6 => 'TTL expired',
7 => 'Command not supported',
8 => 'Address type not supported'
];

/**
* @throws StreamException
* @see https://datatracker.ietf.org/doc/html/rfc1928#section-3
*/
private static function writeHello(?string $username, ?string $password, Socket $socket): void
{
$methods = \chr(0);
if (isset($username) && isset($password)) {
$methods .= \chr(2);
}

$socket->write(\chr(5) . \chr(\strlen($methods)) . $methods);
}

/**
* @throws SocketException
* @throws StreamException
* @see https://datatracker.ietf.org/doc/html/rfc1928#section-4
*/
private static function writeConnectRequest(Uri $uri, Socket $socket): void
{
$host = $uri->getHost();
if ($host === null) {
throw new SocketException("Host is null!");
}

$payload = \pack('C3', 0x5, 0x1, 0x0);

$ip = \inet_pton($host);
if ($ip !== false) {
$payload .= \chr(\strlen($ip) === 4 ? 0x1 : 0x4) . $ip;
} else {
$payload .= \chr(0x3) . \chr(\strlen($host)) . $host;
}

$payload .= \pack('n', $uri->getPort());

$socket->write($payload);
}

use ForbidCloning;
use ForbidSerialization;

public static function tunnel(
Socket $socket,
string $target,
?string $username,
?string $password,
?Cancellation $cancellation
): Socket {

Check failure on line 75 in src/Socks5SocketConnector.php

View workflow job for this annotation

GitHub Actions / PHP 8.1

InvalidReturnType

src/Socks5SocketConnector.php:75:8: InvalidReturnType: No return statements were found for method Amp\Socket\Socks5SocketConnector::tunnel but return type 'Amp\Socket\Socket' was expected (see https://psalm.dev/011)

Check failure on line 75 in src/Socks5SocketConnector.php

View workflow job for this annotation

GitHub Actions / PHP 8.2

InvalidReturnType

src/Socks5SocketConnector.php:75:8: InvalidReturnType: No return statements were found for method Amp\Socket\Socks5SocketConnector::tunnel but return type 'Amp\Socket\Socket' was expected (see https://psalm.dev/011)

Check failure on line 75 in src/Socks5SocketConnector.php

View workflow job for this annotation

GitHub Actions / PHP 8.3

InvalidReturnType

src/Socks5SocketConnector.php:75:8: InvalidReturnType: No return statements were found for method Amp\Socket\Socks5SocketConnector::tunnel but return type 'Amp\Socket\Socket' was expected (see https://psalm.dev/011)
if (($username === null) !== ($password === null)) {
throw new \Error("Both or neither username and password must be provided!");
}

$uri = Uri::createFromString($target);

Check failure on line 80 in src/Socks5SocketConnector.php

View workflow job for this annotation

GitHub Actions / PHP 8.1

DeprecatedMethod

src/Socks5SocketConnector.php:80:16: DeprecatedMethod: The method League\Uri\Uri::createFromString has been marked as deprecated (see https://psalm.dev/001)

Check failure on line 80 in src/Socks5SocketConnector.php

View workflow job for this annotation

GitHub Actions / PHP 8.2

DeprecatedMethod

src/Socks5SocketConnector.php:80:16: DeprecatedMethod: The method League\Uri\Uri::createFromString has been marked as deprecated (see https://psalm.dev/001)

Check failure on line 80 in src/Socks5SocketConnector.php

View workflow job for this annotation

GitHub Actions / PHP 8.3

DeprecatedMethod

src/Socks5SocketConnector.php:80:16: DeprecatedMethod: The method League\Uri\Uri::createFromString has been marked as deprecated (see https://psalm.dev/001)

$read = function (int $length) use ($socket, $cancellation): string {
\assert($length > 0);

$buffer = '';

do {
$chunk = $socket->read($cancellation, $length - \strlen($buffer));

Check failure on line 88 in src/Socks5SocketConnector.php

View workflow job for this annotation

GitHub Actions / PHP 8.1

InvalidArgument

src/Socks5SocketConnector.php:88:55: InvalidArgument: Argument 2 of Amp\Socket\Socket::read expects int<1, max>|null, but int<min, max> provided (see https://psalm.dev/004)

Check failure on line 88 in src/Socks5SocketConnector.php

View workflow job for this annotation

GitHub Actions / PHP 8.2

InvalidArgument

src/Socks5SocketConnector.php:88:55: InvalidArgument: Argument 2 of Amp\Socket\Socket::read expects int<1, max>|null, but int<min, max> provided (see https://psalm.dev/004)

Check failure on line 88 in src/Socks5SocketConnector.php

View workflow job for this annotation

GitHub Actions / PHP 8.3

InvalidArgument

src/Socks5SocketConnector.php:88:55: InvalidArgument: Argument 2 of Amp\Socket\Socket::read expects int<1, max>|null, but int<min, max> provided (see https://psalm.dev/004)
if ($chunk === null) {
throw new SocketException("The socket was closed before the tunnel could be established");
}

$buffer .= $chunk;
} while (\strlen($buffer) !== $length);

return $buffer;
};

self::writeHello($username, $password, $socket);

$version = \ord($read(1));
if ($version !== 5) {
throw new SocketException("Wrong SOCKS5 version: $version");
}

$method = \ord($read(1));
if ($method === 2) {
if ($username === null || $password === null) {
throw new SocketException("Unexpected method: $method");
}

$socket->write(
\chr(1) .
\chr(\strlen($username)) .
$username .
\chr(\strlen($password)) .
$password
);

$version = \ord($read(1));
if ($version !== 1) {
throw new SocketException("Wrong authorized SOCKS version: $version");
}

$result = \ord($read(1));
if ($result !== 0) {
throw new SocketException("Wrong authorization status: $result");
}
} elseif ($method !== 0) {
throw new SocketException("Unexpected method: $method");
}

self::writeConnectRequest($uri, $socket);

$version = \ord($read(1));
if ($version !== 5) {
throw new SocketException("Wrong SOCKS5 version: $version");
}

$reply = \ord($read(1));
if ($reply !== 0) {
$reply = self::REPLIES[$reply] ?? $reply;
throw new SocketException("Wrong SOCKS5 reply: $reply");
}

$rsv = \ord($read(1));
if ($rsv !== 0) {
throw new SocketException("Wrong SOCKS5 RSV: $rsv");
}

$read(match (\ord($read(1))) {
0x1 => 6,
0x4 => 18,
0x3 => \ord($read(1)) + 2
});

return $socket;
}

public function __construct(
private readonly SocketAddress|string $proxyAddress,
private readonly ?string $username = null,
private readonly ?string $password = null,
private readonly ?SocketConnector $socketConnector = null
) {
if (($username === null) !== ($password === null)) {
throw new \Error("Both or neither username and password must be provided!");
}
}

public function connect(SocketAddress|string $uri, ?ConnectContext $context = null, ?Cancellation $cancellation = null): Socket
{
$connector = $this->socketConnector ?? socketConnector();

$socket = $connector->connect($this->proxyAddress, $context, $cancellation);

return self::tunnel($socket, (string) $uri, $this->username, $this->password, $cancellation);
}
}

0 comments on commit bf9cfb7

Please sign in to comment.