public function handleSocks5(Stream $stream, $auth = null) { $reader = new StreamReader($stream); $that = $this; return $reader->readByte()->then(function ($num) use($reader) { // $num different authentication mechanisms offered return $reader->readLength($num); })->then(function ($methods) use($reader, $stream, $auth) { if ($auth === null && strpos($methods, "") !== false) { // accept "no authentication" $stream->write(pack('C2', 0x5, 0x0)); return 0x0; } else { if ($auth !== null && strpos($methods, "") !== false) { // username/password authentication (RFC 1929) sub negotiation $stream->write(pack('C2', 0x5, 0x2)); return $reader->readByteAssert(0x1)->then(function () use($reader) { return $reader->readByte(); })->then(function ($length) use($reader) { return $reader->readLength($length); })->then(function ($username) use($reader, $auth, $stream) { return $reader->readByte()->then(function ($length) use($reader) { return $reader->readLength($length); })->then(function ($password) use($username, $auth, $stream) { // username and password known => authenticate // echo 'auth: ' . $username.' : ' . $password . PHP_EOL; return $auth($username, $password)->then(function () use($stream, $username) { // accept $stream->emit('auth', array($username)); $stream->write(pack('C2', 0x1, 0x0)); }, function () use($stream) { // reject => send any code but 0x00 $stream->end(pack('C2', 0x1, 0xff)); throw new UnexpectedValueException('Unable to authenticate'); }); }); }); } else { // reject all offered authentication methods $stream->end(pack('C2', 0x5, 0xff)); throw new UnexpectedValueException('No acceptable authentication mechanism found'); } } })->then(function ($method) use($reader, $stream) { return $reader->readBinary(array('version' => 'C', 'command' => 'C', 'null' => 'C', 'type' => 'C')); })->then(function ($data) use($reader) { if ($data['version'] !== 0x5) { throw new UnexpectedValueException('Invalid SOCKS version'); } if ($data['command'] !== 0x1) { throw new UnexpectedValueException('Only CONNECT requests supported'); } // if ($data['null'] !== 0x00) { // throw new UnexpectedValueException('Reserved byte has to be NULL'); // } if ($data['type'] === 0x3) { // target hostname string return $reader->readByte()->then(function ($len) use($reader) { return $reader->readLength($len); }); } else { if ($data['type'] === 0x1) { // target IPv4 return $reader->readLength(4)->then(function ($addr) { return inet_ntop($addr); }); } else { if ($data['type'] === 0x4) { // target IPv6 return $reader->readLength(16)->then(function ($addr) { return inet_ntop($addr); }); } else { throw new UnexpectedValueException('Invalid target type'); } } } })->then(function ($host) use($reader) { return $reader->readBinary(array('port' => 'n'))->then(function ($data) use($host) { return array($host, $data['port']); }); })->then(function ($target) use($that, $stream) { return $that->connectTarget($stream, $target); }, function ($error) use($stream) { throw new UnexpectedValueException('SOCKS5 protocol error', 0, $error); })->then(function (Stream $remote) use($stream) { $stream->write(pack('C4Nn', 0x5, 0x0, 0x0, 0x1, 0, 0)); return $remote; }, function (Exception $error) use($stream) { $code = 0x1; $stream->end(pack('C4Nn', 0x5, $code, 0x0, 0x1, 0, 0)); throw $error; }); }
protected function handleSocks5(Stream $stream, $host, $port, $auth = null) { // protocol version 5 $data = pack('C', 0x5); if ($auth === null) { // one method, no authentication $data .= pack('C2', 0x1, 0x0); } else { // two methods, username/password and no authentication $data .= pack('C3', 0x2, 0x2, 0x0); } $stream->write($data); $that = $this; $reader = new StreamReader($stream); return $reader->readBinary(array('version' => 'C', 'method' => 'C'))->then(function ($data) use($auth, $stream, $reader) { if ($data['version'] !== 0x5) { throw new Exception('Version/Protocol mismatch'); } if ($data['method'] === 0x2 && $auth !== null) { // username/password authentication requested and provided $stream->write($auth); return $reader->readBinary(array('version' => 'C', 'status' => 'C'))->then(function ($data) { if ($data['version'] !== 0x1 || $data['status'] !== 0x0) { throw new Exception('Username/Password authentication failed'); } }); } else { if ($data['method'] !== 0x0) { // any other method than "no authentication" throw new Exception('Unacceptable authentication method requested'); } } })->then(function () use($stream, $reader, $host, $port) { // do not resolve hostname. only try to convert to (binary/packed) IP $ip = @inet_pton($host); $data = pack('C3', 0x5, 0x1, 0x0); if ($ip === false) { // not an IP, send as hostname $data .= pack('C2', 0x3, strlen($host)) . $host; } else { // send as IPv4 / IPv6 $data .= pack('C', strpos($host, ':') === false ? 0x1 : 0x4) . $ip; } $data .= pack('n', $port); $stream->write($data); return $reader->readBinary(array('version' => 'C', 'status' => 'C', 'null' => 'C', 'type' => 'C')); })->then(function ($data) use($reader) { if ($data['version'] !== 0x5 || $data['status'] !== 0x0 || $data['null'] !== 0x0) { throw new Exception('Invalid SOCKS response'); } if ($data['type'] === 0x1) { // IPv4 address => skip IP and port return $reader->readLength(6); } else { if ($data['type'] === 0x3) { // domain name => read domain name length return $reader->readBinary(array('length' => 'C'))->then(function ($data) use($that) { // skip domain name and port return $that->readLength($data['length'] + 2); }); } else { if ($data['type'] === 0x4) { // IPv6 address => skip IP and port return $reader->readLength(18); } else { throw new Exception('Invalid SOCKS reponse: Invalid address type'); } } } }); }