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

159 lines
7.7 KiB
PHP

<?php
declare(strict_types=1);
/**
* 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>
* @copyright 2016-2023 Daniil Gentili <daniil@daniil.it>
* @license https://opensource.org/licenses/AGPL-3.0 AGPLv3
* @link https://docs.madelineproto.xyz MadelineProto documentation
*/
namespace danog\MadelineProto;
use Amp\Cancellation;
use Amp\CancelledException;
use Amp\DeferredFuture;
use Amp\Dns\DnsRecord;
use Amp\Dns\DnsTimeoutException;
use Amp\NullCancellation;
use Amp\Socket\ConnectContext;
use Amp\Socket\ConnectException;
use Amp\Socket\ResourceSocket;
use Amp\Socket\Socket;
use Amp\Socket\SocketAddress;
use Amp\Socket\SocketConnector;
use AssertionError;
use danog\MadelineProto\Stream\ConnectionContext;
use Revolt\EventLoop;
use const STREAM_CLIENT_ASYNC_CONNECT;
use const STREAM_CLIENT_CONNECT;
use function Amp\Socket\Internal\parseUri;
/** @internal */
final class DoHConnector implements SocketConnector
{
public function __construct(private DoHWrapper $dataCenter, private ConnectionContext $ctx)
{
}
public function connect(
SocketAddress|string $uri,
?ConnectContext $context = null,
?Cancellation $token = null
): Socket {
$socketContext = $context ?? new ConnectContext();
$token ??= new NullCancellation();
$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 = $this->dataCenter->nonProxiedDoHClient->resolve($host, $socketContext->getDnsTypeRestriction());
} else {
$records = $this->dataCenter->DoHClient->resolve($host, $socketContext->getDnsTypeRestriction());
}
usort($records, static fn (DnsRecord $a, DnsRecord $b) => $a->getType() - $b->getType());
if ($this->ctx->getIpv6()) {
$records = array_reverse($records);
}
foreach ($records as $record) {
if ($record->getType() === DnsRecord::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();
$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))) {
throw new ConnectException(sprintf('Connection to %s failed: [Error #%d] %s%s', (string) $uri, $errno, $errstr, $failures ? '; previous attempts: '.implode('', $failures) : ''), $errno);
}
stream_set_blocking($socket, false);
$deferred = new DeferredFuture();
/** @psalm-suppress InvalidArgument */
$watcher = EventLoop::onWritable($socket, $deferred->complete(...));
$id = $token->subscribe($deferred->error(...));
try {
$deferred->getFuture()->await(Tools::getTimeoutCancellation($timeout));
} catch (CancelledException $e) {
if (!$e->getPrevious() instanceof DnsTimeoutException) {
throw $e;
}
throw new ConnectException(sprintf('Connecting to %s failed: timeout exceeded (%d ms)%s', (string) $uri, $timeout, $failures ? '; previous attempts: '.implode('', $failures) : ''), 110);
// 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);
throw new ConnectException(sprintf('Connection to %s refused%s', (string) $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;
$failures[] = "{$uri} ({$reason})";
continue;
}
return ResourceSocket::fromClientSocket($socket, $socketContext->getTlsContext());
}
// This is reached if either all URIs failed or the maximum number of attempts is reached.
if ($e) {
throw $e;
}
throw new AssertionError("Unreachable!");
}
}