diff --git a/src/danog/MadelineProto/API.php b/src/danog/MadelineProto/API.php index 31533ffb..baa335e8 100644 --- a/src/danog/MadelineProto/API.php +++ b/src/danog/MadelineProto/API.php @@ -96,7 +96,7 @@ class API extends InternalDoc if ($e->getFile() === 'MadelineProto' && $e->getLine() === 1) { throw $e; } - if (\defined('MADELINEPROTO_TEST') && MADELINEPROTO_TEST === 'pony') { + if (\MADELINEPROTO_TEST === 'pony') { throw $e; } \class_exists('\\Volatile'); diff --git a/src/danog/MadelineProto/ContextConnector.php b/src/danog/MadelineProto/ContextConnector.php index 0536a040..bd9f39a3 100644 --- a/src/danog/MadelineProto/ContextConnector.php +++ b/src/danog/MadelineProto/ContextConnector.php @@ -1,7 +1,7 @@ dataCenter = $dataCenter; - $this->ctx = $ctx; + $this->fromDns = false; } - public function connect(string $uri, ?ConnectContext $socketContext = null, ?CancellationToken $token = null): Promise + public function connect(string $uri, ?ConnectContext $ctx = null, ?CancellationToken $token = null): Promise { - return Tools::call(function () use ($uri, $socketContext, $token) { - $socketContext = $socketContext ?? new ConnectContext; + return Tools::call(function () use ($uri, $ctx, $token) { + $ctx = $ctx ?? new ConnectContext; $token = $token ?? new NullCancellationToken; - $attempt = 0; - $uris = []; - $failures = []; - [$scheme, $host, $port] = parseUri($uri); - if ($host[0] === '[') { - $host = \substr($host, 1, -1); + $ctxs = $this->datacenter->generateContexts(0, $uri, $ctx); + if (empty($ctxs)) { + throw new Exception("No contexts for raw connection to URI $uri"); } - if ($port === 0 || @\inet_pton($host)) { - // Host is already an IP address or file path. - $uris = [$uri]; - } else { - // Host is not an IP address, so resolve the domain name. - // When we're connecting to a host, we may need to resolve the domain name, first. - // The resolution is usually done using DNS over HTTPS. - // - // The DNS over HTTPS resolver needs to resolve the domain name of the DOH server: - // this is handled internally by the DNS over HTTPS client, - // by redirecting the resolution request to the plain DNS client. - // - // However, if the DoH connection is proxied with a proxy that has a domain name itself, - // we cannot resolve it with the DoH resolver, since this will cause an infinite loop - // - // resolve host.com => (DoH resolver) => resolve dohserver.com => (simple resolver) => OK - // - // |> resolve dohserver.com => (simple resolver) => OK - // resolve host.com => (DoH resolver) =| - // |> resolve proxy.com => (non-proxied resolver) => OK - // - // - // This means that we must detect if the domain name we're trying to resolve is a proxy domain name. - // - // Here, we simply check if the connection URI has changed since we first set it: - // this would indicate that a proxy class has changed the connection URI to the proxy URI. - // - if ($this->ctx->isDns()) { - $records = yield $this->dataCenter->getNonProxiedDNSClient()->resolve($host, $socketContext->getDnsTypeRestriction()); - } else { - $records = yield $this->dataCenter->getDNSClient()->resolve($host, $socketContext->getDnsTypeRestriction()); - } - \usort($records, function (Record $a, Record $b) { - return $a->getType() - $b->getType(); - }); - if ($this->ctx->getIpv6()) { - $records = \array_reverse($records); - } - - foreach ($records as $record) { - /** @var Record $record */ - if ($record->getType() === Record::AAAA) { - $uris[] = \sprintf("%s://[%s]:%d", $scheme, $record->getValue(), $port); - } else { - $uris[] = \sprintf("%s://%s:%d", $scheme, $record->getValue(), $port); - } - } - } - - $flags = \STREAM_CLIENT_CONNECT | \STREAM_CLIENT_ASYNC_CONNECT; - $timeout = $socketContext->getConnectTimeout(); - foreach ($uris as $builtUri) { + foreach ($ctxs as $ctx) { + /* @var $ctx \danog\MadelineProto\Stream\ConnectionContext */ try { - $streamContext = \stream_context_create($socketContext->withoutTlsContext()->toStreamContextArray()); - if (!$socket = @\stream_socket_client($builtUri, $errno, $errstr, null, $flags, $streamContext)) { - throw new ConnectException(\sprintf( - 'Connection to %s failed: [Error #%d] %s%s', - $uri, - $errno, - $errstr, - $failures ? '; previous attempts: ' . \implode($failures) : '' - ), $errno); + $ctx->setIsDns($this->fromDns); + $ctx->setCancellationToken($token); + $result = yield $ctx->getStream(); + $this->API->logger->logger('OK!', \danog\MadelineProto\Logger::WARNING); + + return $result->getSocket(); + } catch (\Throwable $e) { + if (\MADELINEPROTO_TEST === 'pony') { + throw $e; } - \stream_set_blocking($socket, false); - $deferred = new Deferred; - $watcher = Loop::onWritable($socket, [$deferred, 'resolve']); - $id = $token->subscribe([$deferred, 'fail']); - try { - yield Promise\timeout($deferred->promise(), $timeout); - } catch (TimeoutException $e) { - throw new ConnectException(\sprintf( - 'Connecting to %s failed: timeout exceeded (%d ms)%s', - $uri, - $timeout, - $failures ? '; previous attempts: ' . \implode($failures) : '' - ), 110); // See ETIMEDOUT in http://www.virtsync.com/c-error-codes-include-errno - } finally { - Loop::cancel($watcher); - $token->unsubscribe($id); + $this->API->logger->logger('Connection failed: '.$e, \danog\MadelineProto\Logger::ERROR); + if ($e instanceof MultiReasonException) { + foreach ($e->getReasons() as $reason) { + $this->API->logger->logger('Multireason: '.$reason, \danog\MadelineProto\Logger::ERROR); + } } - // The following hack looks like the only way to detect connection refused errors with PHP's stream sockets. - if (\stream_socket_get_name($socket, true) === false) { - \fclose($socket); - throw new ConnectException(\sprintf( - 'Connection to %s refused%s', - $uri, - $failures ? '; previous attempts: ' . \implode($failures) : '' - ), 111); // See ECONNREFUSED in http://www.virtsync.com/c-error-codes-include-errno - } - } catch (ConnectException $e) { - // Includes only error codes used in this file, as error codes on other OS families might be different. - // In fact, this might show a confusing error message on OS families that return 110 or 111 by itself. - $knownReasons = [ - 110 => 'connection timeout', - 111 => 'connection refused', - ]; - $code = $e->getCode(); - $reason = $knownReasons[$code] ?? ('Error #' . $code); - if (++$attempt === $socketContext->getMaxAttempts()) { - break; - } - $failures[] = "{$uri} ({$reason})"; - continue; // Could not connect to host, try next host in the list. } - return ResourceSocket::fromClientSocket($socket, $socketContext->getTlsContext()); } - // This is reached if either all URIs failed or the maximum number of attempts is reached. - /** @noinspection PhpUndefinedVariableInspection */ - throw $e; + + throw new \danog\MadelineProto\Exception("Could not connect to URI $uri"); }); } diff --git a/src/danog/MadelineProto/DataCenter.php b/src/danog/MadelineProto/DataCenter.php index 2e151e53..f966d85d 100644 --- a/src/danog/MadelineProto/DataCenter.php +++ b/src/danog/MadelineProto/DataCenter.php @@ -19,9 +19,6 @@ namespace danog\MadelineProto; -use Amp\CancellationToken; -use Amp\Deferred; -use Amp\Dns\Record; use Amp\Dns\Resolver; use Amp\Dns\Rfc1035StubResolver; use Amp\DoH\DoHConfig; @@ -34,15 +31,8 @@ use Amp\Http\Client\Cookie\CookieJar; use Amp\Http\Client\Cookie\InMemoryCookieJar; use Amp\Http\Client\DelegateHttpClient; use Amp\Http\Client\HttpClientBuilder; -use Amp\Loop; -use Amp\MultiReasonException; -use Amp\NullCancellationToken; -use Amp\Promise; -use Amp\Socket\ClientSocket; -use Amp\Socket\ClientTlsContext; use Amp\Socket\ConnectContext; -use Amp\Socket\ConnectException; -use Amp\TimeoutException; +use Amp\Websocket\Client\Rfc6455Connector; use danog\MadelineProto\MTProto\PermAuthKey; use danog\MadelineProto\MTProto\TempAuthKey; use danog\MadelineProto\Stream\Common\BufferedRawStream; @@ -60,8 +50,6 @@ use danog\MadelineProto\Stream\StreamInterface; use danog\MadelineProto\Stream\Transport\DefaultStream; use danog\MadelineProto\Stream\Transport\WssStream; use danog\MadelineProto\Stream\Transport\WsStream; -use function Amp\call; -use function Amp\Socket\Internal\parseUri; /** * Manages datacenters. @@ -117,7 +105,7 @@ class DataCenter * * @var \Amp\DoH\Rfc8484StubResolver */ - private $NonProxiedDoHClient; + private $nonProxiedDoHClient; /** * Cookie jar. * @@ -231,12 +219,12 @@ class DataCenter $this->CookieJar = $jar ?? new InMemoryCookieJar; $this->HTTPClient = (new HttpClientBuilder) ->interceptNetwork(new CookieInterceptor($this->CookieJar)) - ->usingPool(new UnlimitedConnectionPool(new DefaultConnectionFactory(new ProxyConnector($this)))) + ->usingPool(new UnlimitedConnectionPool(new DefaultConnectionFactory(new ContextConnector($this)))) ->build(); $DoHHTTPClient = (new HttpClientBuilder) ->interceptNetwork(new CookieInterceptor($this->CookieJar)) - ->usingPool(new UnlimitedConnectionPool(new DefaultConnectionFactory(new ProxyConnector($this, true)))) + ->usingPool(new UnlimitedConnectionPool(new DefaultConnectionFactory(new ContextConnector($this, true)))) ->build(); $DoHConfig = new DoHConfig( @@ -246,19 +234,19 @@ class DataCenter ], $DoHHTTPClient ); - $NonProxiedDoHConfig = new DoHConfig( + $nonProxiedDoHConfig = new DoHConfig( [ new Nameserver('https://mozilla.cloudflare-dns.com/dns-query'), new Nameserver('https://dns.google/resolve'), ] ); - $this->DoHClient = Magic::$altervista || Magic::$zerowebhost ? - new Rfc1035StubResolver() : + $this->DoHClient = Magic::$altervista || Magic::$zerowebhost ? + new Rfc1035StubResolver() : new Rfc8484StubResolver($DoHConfig); - $this->NonProxiedDoHClient = Magic::$altervista || Magic::$zerowebhost ? - new Rfc1035StubResolver() : - new Rfc8484StubResolver($NonProxiedDoHConfig); + $this->nonProxiedDoHClient = Magic::$altervista || Magic::$zerowebhost ? + new Rfc1035StubResolver() : + new Rfc8484StubResolver($nonProxiedDoHConfig); } } @@ -297,12 +285,10 @@ class DataCenter return true; } catch (\Throwable $e) { - if (\defined(\MADELINEPROTO_TEST::class) && MADELINEPROTO_TEST === 'pony') { + if (\MADELINEPROTO_TEST === 'pony') { throw $e; } $this->API->logger->logger('Connection failed: '.$e->getMessage(), \danog\MadelineProto\Logger::ERROR); - } catch (\Exception $e) { - $this->API->logger->logger('Connection failed: '.$e->getMessage(), \danog\MadelineProto\Logger::ERROR); } } @@ -454,7 +440,7 @@ class DataCenter foreach ($ipv6 as $ipv6) { // This is only for non-MTProto connections if (!$dc_number) { - /** @var $ctx \danog\MadelineProto\Stream\ConnectionContext */ + /* @var $ctx \danog\MadelineProto\Stream\ConnectionContext */ $ctx = (new ConnectionContext()) ->setSocketContext($context) ->setUri($uri) @@ -462,15 +448,7 @@ class DataCenter foreach ($combo as $stream) { if ($stream[0] === DefaultStream::getName() && $stream[1] === []) { - $stream[1] = [ - function ( - string $uri, - ClientConnectContext $socketContext = null, - CancellationToken $token = null - ) use ($ctx): Promise { - return $this->socketConnect($ctx, $uri, $socketContext, $token); - } - ]; + $stream[1] = new DoHConnector($this, $ctx); } $ctx->addStream(...$stream); } @@ -522,7 +500,7 @@ class DataCenter $uri = 'tcp://'.$address.':'.$port.'/'.$path; } - /** @var $ctx \danog\MadelineProto\Stream\ConnectionContext */ + /* @var $ctx \danog\MadelineProto\Stream\ConnectionContext */ $ctx = (new ConnectionContext()) ->setDc($dc_number) ->setTest($this->settings[$dc_config_number]['test_mode']) @@ -532,7 +510,20 @@ class DataCenter foreach ($combo as $stream) { if ($stream[0] === DefaultStream::getName() && $stream[1] === []) { - $stream[1] = new ContextConnector($this, $ctx); + $stream[1] = new DoHConnector($this, $ctx); + } + if (\in_array($stream[0], [WsStream::class, WssStream::class]) && $stream[1] === []) { + $stream[1] = new Rfc6455Connector( + (new HttpClientBuilder) + ->usingPool( + new UnlimitedConnectionPool( + new DefaultConnectionFactory( + new DoHConnector($this, $ctx) + ) + ) + ) + ->build() + ); } $ctx->addStream(...$stream); } @@ -549,7 +540,7 @@ class DataCenter unset($this->sockets[$dc_number]); $this->API->logger->logger("No info for DC $dc_number", \danog\MadelineProto\Logger::ERROR); - } elseif (\defined('MADELINEPROTO_TEST') && MADELINEPROTO_TEST === 'pony') { + } elseif (\MADELINEPROTO_TEST === 'pony') { return [$ctxs[0]]; } diff --git a/src/danog/MadelineProto/DoHConnector.php b/src/danog/MadelineProto/DoHConnector.php new file mode 100644 index 00000000..0454eb5c --- /dev/null +++ b/src/danog/MadelineProto/DoHConnector.php @@ -0,0 +1,182 @@ +. + * + * @author Daniil Gentili + * @copyright 2016-2019 Daniil Gentili + * @license https://opensource.org/licenses/AGPL-3.0 AGPLv3 + * + * @link https://docs.madelineproto.xyz MadelineProto documentation + */ + +namespace danog\MadelineProto; + +use Amp\CancellationToken; +use Amp\Deferred; +use Amp\Dns\Record; +use Amp\Dns\TimeoutException; +use Amp\Loop; +use Amp\NullCancellationToken; +use Amp\Promise; +use Amp\Socket\ConnectContext; +use Amp\Socket\ConnectException; +use Amp\Socket\Connector; +use Amp\Socket\ResourceSocket; +use danog\MadelineProto\Stream\ConnectionContext; + +use function Amp\Socket\Internal\parseUri; + +class DoHConnector implements Connector +{ + /** + * Datacenter instance + * + * @property DataCenter $dataCenter + */ + private $dataCenter; + /** + * Connection context + * + * @var ConnectionContext + */ + private $ctx; + public function __construct(DataCenter $dataCenter, ConnectionContext $ctx) + { + $this->dataCenter = $dataCenter; + $this->ctx = $ctx; + } + + public function connect(string $uri, ?ConnectContext $socketContext = null, ?CancellationToken $token = null): Promise + { + return Tools::call(function () use ($uri, $socketContext, $token) { + $socketContext = $socketContext ?? new ConnectContext; + $token = $token ?? new NullCancellationToken; + + $attempt = 0; + $uris = []; + $failures = []; + [$scheme, $host, $port] = parseUri($uri); + if ($host[0] === '[') { + $host = \substr($host, 1, -1); + } + if ($port === 0 || @\inet_pton($host)) { + // Host is already an IP address or file path. + $uris = [$uri]; + } else { + // Host is not an IP address, so resolve the domain name. + // When we're connecting to a host, we may need to resolve the domain name, first. + // The resolution is usually done using DNS over HTTPS. + // + // The DNS over HTTPS resolver needs to resolve the domain name of the DOH server: + // this is handled internally by the DNS over HTTPS client, + // by redirecting the resolution request to the plain DNS client. + // + // However, if the DoH connection is proxied with a proxy that has a domain name itself, + // we cannot resolve it with the DoH resolver, since this will cause an infinite loop + // + // resolve host.com => (DoH resolver) => resolve dohserver.com => (simple resolver) => OK + // + // |> resolve dohserver.com => (simple resolver) => OK + // resolve host.com => (DoH resolver) =| + // |> resolve proxy.com => (non-proxied resolver) => OK + // + // + // This means that we must detect if the domain name we're trying to resolve is a proxy domain name. + // + // Here, we simply check if the connection URI has changed since we first set it: + // this would indicate that a proxy class has changed the connection URI to the proxy URI. + // + if ($this->ctx->isDns()) { + $records = yield $this->dataCenter->getNonProxiedDNSClient()->resolve($host, $socketContext->getDnsTypeRestriction()); + } else { + $records = yield $this->dataCenter->getDNSClient()->resolve($host, $socketContext->getDnsTypeRestriction()); + } + \usort($records, function (Record $a, Record $b) { + return $a->getType() - $b->getType(); + }); + if ($this->ctx->getIpv6()) { + $records = \array_reverse($records); + } + + foreach ($records as $record) { + /** @var Record $record */ + if ($record->getType() === Record::AAAA) { + $uris[] = \sprintf("%s://[%s]:%d", $scheme, $record->getValue(), $port); + } else { + $uris[] = \sprintf("%s://%s:%d", $scheme, $record->getValue(), $port); + } + } + } + + $flags = \STREAM_CLIENT_CONNECT | \STREAM_CLIENT_ASYNC_CONNECT; + $timeout = $socketContext->getConnectTimeout(); + foreach ($uris as $builtUri) { + try { + $streamContext = \stream_context_create($socketContext->withoutTlsContext()->toStreamContextArray()); + if (!$socket = @\stream_socket_client($builtUri, $errno, $errstr, null, $flags, $streamContext)) { + throw new ConnectException(\sprintf( + 'Connection to %s failed: [Error #%d] %s%s', + $uri, + $errno, + $errstr, + $failures ? '; previous attempts: ' . \implode($failures) : '' + ), $errno); + } + \stream_set_blocking($socket, false); + $deferred = new Deferred; + $watcher = Loop::onWritable($socket, [$deferred, 'resolve']); + $id = $token->subscribe([$deferred, 'fail']); + try { + yield Promise\timeout($deferred->promise(), $timeout); + } catch (TimeoutException $e) { + throw new ConnectException(\sprintf( + 'Connecting to %s failed: timeout exceeded (%d ms)%s', + $uri, + $timeout, + $failures ? '; previous attempts: ' . \implode($failures) : '' + ), 110); // See ETIMEDOUT in http://www.virtsync.com/c-error-codes-include-errno + } finally { + Loop::cancel($watcher); + $token->unsubscribe($id); + } + // The following hack looks like the only way to detect connection refused errors with PHP's stream sockets. + if (\stream_socket_get_name($socket, true) === false) { + \fclose($socket); + throw new ConnectException(\sprintf( + 'Connection to %s refused%s', + $uri, + $failures ? '; previous attempts: ' . \implode($failures) : '' + ), 111); // See ECONNREFUSED in http://www.virtsync.com/c-error-codes-include-errno + } + } catch (ConnectException $e) { + // Includes only error codes used in this file, as error codes on other OS families might be different. + // In fact, this might show a confusing error message on OS families that return 110 or 111 by itself. + $knownReasons = [ + 110 => 'connection timeout', + 111 => 'connection refused', + ]; + $code = $e->getCode(); + $reason = $knownReasons[$code] ?? ('Error #' . $code); + if (++$attempt === $socketContext->getMaxAttempts()) { + break; + } + $failures[] = "{$uri} ({$reason})"; + continue; // Could not connect to host, try next host in the list. + } + return ResourceSocket::fromClientSocket($socket, $socketContext->getTlsContext()); + } + // This is reached if either all URIs failed or the maximum number of attempts is reached. + /** @noinspection PhpUndefinedVariableInspection */ + throw $e; + + }); + } +} diff --git a/src/danog/MadelineProto/PredefinedConnector.php b/src/danog/MadelineProto/PredefinedConnector.php new file mode 100644 index 00000000..e69de29b diff --git a/src/danog/MadelineProto/ProxyConnector.php b/src/danog/MadelineProto/ProxyConnector.php deleted file mode 100644 index 745ae9df..00000000 --- a/src/danog/MadelineProto/ProxyConnector.php +++ /dev/null @@ -1,70 +0,0 @@ -. - * - * @author Daniil Gentili - * @copyright 2016-2019 Daniil Gentili - * @license https://opensource.org/licenses/AGPL-3.0 AGPLv3 - * - * @link https://docs.madelineproto.xyz MadelineProto documentation - */ - -namespace danog\MadelineProto; - -use Amp\Socket\Connector; - -class ProxyConnector implements Connector -{ - private $dataCenter; - private $fromDns = false; - public function __construct(DataCenter $dataCenter, bool $fromDns = false) - { - $this->dataCenter = $dataCenter; - $this->fromDns = false; - } - - public function connect(string $uri, ?ConnectContext $ctx = null, ?CancellationToken $token = null): Promise - { - return Tools::call(function () use ($uri, $ctx, $token) { - $ctx = $ctx ?? new ConnectContext; - $token = $token ?? new NullCancellationToken; - - $ctxs = $this->datacenter->generateContexts(0, $uri, $ctx); - if (empty($ctxs)) { - throw new Exception("No contexts for raw connection to URI $uri"); - } - foreach ($ctxs as $ctx) { - /* @var $ctx \danog\MadelineProto\Stream\ConnectionContext */ - try { - $ctx->setIsDns($this->fromDns); - $ctx->setCancellationToken($token); - $result = yield $ctx->getStream(); - $this->API->logger->logger('OK!', \danog\MadelineProto\Logger::WARNING); - - return $result->getSocket(); - } catch (\Throwable $e) { - if (\defined('MADELINEPROTO_TEST') && MADELINEPROTO_TEST === 'pony') { - throw $e; - } - $this->API->logger->logger('Connection failed: '.$e, \danog\MadelineProto\Logger::ERROR); - if ($e instanceof MultiReasonException) { - foreach ($e->getReasons() as $reason) { - $this->API->logger->logger('Multireason: '.$reason, \danog\MadelineProto\Logger::ERROR); - } - } - } - } - - throw new \danog\MadelineProto\Exception("Could not connect to URI $uri"); - - }); - } -} \ No newline at end of file diff --git a/src/danog/MadelineProto/Stream/ConnectionContext.php b/src/danog/MadelineProto/Stream/ConnectionContext.php index f026e078..7c6e4c6c 100644 --- a/src/danog/MadelineProto/Stream/ConnectionContext.php +++ b/src/danog/MadelineProto/Stream/ConnectionContext.php @@ -20,7 +20,6 @@ namespace danog\MadelineProto\Stream; use Amp\CancellationToken; use Amp\Socket\ConnectContext; -use Amp\Uri\Uri; use danog\MadelineProto\Exception; use danog\MadelineProto\Stream\MTProtoTransport\ObfuscatedStream; use danog\MadelineProto\Stream\Transport\DefaultStream; diff --git a/src/danog/MadelineProto/Stream/Transport/DefaultStream.php b/src/danog/MadelineProto/Stream/Transport/DefaultStream.php index bd58abc8..e61c9c6d 100644 --- a/src/danog/MadelineProto/Stream/Transport/DefaultStream.php +++ b/src/danog/MadelineProto/Stream/Transport/DefaultStream.php @@ -36,7 +36,7 @@ use function Amp\Socket\connector; * * @author Daniil Gentili */ -class DefaultStream extends Socket implements +class DefaultStream implements RawStreamInterface, ProxyStreamInterface { diff --git a/src/danog/MadelineProto/Stream/Transport/WsStream.php b/src/danog/MadelineProto/Stream/Transport/WsStream.php index 46c8aa79..045c03ad 100644 --- a/src/danog/MadelineProto/Stream/Transport/WsStream.php +++ b/src/danog/MadelineProto/Stream/Transport/WsStream.php @@ -46,7 +46,7 @@ class WsStream implements RawStreamInterface /** * Websocket stream. * - * @var Rfc6455Connection + * @var Connection */ private $stream; /** diff --git a/src/polyfill.php b/src/polyfill.php index 7427abb6..78a960b4 100644 --- a/src/polyfill.php +++ b/src/polyfill.php @@ -20,3 +20,7 @@ if (!\function_exists('error_clear_last')) { @\trigger_error(""); } } + +if (!\defined('MADELINEPROTO_TEST')) { + \define('MADELINEPROTO_TEST', 'NOT PONY'); +}