Skip to content

Improve error reporting by always including target URI in exceptions #83

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 16, 2018
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
113 changes: 80 additions & 33 deletions src/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,9 @@ public function connect($uri)
// start TCP/IP connection to SOCKS server
$connecting = $this->connector->connect($socksUri);

$deferred = new Deferred(function ($_, $reject) use ($connecting) {
$deferred = new Deferred(function ($_, $reject) use ($uri, $connecting) {
$reject(new RuntimeException(
'Connection cancelled while waiting for proxy (ECONNABORTED)',
'Connection to ' . $uri . ' cancelled while waiting for proxy (ECONNABORTED)',
defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103
));

Expand All @@ -177,12 +177,12 @@ public function connect($uri)
// resolve plain connection once SOCKS protocol is completed
$that = $this;
$connecting->then(
function (ConnectionInterface $stream) use ($that, $host, $port, $deferred) {
$that->handleConnectedSocks($stream, $host, $port, $deferred);
function (ConnectionInterface $stream) use ($that, $host, $port, $deferred, $uri) {
$that->handleConnectedSocks($stream, $host, $port, $deferred, $uri);
},
function (Exception $e) use ($deferred) {
function (Exception $e) use ($uri, $deferred) {
$deferred->reject($e = new RuntimeException(
'Connection failed because connection to proxy failed (ECONNREFUSED)',
'Connection to ' . $uri . ' failed because connection to proxy failed (ECONNREFUSED)',
defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111,
$e
));
Expand Down Expand Up @@ -213,26 +213,33 @@ function (Exception $e) use ($deferred) {
* @param string $host
* @param int $port
* @param Deferred $deferred
* @param string $uri
* @return void
* @internal
*/
public function handleConnectedSocks(ConnectionInterface $stream, $host, $port, Deferred $deferred)
public function handleConnectedSocks(ConnectionInterface $stream, $host, $port, Deferred $deferred, $uri)
{
$reader = new StreamReader();
$stream->on('data', array($reader, 'write'));

$stream->on('error', $onError = function (Exception $e) use ($deferred) {
$deferred->reject(new RuntimeException('Stream error while waiting for response from proxy (EIO)', defined('SOCKET_EIO') ? SOCKET_EIO : 5, $e));
$stream->on('error', $onError = function (Exception $e) use ($deferred, $uri) {
$deferred->reject(new RuntimeException(
'Connection to ' . $uri . ' failed because connection to proxy caused a stream error (EIO)',
defined('SOCKET_EIO') ? SOCKET_EIO : 5, $e)
);
});

$stream->on('close', $onClose = function () use ($deferred) {
$deferred->reject(new RuntimeException('Connection to proxy lost while waiting for response (ECONNRESET)', defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 104));
$stream->on('close', $onClose = function () use ($deferred, $uri) {
$deferred->reject(new RuntimeException(
'Connection to ' . $uri . ' failed because connection to proxy was lost while waiting for response from proxy (ECONNRESET)',
defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 104)
);
});

if ($this->protocolVersion === 5) {
$promise = $this->handleSocks5($stream, $host, $port, $reader);
$promise = $this->handleSocks5($stream, $host, $port, $reader, $uri);
} else {
$promise = $this->handleSocks4($stream, $host, $port, $reader);
$promise = $this->handleSocks4($stream, $host, $port, $reader, $uri);
}

$promise->then(function () use ($deferred, $stream, $reader, $onError, $onClose) {
Expand All @@ -241,18 +248,22 @@ public function handleConnectedSocks(ConnectionInterface $stream, $host, $port,
$stream->removeListener('close', $onClose);

$deferred->resolve($stream);
}, function (Exception $error) use ($deferred, $stream) {
}, function (Exception $error) use ($deferred, $stream, $uri) {
// pass custom RuntimeException through as-is, otherwise wrap in protocol error
if (!$error instanceof RuntimeException) {
$error = new RuntimeException('Invalid response received from proxy (EBADMSG)', defined('SOCKET_EBADMSG') ? SOCKET_EBADMSG: 71, $error);
$error = new RuntimeException(
'Connection to ' . $uri . ' failed because proxy returned invalid response (EBADMSG)',
defined('SOCKET_EBADMSG') ? SOCKET_EBADMSG: 71,
$error
);
}

$deferred->reject($error);
$stream->close();
});
}

private function handleSocks4(ConnectionInterface $stream, $host, $port, StreamReader $reader)
private function handleSocks4(ConnectionInterface $stream, $host, $port, StreamReader $reader, $uri)
{
// do not resolve hostname. only try to convert to IP
$ip = ip2long($host);
Expand All @@ -272,17 +283,20 @@ private function handleSocks4(ConnectionInterface $stream, $host, $port, StreamR
'status' => 'C',
'port' => 'n',
'ip' => 'N'
))->then(function ($data) {
))->then(function ($data) use ($uri) {
if ($data['null'] !== 0x00) {
throw new Exception('Invalid SOCKS response');
}
if ($data['status'] !== 0x5a) {
throw new RuntimeException('Proxy refused connection with SOCKS error code ' . sprintf('0x%02X', $data['status']) . ' (ECONNREFUSED)', defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111);
throw new RuntimeException(
'Connection to ' . $uri . ' failed because proxy refused connection with error code ' . sprintf('0x%02X', $data['status']) . ' (ECONNREFUSED)',
defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111
);
}
});
}

private function handleSocks5(ConnectionInterface $stream, $host, $port, StreamReader $reader)
private function handleSocks5(ConnectionInterface $stream, $host, $port, StreamReader $reader, $uri)
{
// protocol version 5
$data = pack('C', 0x05);
Expand All @@ -302,7 +316,7 @@ private function handleSocks5(ConnectionInterface $stream, $host, $port, StreamR
return $reader->readBinary(array(
'version' => 'C',
'method' => 'C'
))->then(function ($data) use ($auth, $stream, $reader) {
))->then(function ($data) use ($auth, $stream, $reader, $uri) {
if ($data['version'] !== 0x05) {
throw new Exception('Version/Protocol mismatch');
}
Expand All @@ -314,14 +328,20 @@ private function handleSocks5(ConnectionInterface $stream, $host, $port, StreamR
return $reader->readBinary(array(
'version' => 'C',
'status' => 'C'
))->then(function ($data) {
))->then(function ($data) use ($uri) {
if ($data['version'] !== 0x01 || $data['status'] !== 0x00) {
throw new RuntimeException('Username/Password authentication failed (EACCES)', defined('SOCKET_EACCES') ? SOCKET_EACCES : 13);
throw new RuntimeException(
'Connection to ' . $uri . ' failed because proxy denied access with given authentication details (EACCES)',
defined('SOCKET_EACCES') ? SOCKET_EACCES : 13
);
}
});
} else if ($data['method'] !== 0x00) {
// any other method than "no authentication"
throw new RuntimeException('No acceptable authentication method found (EACCES)', defined('SOCKET_EACCES') ? SOCKET_EACCES : 13);
throw new RuntimeException(
'Connection to ' . $uri . ' failed because proxy denied access due to unsupported authentication method (EACCES)',
defined('SOCKET_EACCES') ? SOCKET_EACCES : 13
);
}
})->then(function () use ($stream, $reader, $host, $port) {
// do not resolve hostname. only try to convert to (binary/packed) IP
Expand All @@ -345,32 +365,59 @@ private function handleSocks5(ConnectionInterface $stream, $host, $port, StreamR
'null' => 'C',
'type' => 'C'
));
})->then(function ($data) use ($reader) {
})->then(function ($data) use ($reader, $uri) {
if ($data['version'] !== 0x05 || $data['null'] !== 0x00) {
throw new Exception('Invalid SOCKS response');
}
if ($data['status'] !== 0x00) {
// map limited list of SOCKS error codes to common socket error conditions
// @link https://tools.ietf.org/html/rfc1928#section-6
if ($data['status'] === Server::ERROR_GENERAL) {
throw new RuntimeException('SOCKS server reported a general server failure (ECONNREFUSED)', defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111);
throw new RuntimeException(
'Connection to ' . $uri . ' failed because proxy refused connection with general server failure (ECONNREFUSED)',
defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111
);
} elseif ($data['status'] === Server::ERROR_NOT_ALLOWED_BY_RULESET) {
throw new RuntimeException('SOCKS server reported connection is not allowed by ruleset (EACCES)', defined('SOCKET_EACCES') ? SOCKET_EACCES : 13);
throw new RuntimeException(
'Connection to ' . $uri . ' failed because proxy denied access due to ruleset (EACCES)',
defined('SOCKET_EACCES') ? SOCKET_EACCES : 13
);
} elseif ($data['status'] === Server::ERROR_NETWORK_UNREACHABLE) {
throw new RuntimeException('SOCKS server reported network unreachable (ENETUNREACH)', defined('SOCKET_ENETUNREACH') ? SOCKET_ENETUNREACH : 101);
throw new RuntimeException(
'Connection to ' . $uri . ' failed because proxy reported network unreachable (ENETUNREACH)',
defined('SOCKET_ENETUNREACH') ? SOCKET_ENETUNREACH : 101
);
} elseif ($data['status'] === Server::ERROR_HOST_UNREACHABLE) {
throw new RuntimeException('SOCKS server reported host unreachable (EHOSTUNREACH)', defined('SOCKET_EHOSTUNREACH') ? SOCKET_EHOSTUNREACH : 113);
throw new RuntimeException(
'Connection to ' . $uri . ' failed because proxy reported host unreachable (EHOSTUNREACH)',
defined('SOCKET_EHOSTUNREACH') ? SOCKET_EHOSTUNREACH : 113
);
} elseif ($data['status'] === Server::ERROR_CONNECTION_REFUSED) {
throw new RuntimeException('SOCKS server reported connection refused (ECONNREFUSED)', defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111);
throw new RuntimeException(
'Connection to ' . $uri . ' failed because proxy reported connection refused (ECONNREFUSED)',
defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111
);
} elseif ($data['status'] === Server::ERROR_TTL) {
throw new RuntimeException('SOCKS server reported TTL/timeout expired (ETIMEDOUT)', defined('SOCKET_ETIMEDOUT') ? SOCKET_ETIMEDOUT : 110);
throw new RuntimeException(
'Connection to ' . $uri . ' failed because proxy reported TTL/timeout expired (ETIMEDOUT)',
defined('SOCKET_ETIMEDOUT') ? SOCKET_ETIMEDOUT : 110
);
} elseif ($data['status'] === Server::ERROR_COMMAND_UNSUPPORTED) {
throw new RuntimeException('SOCKS server does not support the CONNECT command (EPROTO)', defined('SOCKET_EPROTO') ? SOCKET_EPROTO : 71);
throw new RuntimeException(
'Connection to ' . $uri . ' failed because proxy does not support the CONNECT command (EPROTO)',
defined('SOCKET_EPROTO') ? SOCKET_EPROTO : 71
);
} elseif ($data['status'] === Server::ERROR_ADDRESS_UNSUPPORTED) {
throw new RuntimeException('SOCKS server does not support this address type (EPROTO)', defined('SOCKET_EPROTO') ? SOCKET_EPROTO : 71);
throw new RuntimeException(
'Connection to ' . $uri . ' failed because proxy does not support this address type (EPROTO)',
defined('SOCKET_EPROTO') ? SOCKET_EPROTO : 71
);
}

throw new RuntimeException('SOCKS server reported an unassigned error code ' . sprintf('0x%02X', $data['status']) . ' (ECONNREFUSED)', defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111);
throw new RuntimeException(
'Connection to ' . $uri . ' failed because proxy server refused connection with unknown error code ' . sprintf('0x%02X', $data['status']) . ' (ECONNREFUSED)',
defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111
);
}
if ($data['type'] === 0x01) {
// IPv4 address => skip IP and port
Expand Down
Loading