diff --git a/.travis.yml b/.travis.yml index d5ce804..a2f3e19 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,8 @@ language: php php: - - '7.1' - '7.2' - '7.3' + - '7.4' sudo: false script: - make test diff --git a/composer.json b/composer.json index fd1be8c..ffea76b 100644 --- a/composer.json +++ b/composer.json @@ -12,8 +12,8 @@ "php": "^7.1", "react/event-loop": "^1.0", "react/socket": "^1.1", - "symfony/console": "^4.1", - "symfony/http-foundation": "^4.1" + "symfony/console": "^5.0", + "symfony/http-foundation": "^5.0" }, "autoload": { "psr-4": { @@ -22,9 +22,9 @@ }, "require-dev": { "friendsofphp/php-cs-fixer": "^2.13", - "phpstan/phpstan": "^0.11.2", - "phpstan/phpstan-strict-rules": "^0.11.0", - "phpunit/phpunit": "^7.3|^8.0" + "phpstan/phpstan": "^0.12.23", + "phpstan/phpstan-strict-rules": "^0.12.2", + "phpunit/phpunit": "^7.0|^8.0|^9.0" }, "autoload-dev": { "psr-4": { diff --git a/phproxy-wrapper.sh b/phproxy-wrapper.sh new file mode 100755 index 0000000..fd3870d --- /dev/null +++ b/phproxy-wrapper.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +set -axupe + +PROXY_PORT=4269 +PACPORT=4270 +LOCAL_IP=$(ip -o route get to 8.8.8.8 | sed -n 's/.*src \([0-9.]\+\).*/\1/p') + +echo "Listening on ${LOCAL_IP}:${PROXY_PORT}" +echo "Autoconfiguration url : http://${LOCAL_IP}:${PACPORT}/" +$(dirname ${0})/bin/phproxy run 0.0.0.0:${PROXY_PORT} --pac 0.0.0.0:${PACPORT}:${LOCAL_IP}:${PROXY_PORT} "$@" diff --git a/src/Application.php b/src/Application.php index e51db4e..2d9e65d 100644 --- a/src/Application.php +++ b/src/Application.php @@ -5,22 +5,37 @@ namespace Paxal\Phproxy; use Paxal\Phproxy\Command\ProxyCommand; +use Paxal\Phproxy\Logger\LoggerWrapper; use React\EventLoop\Factory; use Symfony\Component\Console\Application as BaseApplication; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Logger\ConsoleLogger; +use Symfony\Component\Console\Output\OutputInterface; final class Application extends BaseApplication { /** @var \React\EventLoop\LoopInterface */ private $loop; + /** @var LoggerWrapper */ + private $logger; public function __construct() { parent::__construct('PHPROXY', '@package_version@'); $this->loop = Factory::create(); + $this->logger = new LoggerWrapper(); - $command = new ProxyCommand($this->loop); + $command = new ProxyCommand($this->loop, $this->logger); $this->add($command); - $this->setDefaultCommand($command->getName()); + $this->setDefaultCommand((string) $command->getName()); + } + + protected function doRunCommand(Command $command, InputInterface $input, OutputInterface $output): int + { + $this->logger->setLogger(new ConsoleLogger($output)); + + return parent::doRunCommand($command, $input, $output); } } diff --git a/src/Command/OptionsHelper.php b/src/Command/OptionsHelper.php index 952a906..3c80f06 100644 --- a/src/Command/OptionsHelper.php +++ b/src/Command/OptionsHelper.php @@ -6,6 +6,9 @@ class OptionsHelper { + /** + * @return array> + */ public static function read(string $filename): array { $contents = @file_get_contents($filename); @@ -25,6 +28,9 @@ public static function read(string $filename): array return $json; } + /** + * @param array> $options + */ public static function save(string $filename, array $options): void { @file_put_contents( diff --git a/src/Command/ProxyCommand.php b/src/Command/ProxyCommand.php index eaf9189..b06631b 100644 --- a/src/Command/ProxyCommand.php +++ b/src/Command/ProxyCommand.php @@ -9,6 +9,7 @@ use Paxal\Phproxy\Proxy\ConnectionHandler; use Paxal\Phproxy\Proxy\DataHandlerFactory; use Paxal\Phproxy\Translator\TranslatorBuilder; +use Psr\Log\LoggerInterface; use React\EventLoop\LoopInterface; use React\Socket\SecureServer; use React\Socket\TcpServer; @@ -25,10 +26,16 @@ class ProxyCommand extends Command */ private $loop; - public function __construct(LoopInterface $loop) + /** + * @var LoggerInterface + */ + private $logger; + + public function __construct(LoopInterface $loop, LoggerInterface $logger) { parent::__construct(); $this->loop = $loop; + $this->logger = $logger; } protected function configure(): void @@ -47,14 +54,14 @@ protected function configure(): void ->addArgument('binding', InputArgument::OPTIONAL, 'Bind address', '127.0.0.1:8001'); } - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $configurationFile = $input->getOption('config'); if (is_string($configurationFile)) { if (file_exists($configurationFile)) { $savedOptions = $this->cleanOptionForSave(OptionsHelper::read($configurationFile)); foreach ($savedOptions as $name => $option) { - $input->setOption($name, $input->getOption($name) ?: $option); + $input->setOption($name, $input->getOption($name) ?? $option); } } @@ -64,14 +71,16 @@ protected function execute(InputInterface $input, OutputInterface $output) } $translatorBuilder = $this->buildTranslatorBuilder($input); + /** @phpstan-ignore-next-line */ $authenticator = AuthenticatorFactory::create((array) $input->getOption('auth')); - $dataHandlerFactory = new DataHandlerFactory($this->loop, $translatorBuilder, $authenticator); + $dataHandlerFactory = new DataHandlerFactory($this->loop, $translatorBuilder, $authenticator, $this->logger); $binding = $input->getArgument('binding'); if (!is_string($binding)) { throw new \RuntimeException('Invalid argument binding.'); } $server = new TcpServer($binding, $this->loop); + $this->logger->info('PROXY Server listening on {binding}', ['binding' => $binding]); if ((bool) $input->getOption('ssl')) { $context = []; @@ -95,6 +104,9 @@ protected function execute(InputInterface $input, OutputInterface $output) } $this->loop->run(); + + // Never reached + return 1; } private function buildTranslatorBuilder(InputInterface $input): TranslatorBuilder @@ -109,6 +121,11 @@ private function buildTranslatorBuilder(InputInterface $input): TranslatorBuilde return $translatorBuilder; } + /** + * @param array|string|null> $options + * + * @return array + */ private function cleanOptionForSave(array $options): array { $cleaned = []; @@ -118,22 +135,24 @@ private function cleanOptionForSave(array $options): array } } + /* @phpstan-ignore-next-line */ return $cleaned; } private function configurePACServer(string $pacConfiguration, TranslatorBuilder $translatorBuilder): void { - $serverFactory = new ServerFactory($this->loop); + $serverFactory = new ServerFactory($this->loop, $this->logger); [$binding, $proxyHost] = $this->parsePACConfiguration($pacConfiguration); $serverFactory->create($binding, $proxyHost, $translatorBuilder); } + /** + * @return array{0: string, 1: string} + */ private function parsePACConfiguration(string $pacConfiguration): array { if (!(bool) preg_match('|^(.+?:\d+?):(.+?:\d+?)$|', $pacConfiguration, $matches)) { - throw new \InvalidArgumentException( - 'Bad PAC configuration value : should be BINDING_HOST:BINDING_PORT:EXTERNAL_PROXY_HOST:EXTERNAL_PROXY_PORT' - ); + throw new \InvalidArgumentException('Bad PAC configuration value : should be BINDING_HOST:BINDING_PORT:EXTERNAL_PROXY_HOST:EXTERNAL_PROXY_PORT'); } return [$matches[1], $matches[2]]; diff --git a/src/Logger/LoggerWrapper.php b/src/Logger/LoggerWrapper.php new file mode 100644 index 0000000..a19cdb1 --- /dev/null +++ b/src/Logger/LoggerWrapper.php @@ -0,0 +1,24 @@ +logger = new NullLogger(); + } + + public function log($level, $message, array $context = []) + { + $this->logger->log($level, $message, $context); + } +} diff --git a/src/PAC/ResponseFactory.php b/src/PAC/ResponseFactory.php index 29b9a8f..562bf82 100644 --- a/src/PAC/ResponseFactory.php +++ b/src/PAC/ResponseFactory.php @@ -50,10 +50,7 @@ function FindProxyForURL(url, host) { /** * Build matches given hostnames. * - * @param string $proxyHost * @param string[] $domains - * - * @return string */ private static function buildMatches(string $proxyHost, array $domains): string { diff --git a/src/PAC/ServerFactory.php b/src/PAC/ServerFactory.php index b6fb273..1016679 100644 --- a/src/PAC/ServerFactory.php +++ b/src/PAC/ServerFactory.php @@ -5,6 +5,7 @@ namespace Paxal\Phproxy\PAC; use Paxal\Phproxy\Translator\TranslatorBuilder; +use Psr\Log\LoggerInterface; use React\EventLoop\LoopInterface; use React\Socket\ConnectionInterface; use React\Socket\ServerInterface; @@ -20,9 +21,13 @@ class ServerFactory /** @var LoopInterface */ private $loop; - public function __construct(LoopInterface $loop) + /** @var LoggerInterface */ + private $logger; + + public function __construct(LoopInterface $loop, LoggerInterface $logger) { $this->loop = $loop; + $this->logger = $logger; } /** @@ -31,8 +36,6 @@ public function __construct(LoopInterface $loop) * @param string $binding Binding host, eg ip:port * @param string $proxyHost The proxy host, as of remote-side view (eg external_ip:port) * @param TranslatorBuilder $translatorBuilder The translator builder - * - * @return ServerInterface */ public function create(string $binding, string $proxyHost, TranslatorBuilder $translatorBuilder): ServerInterface { @@ -42,14 +45,19 @@ public function create(string $binding, string $proxyHost, TranslatorBuilder $tr $this->handle($connection, $contents); }); + $this->loop->futureTick(function () use ($binding, $contents): void { + $this->logger->info('PAC Server listening on {binding}', ['binding' => $binding]); + $this->logger->debug('PAC Contents :'.PHP_EOL.$contents); + }); + return $server; } private function handle(ConnectionInterface $connection, string $contents): void { // Hodor ! - $connection->on('data', function () use ($contents, $connection) { - error_log('Connection from '.$connection->getRemoteAddress()); + $connection->on('data', function () use ($contents, $connection): void { + $this->logger->info('New PAC Connection from {remote}', ['remote' => $connection->getRemoteAddress()]); $connection->end($contents); }); } diff --git a/src/Proxy/Authenticator/AuthenticatorFactory.php b/src/Proxy/Authenticator/AuthenticatorFactory.php index f7cb2c2..d163069 100644 --- a/src/Proxy/Authenticator/AuthenticatorFactory.php +++ b/src/Proxy/Authenticator/AuthenticatorFactory.php @@ -6,7 +6,12 @@ final class AuthenticatorFactory { - public static function create(array $credentials, string $type = 'Basic'): Authenticator + /** + * Creates authenticator. If any credential is given, it will be considered as basic. + * + * @param string[] $credentials + */ + public static function create(array $credentials, string $type = 'basic'): Authenticator { if (0 !== count($credentials)) { switch ($type) { diff --git a/src/Proxy/Authenticator/BasicAuthenticator.php b/src/Proxy/Authenticator/BasicAuthenticator.php index fcc23ad..ba0b53b 100644 --- a/src/Proxy/Authenticator/BasicAuthenticator.php +++ b/src/Proxy/Authenticator/BasicAuthenticator.php @@ -9,15 +9,23 @@ final class BasicAuthenticator implements Authenticator { /** - * @var array + * @var array */ private $credentials; + /** + * @param array $credentials + */ public function __construct(array $credentials) { $this->credentials = array_flip($this->encode($credentials)); } + /** + * @param array $credentials + * + * @return array + */ private function encode(array $credentials): array { return array_map( @@ -30,7 +38,7 @@ function (string $credentials): string { public function isAuthorized(ProxyRequest $request): bool { - $headerValue = $request->getHeaders()->get('proxy-authorization', null, true); + $headerValue = $request->getHeaders()->get('proxy-authorization', null); if (!\is_string($headerValue)) { return false; } diff --git a/src/Proxy/DataHandler.php b/src/Proxy/DataHandler.php index f5d2df2..c2720b3 100644 --- a/src/Proxy/DataHandler.php +++ b/src/Proxy/DataHandler.php @@ -8,6 +8,7 @@ use Paxal\Phproxy\Proxy\Protocol\ProtocolProvider; use Paxal\Phproxy\Proxy\Request\ProxyRequest; use Paxal\Phproxy\Translator\TranslatorInterface; +use Psr\Log\LoggerInterface; use React\EventLoop\LoopInterface; use React\Socket\ConnectionInterface; use React\Socket\Connector; @@ -38,19 +39,37 @@ final class DataHandler */ private $connection; - public function __construct(LoopInterface $loop, TranslatorInterface $translator, Authenticator $authenticator, ConnectionInterface $connection) - { + /** + * @var LoggerInterface + */ + private $logger; + + public function __construct( + LoopInterface $loop, + TranslatorInterface $translator, + Authenticator $authenticator, + ConnectionInterface $connection, + LoggerInterface $logger + ) { $this->loop = $loop; $this->translator = $translator; $this->authenticator = $authenticator; $this->connection = $connection; + $this->logger = $logger; } public function __invoke(string $data): void { try { $request = ProxyRequest::create($data); + + $this->logger->info('{method} {uri} {protocol}', [ + 'method' => $request->getMethod(), + 'uri' => $request->getUri(), + 'protocol' => $request->getProtocol(), + ]); } catch (\Throwable $e) { + $this->logger->error('Unrecognized command', ['error' => $e->getMessage()]); $this->sendError(500, 'Unrecognized command : '.$e->getMessage()); return; @@ -80,17 +99,20 @@ public function __invoke(string $data): void $protocol->onClose($this); $targetHost = $this->translator->translate($protocol->getTargetHost()); - error_log("{$request->getUri()} -> {$targetHost}"); + $this->logger->notice('Redirecting : {uri} => {target}', ['uri' => $request->getUri(), 'target' => $targetHost]); (new Connector($this->loop)) ->connect($targetHost) ->then( $protocol, - function (\Exception $exception) { + function (\Exception $exception): void { $this->sendError(500, 'Unable to connect', $exception->getMessage()); } ); } + /** + * @param array $headers + */ private function sendError(int $status, string $statusText, string $content = '', string $contentType = 'text/plain', array $headers = []): void { $response = Response::create( diff --git a/src/Proxy/DataHandlerFactory.php b/src/Proxy/DataHandlerFactory.php index 57f18c9..02bf5f7 100644 --- a/src/Proxy/DataHandlerFactory.php +++ b/src/Proxy/DataHandlerFactory.php @@ -7,6 +7,7 @@ use Paxal\Phproxy\Proxy\Authenticator\Authenticator; use Paxal\Phproxy\Translator\TranslatorBuilder; use Paxal\Phproxy\Translator\TranslatorInterface; +use Psr\Log\LoggerInterface; use React\EventLoop\LoopInterface; use React\Socket\ConnectionInterface; @@ -27,17 +28,23 @@ final class DataHandlerFactory */ private $authenticator; - public function __construct(LoopInterface $loop, TranslatorBuilder $translatorBuilder, Authenticator $authenticator) + /** + * @var LoggerInterface + */ + private $logger; + + public function __construct(LoopInterface $loop, TranslatorBuilder $translatorBuilder, Authenticator $authenticator, LoggerInterface $logger) { $this->loop = $loop; $this->translator = $translatorBuilder->build(); $this->authenticator = $authenticator; + $this->logger = $logger; } public function create(ConnectionInterface $connection): DataHandler { - error_log('Proxy Connection from '.$connection->getRemoteAddress()); + $this->logger->info('Proxy Connection from {remote}', ['remote' => $connection->getRemoteAddress()]); - return new DataHandler($this->loop, $this->translator, $this->authenticator, $connection); + return new DataHandler($this->loop, $this->translator, $this->authenticator, $connection, $this->logger); } } diff --git a/src/Proxy/Protocol/AbstractProtocol.php b/src/Proxy/Protocol/AbstractProtocol.php index 5ff0bed..383564e 100644 --- a/src/Proxy/Protocol/AbstractProtocol.php +++ b/src/Proxy/Protocol/AbstractProtocol.php @@ -47,7 +47,7 @@ protected function pipe(): void { $this->local->pipe($this->remote); $this->remote->pipe($this->local, [/*'end' => false*/]); - $this->remote->on('close', function () { + $this->remote->on('close', function (): void { $this->local->on('data', $this->afterClosedOnData); }); } diff --git a/src/Proxy/Protocol/DirectProtocol.php b/src/Proxy/Protocol/DirectProtocol.php index 57258f4..c625d85 100644 --- a/src/Proxy/Protocol/DirectProtocol.php +++ b/src/Proxy/Protocol/DirectProtocol.php @@ -72,8 +72,6 @@ private function getTargetHeaders(): string * * @param string $scheme The scheme * - * @return int - * * @throws \Exception */ private function getDefaultPort(string $scheme): int diff --git a/src/Proxy/Request/ProxyRequest.php b/src/Proxy/Request/ProxyRequest.php index e34844b..c14f7d3 100644 --- a/src/Proxy/Request/ProxyRequest.php +++ b/src/Proxy/Request/ProxyRequest.php @@ -27,7 +27,7 @@ final class ProxyRequest private $protocol; /** - * @var HeaderBag + * @var HeaderBag */ private $headers; @@ -36,6 +36,9 @@ final class ProxyRequest */ private $body; + /** + * @param HeaderBag $headers + */ private function __construct(string $method, string $uri, string $protocol, HeaderBag $headers, string $body) { $this->method = $method; @@ -63,6 +66,11 @@ public static function create(string $data): self return new self($matches['METHOD'], $matches['URI'], $matches['PROTOCOL'], static::parseHeaders($headersLines), $body); } + /** + * @param string[] $headers + * + * @return HeaderBag + */ private static function parseHeaders(array $headers): HeaderBag { $bag = new HeaderBag(); @@ -74,41 +82,29 @@ private static function parseHeaders(array $headers): HeaderBag return $bag; } - /** - * @return string - */ public function getMethod(): string { return $this->method; } - /** - * @return string - */ public function getUri(): string { return $this->uri; } - /** - * @return string - */ public function getProtocol(): string { return $this->protocol; } /** - * @return HeaderBag + * @return HeaderBag */ public function getHeaders(): HeaderBag { return $this->headers; } - /** - * @return string - */ public function getBody(): string { return $this->body; diff --git a/src/Translator/Translator.php b/src/Translator/Translator.php index 1fe4408..046c840 100644 --- a/src/Translator/Translator.php +++ b/src/Translator/Translator.php @@ -9,26 +9,26 @@ class Translator implements TranslatorInterface /** * List of all translations. * - * @var string[] + * @var array */ private $translations = []; /** * List of translations that translate a wildcard domain. * - * @var string[] + * @var array */ private $suffixHostsTranslations = []; /** - * @var string[] List of all translations + * @param array $translations List of all translations */ public function __construct(array $translations = []) { $this->translations = $translations; $this->suffixHostsTranslations = array_filter( $translations, - function (string $hostname) { + function (string $hostname): bool { return '.' === $hostname[0]; }, ARRAY_FILTER_USE_KEY diff --git a/src/Translator/TranslatorBuilder.php b/src/Translator/TranslatorBuilder.php index 31741d0..de72f9b 100644 --- a/src/Translator/TranslatorBuilder.php +++ b/src/Translator/TranslatorBuilder.php @@ -17,8 +17,6 @@ protected function __construct() /** * Build the translator. - * - * @return TranslatorInterface */ public function build(): TranslatorInterface {