Skip to content

Commit 913b349

Browse files
committed
feat: add request proxy middleware
1 parent e896c1a commit 913b349

21 files changed

+1729
-1
lines changed

src/Core/DefaultContainer.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ public function has(string $id): bool
5858

5959
private function registerDefaultBindings(): void
6060
{
61+
$this->container->addShared(
62+
ContainerInterface::class,
63+
$this->container,
64+
);
6165
$this->container->addShared(
6266
LoggerInterface::class,
6367
static fn () => (new Logger('roach'))->pushHandler(new StreamHandler('php://stdout')),
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* Copyright (c) 2024 Kai Sassnowski
7+
*
8+
* For the full copyright and license information, please view
9+
* the LICENSE file that was distributed with this source code.
10+
*
11+
* @see https://github.com/roach-php/roach
12+
*/
13+
14+
namespace RoachPHP\Downloader\Middleware;
15+
16+
use Psr\Container\ContainerInterface;
17+
use Psr\Log\LoggerInterface;
18+
use RoachPHP\Downloader\Proxy\ArrayConfigurationLoader;
19+
use RoachPHP\Downloader\Proxy\ConfigurationLoaderInterface;
20+
use RoachPHP\Downloader\Proxy\Proxy;
21+
use RoachPHP\Http\Request;
22+
use RoachPHP\Support\Configurable;
23+
24+
final class ProxyMiddleware implements RequestMiddlewareInterface
25+
{
26+
use Configurable;
27+
28+
private ?Proxy $proxy = null;
29+
30+
public function __construct(
31+
private readonly ContainerInterface $container,
32+
private readonly LoggerInterface $logger,
33+
) {
34+
}
35+
36+
public function handleRequest(Request $request): Request
37+
{
38+
if (null === $this->proxy) {
39+
$this->logger->warning(
40+
'[ProxyMiddleware] No proxy configured for middleware',
41+
);
42+
43+
return $request;
44+
}
45+
46+
$options = $this->proxy->optionsFor($request);
47+
48+
if ($options->isEmpty()) {
49+
return $request;
50+
}
51+
52+
$this->logger->info(
53+
'[ProxyMiddleware] Using proxy for request',
54+
$options->toArray(),
55+
);
56+
57+
return $request->addOption('proxy', $options->toArray());
58+
}
59+
60+
private function defaultOptions(): array
61+
{
62+
return [
63+
'proxy' => [],
64+
'loader' => null,
65+
];
66+
}
67+
68+
private function onAfterConfigured(): void
69+
{
70+
/** @var null|class-string<ConfigurationLoaderInterface> $loaderClass */
71+
$loaderClass = $this->option('loader');
72+
73+
if (null !== $loaderClass) {
74+
/** @var ConfigurationLoaderInterface $loader */
75+
$loader = $this->container->get($loaderClass);
76+
} else {
77+
/** @var array<string, array{http?: string, https?: string, no?: array<int, string>}|string>|string $options */
78+
$options = $this->option('proxy');
79+
$loader = new ArrayConfigurationLoader($options);
80+
}
81+
82+
$this->proxy = $loader->loadProxyConfiguration();
83+
}
84+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* Copyright (c) 2024 Kai Sassnowski
7+
*
8+
* For the full copyright and license information, please view
9+
* the LICENSE file that was distributed with this source code.
10+
*
11+
* @see https://github.com/roach-php/roach
12+
*/
13+
14+
namespace RoachPHP\Downloader\Proxy;
15+
16+
final class ArrayConfigurationLoader implements ConfigurationLoaderInterface
17+
{
18+
/**
19+
* @param array<string, array{http?: string, https?: string, no?: array<int, string>}|string>|string $params
20+
*/
21+
public function __construct(private readonly array|string $params)
22+
{
23+
}
24+
25+
public function loadProxyConfiguration(): Proxy
26+
{
27+
if (\is_string($this->params)) {
28+
return new Proxy([
29+
'*' => ProxyOptions::allProtocols($this->params),
30+
]);
31+
}
32+
33+
/** @var array<string, ProxyOptions> $proxyList */
34+
$proxyList = [];
35+
36+
foreach ($this->params as $domain => $options) {
37+
if (\is_string($options)) {
38+
$proxyList[$domain] = ProxyOptions::allProtocols($options);
39+
} else {
40+
$proxyList[$domain] = new ProxyOptions(
41+
$options['http'] ?? null,
42+
$options['https'] ?? null,
43+
$options['no'] ?? [],
44+
);
45+
}
46+
}
47+
48+
return new Proxy($proxyList);
49+
}
50+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* Copyright (c) 2024 Kai Sassnowski
7+
*
8+
* For the full copyright and license information, please view
9+
* the LICENSE file that was distributed with this source code.
10+
*
11+
* @see https://github.com/roach-php/roach
12+
*/
13+
14+
namespace RoachPHP\Downloader\Proxy;
15+
16+
interface ConfigurationLoaderInterface
17+
{
18+
public function loadProxyConfiguration(): Proxy;
19+
}

src/Downloader/Proxy/Proxy.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* Copyright (c) 2024 Kai Sassnowski
7+
*
8+
* For the full copyright and license information, please view
9+
* the LICENSE file that was distributed with this source code.
10+
*
11+
* @see https://github.com/roach-php/roach
12+
*/
13+
14+
namespace RoachPHP\Downloader\Proxy;
15+
16+
use RoachPHP\Http\Request;
17+
18+
final class Proxy
19+
{
20+
/**
21+
* @param array<string, ProxyOptions> $proxyList
22+
*/
23+
public function __construct(private readonly array $proxyList = [])
24+
{
25+
}
26+
27+
public function optionsFor(Request $request): ProxyOptions
28+
{
29+
$host = $request->url->host;
30+
31+
if (null === $host) {
32+
return ProxyOptions::make();
33+
}
34+
35+
if (\array_key_exists($host, $this->proxyList)) {
36+
return $this->proxyList[$host];
37+
}
38+
39+
if (\array_key_exists('*', $this->proxyList)) {
40+
return $this->proxyList['*'];
41+
}
42+
43+
return ProxyOptions::make();
44+
}
45+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* Copyright (c) 2024 Kai Sassnowski
7+
*
8+
* For the full copyright and license information, please view
9+
* the LICENSE file that was distributed with this source code.
10+
*
11+
* @see https://github.com/roach-php/roach
12+
*/
13+
14+
namespace RoachPHP\Downloader\Proxy;
15+
16+
final class ProxyOptions
17+
{
18+
/**
19+
* @param array<int, string> $excludedDomains
20+
*/
21+
public function __construct(
22+
private readonly ?string $httpProxyURL = null,
23+
private readonly ?string $httpsProxyURL = null,
24+
private readonly array $excludedDomains = [],
25+
) {
26+
}
27+
28+
public static function make(): self
29+
{
30+
return new self();
31+
}
32+
33+
/**
34+
* Configure the same proxy URL to be used for HTTP and HTTPS.
35+
*/
36+
public static function allProtocols(string $url): self
37+
{
38+
return new self($url, $url, []);
39+
}
40+
41+
/**
42+
* Configure the proxy URL to be used for requests using HTTP.
43+
*/
44+
public function http(string $url): self
45+
{
46+
return new self($url, $this->httpsProxyURL, $this->excludedDomains);
47+
}
48+
49+
/**
50+
* Configure the proxy URL to be used for requests using HTTPs.
51+
*/
52+
public function https(string $url): self
53+
{
54+
return new self($this->httpProxyURL, $url, $this->excludedDomains);
55+
}
56+
57+
/**
58+
* Configure the domains or TLDs that should not use proxies.
59+
*
60+
* @param array<int, string>|string $domains
61+
*/
62+
public function exclude(array|string $domains): self
63+
{
64+
return new self(
65+
$this->httpProxyURL,
66+
$this->httpsProxyURL,
67+
(array) $domains,
68+
);
69+
}
70+
71+
public function isEmpty(): bool
72+
{
73+
return null === $this->httpProxyURL
74+
&& null === $this->httpsProxyURL
75+
&& \count($this->excludedDomains) === 0;
76+
}
77+
78+
public function equals(self $other): bool
79+
{
80+
return $this->httpProxyURL === $other->httpProxyURL
81+
&& $this->httpsProxyURL === $other->httpsProxyURL
82+
&& $this->excludedDomains === $other->excludedDomains;
83+
}
84+
85+
/**
86+
* @return array{
87+
* http?: string,
88+
* https?: string,
89+
* no?: array<int, string>
90+
* }
91+
*/
92+
public function toArray(): array
93+
{
94+
return \array_filter([
95+
'http' => $this->httpProxyURL,
96+
'https' => $this->httpsProxyURL,
97+
'no' => $this->excludedDomains,
98+
]);
99+
}
100+
}

src/Http/MalformedUriException.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* Copyright (c) 2024 Kai Sassnowski
7+
*
8+
* For the full copyright and license information, please view
9+
* the LICENSE file that was distributed with this source code.
10+
*
11+
* @see https://github.com/roach-php/roach
12+
*/
13+
14+
namespace RoachPHP\Http;
15+
16+
use Exception;
17+
18+
final class MalformedUriException extends Exception
19+
{
20+
public static function forUri(string $uri): self
21+
{
22+
return new self("Unable to parse URI [{$uri}]");
23+
}
24+
}

0 commit comments

Comments
 (0)