This commit is contained in:
Daniil Gentili 2019-12-13 14:40:48 +01:00
parent c8ed5971d9
commit 89bb80285b
Signed by: danog
GPG Key ID: 8C1BE3B34B230CA7
10 changed files with 251 additions and 241 deletions

View File

@ -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');

View File

@ -1,7 +1,7 @@
<?php
/**
* DataCenter DoH proxying AMPHP connector.
* 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.
@ -18,152 +18,56 @@
*/
namespace danog\MadelineProto;
use Amp\CancellationToken;
use Amp\MultiReasonException;
use Amp\NullCancellationToken;
use Amp\Promise;
use Amp\Socket\ConnectContext;
use Amp\Socket\Connector;
use danog\MadelineProto\Stream\ConnectionContext;
class ContextConnector implements Connector
{
/**
* Datacenter instance
*
* @property DataCenter $dataCenter
*/
private $dataCenter;
/**
* Connection context
*
* @var ConnectionContext
*/
private $ctx;
public function __construct(DataCenter $dataCenter, ConnectionContext $ctx)
private $fromDns = false;
public function __construct(DataCenter $dataCenter, bool $fromDns = false)
{
$this->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");
});
}

View File

@ -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]];
}

View File

@ -0,0 +1,182 @@
<?php
/**
* 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-2019 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\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;
});
}
}

View File

@ -1,70 +0,0 @@
<?php
/**
* 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-2019 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\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");
});
}
}

View File

@ -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;

View File

@ -36,7 +36,7 @@ use function Amp\Socket\connector;
*
* @author Daniil Gentili <daniil@daniil.it>
*/
class DefaultStream extends Socket implements
class DefaultStream implements
RawStreamInterface,
ProxyStreamInterface
{

View File

@ -46,7 +46,7 @@ class WsStream implements RawStreamInterface
/**
* Websocket stream.
*
* @var Rfc6455Connection
* @var Connection
*/
private $stream;
/**

View File

@ -20,3 +20,7 @@ if (!\function_exists('error_clear_last')) {
@\trigger_error("");
}
}
if (!\defined('MADELINEPROTO_TEST')) {
\define('MADELINEPROTO_TEST', 'NOT PONY');
}