1
0
mirror of https://github.com/danog/MadelineProto.git synced 2024-12-02 16:17:46 +01:00
MadelineProto/src/DoHConnector.php

159 lines
7.7 KiB
PHP
Raw Normal View History

2022-12-30 21:54:44 +01:00
<?php
declare(strict_types=1);
2019-12-13 14:40:48 +01:00
/**
* DataCenter DoH proxying AMPHP connector.
*
* This file is part of MadelineProto.
* MadelineProto is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
* MadelineProto is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Affero General Public License for more details.
* You should have received a copy of the GNU General Public License along with MadelineProto.
* If not, see <http://www.gnu.org/licenses/>.
*
* @author Daniil Gentili <daniil@daniil.it>
2023-01-04 12:43:01 +01:00
* @copyright 2016-2023 Daniil Gentili <daniil@daniil.it>
2019-12-13 14:40:48 +01:00
* @license https://opensource.org/licenses/AGPL-3.0 AGPLv3
* @link https://docs.madelineproto.xyz MadelineProto documentation
*/
namespace danog\MadelineProto;
2023-01-04 15:13:55 +01:00
use Amp\Cancellation;
2023-01-11 18:47:27 +01:00
use Amp\CancelledException;
2022-12-30 20:24:13 +01:00
use Amp\DeferredFuture;
2023-01-12 14:12:52 +01:00
use Amp\Dns\DnsRecord;
2023-01-13 14:36:10 +01:00
use Amp\Dns\DnsTimeoutException;
2023-01-04 15:13:55 +01:00
use Amp\NullCancellation;
2019-12-13 14:40:48 +01:00
use Amp\Socket\ConnectContext;
use Amp\Socket\ConnectException;
use Amp\Socket\ResourceSocket;
2023-01-22 15:39:10 +01:00
use Amp\Socket\Socket;
2023-01-04 15:13:55 +01:00
use Amp\Socket\SocketAddress;
use Amp\Socket\SocketConnector;
use Amp\TimeoutCancellation;
2023-01-11 18:47:27 +01:00
use AssertionError;
2019-12-13 14:40:48 +01:00
use danog\MadelineProto\Stream\ConnectionContext;
2022-12-30 21:43:58 +01:00
use Revolt\EventLoop;
2022-12-30 19:21:36 +01:00
use const STREAM_CLIENT_ASYNC_CONNECT;
use const STREAM_CLIENT_CONNECT;
2019-12-13 14:40:48 +01:00
use function Amp\Socket\Internal\parseUri;
2023-01-15 12:05:38 +01:00
final class DoHConnector implements SocketConnector
2019-12-13 14:40:48 +01:00
{
2023-01-04 16:04:05 +01:00
public function __construct(private DoHWrapper $dataCenter, private ConnectionContext $ctx)
2019-12-13 14:40:48 +01:00
{
}
2023-01-04 15:13:55 +01:00
public function connect(
SocketAddress|string $uri,
?ConnectContext $context = null,
2023-01-11 18:47:27 +01:00
?Cancellation $token = null
2023-01-22 15:39:10 +01:00
): Socket {
2023-01-03 22:07:58 +01:00
$socketContext = $context ?? new ConnectContext();
2023-01-04 15:13:55 +01:00
$token ??= new NullCancellation();
2023-01-03 22:07:58 +01:00
$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.
2023-01-04 15:13:55 +01:00
//
2023-01-03 22:07:58 +01:00
// 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.
2023-01-04 15:13:55 +01:00
//
2023-01-03 22:07:58 +01:00
// 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
2023-01-04 15:13:55 +01:00
//
2023-01-03 22:07:58 +01:00
// resolve host.com => (DoH resolver) => resolve dohserver.com => (simple resolver) => OK
2023-01-04 15:13:55 +01:00
//
// |> resolve dohserver.com => (simple resolver) => OK
2023-01-03 22:07:58 +01:00
// resolve host.com => (DoH resolver) =|
2023-01-04 15:13:55 +01:00
// |> resolve proxy.com => (non-proxied resolver) => OK
//
//
2023-01-03 22:07:58 +01:00
// This means that we must detect if the domain name we're trying to resolve is a proxy domain name.
2023-01-04 15:13:55 +01:00
//
2023-01-03 22:07:58 +01:00
// 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()) {
2023-01-11 18:47:27 +01:00
$records = $this->dataCenter->nonProxiedDoHClient->resolve($host, $socketContext->getDnsTypeRestriction());
2023-01-03 22:07:58 +01:00
} else {
2023-01-11 18:47:27 +01:00
$records = $this->dataCenter->DoHClient->resolve($host, $socketContext->getDnsTypeRestriction());
2023-01-03 22:07:58 +01:00
}
2023-01-12 14:12:52 +01:00
\usort($records, fn (DnsRecord $a, DnsRecord $b) => $a->getType() - $b->getType());
2023-01-03 22:07:58 +01:00
if ($this->ctx->getIpv6()) {
$records = \array_reverse($records);
}
foreach ($records as $record) {
2023-01-12 14:12:52 +01:00
if ($record->getType() === DnsRecord::AAAA) {
2023-01-04 15:13:55 +01:00
$uris[] = \sprintf('%s://[%s]:%d', $scheme, $record->getValue(), $port);
2019-12-13 14:40:48 +01:00
} else {
2023-01-04 15:13:55 +01:00
$uris[] = \sprintf('%s://%s:%d', $scheme, $record->getValue(), $port);
2019-12-13 14:40:48 +01:00
}
}
2023-01-03 22:07:58 +01:00
}
$flags = STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT;
$timeout = $socketContext->getConnectTimeout();
$e = null;
foreach ($uris as $builtUri) {
try {
$streamContext = \stream_context_create($socketContext->withoutTlsContext()->toStreamContextArray());
/** @psalm-suppress NullArgument */
if (!($socket = @\stream_socket_client($builtUri, $errno, $errstr, null, $flags, $streamContext))) {
2023-01-15 11:21:57 +01:00
throw new ConnectException(\sprintf('Connection to %s failed: [Error #%d] %s%s', (string) $uri, $errno, $errstr, $failures ? '; previous attempts: '.\implode($failures) : ''), $errno);
2023-01-03 22:07:58 +01:00
}
\stream_set_blocking($socket, false);
$deferred = new DeferredFuture();
/** @psalm-suppress InvalidArgument */
2023-01-04 15:13:55 +01:00
$watcher = EventLoop::onWritable($socket, $deferred->complete(...));
$id = $token->subscribe($deferred->error(...));
2019-12-13 14:40:48 +01:00
try {
2023-01-04 15:13:55 +01:00
$deferred->getFuture()->await(new TimeoutCancellation($timeout));
2023-01-11 18:47:27 +01:00
} catch (CancelledException $e) {
2023-01-13 14:36:10 +01:00
if (!$e->getPrevious() instanceof DnsTimeoutException) {
2023-01-11 18:47:27 +01:00
throw $e;
}
2023-01-15 11:21:57 +01:00
throw new ConnectException(\sprintf('Connecting to %s failed: timeout exceeded (%d ms)%s', (string) $uri, $timeout, $failures ? '; previous attempts: '.\implode($failures) : ''), 110);
2023-01-03 22:07:58 +01:00
// See ETIMEDOUT in http://www.virtsync.com/c-error-codes-include-errno
} finally {
EventLoop::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);
2023-01-15 11:21:57 +01:00
throw new ConnectException(\sprintf('Connection to %s refused%s', (string) $uri, $failures ? '; previous attempts: '.\implode($failures) : ''), 111);
2023-01-03 22:07:58 +01:00
// See ECONNREFUSED in http://www.virtsync.com/c-error-codes-include-errno
2019-12-13 14:40:48 +01:00
}
2023-01-03 22:07:58 +01:00
} 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;
$failures[] = "{$uri} ({$reason})";
continue;
2019-12-13 14:40:48 +01:00
}
2023-01-03 22:07:58 +01:00
return ResourceSocket::fromClientSocket($socket, $socketContext->getTlsContext());
}
2020-03-07 21:45:50 +01:00
2023-01-03 22:07:58 +01:00
// This is reached if either all URIs failed or the maximum number of attempts is reached.
if ($e) {
throw $e;
}
2023-01-11 18:47:27 +01:00
throw new AssertionError("Unreachable!");
2019-12-13 14:40:48 +01:00
}
}