From 27ac1c40a5e10f87c249ee503f7ed49031c11526 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Thu, 18 Sep 2025 14:50:26 +0200 Subject: [PATCH 1/2] allow to specify a proxy on curl client --- CHANGELOG.md | 6 ++++++ src/Curl.php | 23 +++++++++++++++++++++ src/Curl/Scheduled.php | 45 +++++++++++++++++++++++++++++++++++++++++- tests/CurlTest.php | 34 +++++++++++++++++++++++++++++++ 4 files changed, 107 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9513f2c..f582fcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [Unreleased] + +### Added + +- `Innmind\HttpTransport\Curl::proxy()` + ## 8.0.0 - 2025-05-10 ### Changed diff --git a/src/Curl.php b/src/Curl.php index 1339ef6..a484ee8 100644 --- a/src/Curl.php +++ b/src/Curl.php @@ -11,6 +11,7 @@ Request, Factory\Header\Factory, }; +use Innmind\Url\Url; use Innmind\TimeContinuum\{ Clock, Period, @@ -33,6 +34,7 @@ private function __construct( private Period $timeout, private \Closure $heartbeat, private bool $disableSSLVerification, + private ?Url $proxy, ) { } @@ -44,6 +46,7 @@ public function __invoke(Request $request): Either $this->io, $request, $this->disableSSLVerification, + $this->proxy, ); $this->concurrency->add($scheduled); @@ -67,6 +70,7 @@ public static function of( Period::second(1), static fn() => null, false, + null, ); } @@ -84,6 +88,7 @@ public function maxConcurrency(int $max): self $this->timeout, $this->heartbeat, $this->disableSSLVerification, + $this->proxy, ); } @@ -105,6 +110,7 @@ public function heartbeat(Period $timeout, ?callable $heartbeat = null): self default => \Closure::fromCallable($heartbeat), }, $this->disableSSLVerification, + $this->proxy, ); } @@ -123,6 +129,23 @@ public function disableSSLVerification(): self $this->timeout, $this->heartbeat, true, + $this->proxy, + ); + } + + /** + * @psalm-mutation-free + */ + public function proxy(Url $proxy): self + { + return new self( + $this->headerFactory, + $this->io, + $this->concurrency, + $this->timeout, + $this->heartbeat, + $this->disableSSLVerification, + $proxy, ); } } diff --git a/src/Curl/Scheduled.php b/src/Curl/Scheduled.php index edb0b0c..3a9008e 100644 --- a/src/Curl/Scheduled.php +++ b/src/Curl/Scheduled.php @@ -15,7 +15,11 @@ Headers, Factory\Header\Factory, }; -use Innmind\Url\Authority\UserInformation\User; +use Innmind\Url\{ + Url, + Authority\UserInformation, + Authority\UserInformation\User, +}; use Innmind\IO\{ IO, Files\Temporary, @@ -36,6 +40,7 @@ private function __construct( private IO $io, private Request $request, private bool $disableSSLVerification, + private ?Url $proxy, ) { } @@ -44,12 +49,14 @@ public static function of( IO $io, Request $request, bool $disableSSLVerification, + ?Url $proxy, ): self { return new self( $headerFactory, $io, $request, $disableSSLVerification, + $proxy, ); } @@ -143,6 +150,42 @@ private function options(): Either $options[] = [\CURLOPT_SSL_VERIFYPEER, false]; } + if ($this->proxy) { + $options[] = [ + \CURLOPT_PROXY, + $this + ->proxy + ->withAuthority( + $this->proxy->authority()->withoutUserInformation(), + ) + ->withoutPath() + ->withoutQuery() + ->withoutFragment() + ->toString(), + ]; + + if (!$this->proxy->authority()->userInformation()->equals(UserInformation::none())) { + $options[] = [ + \CURLOPT_PROXYUSERNAME, + $this + ->proxy + ->authority() + ->userInformation() + ->user() + ->toString(), + ]; + $options[] = [ + \CURLOPT_PROXYPASSWORD, + $this + ->proxy + ->authority() + ->userInformation() + ->password() + ->toString(), + ]; + } + } + $header = match ($this->request->method()) { Method::head => [\CURLOPT_NOBODY, true], Method::get => [\CURLOPT_HTTPGET, true], diff --git a/tests/CurlTest.php b/tests/CurlTest.php index 61b8a10..16784f5 100644 --- a/tests/CurlTest.php +++ b/tests/CurlTest.php @@ -542,4 +542,38 @@ public function testTimeout() } // Don't know how to test MalformedResponse, ConnectionFailed, Information and ServerError + + public function testProxy() + { + $request = Request::of( + Url::of('http://httpbun.org/ip'), + Method::get, + ProtocolVersion::v11, + ); + + $result = ($this->curl)($request)->match( + static fn($success) => $success, + static fn() => null, + ); + + $this->assertInstanceOf(Success::class, $result); + $body = $result->response()->body()->toString(); + $ip = \json_decode($body, true)['origin']; + + // Proxy taken from https://github.com/proxifly/free-proxy-list?tab=readme-ov-file + $curl = $this->curl->proxy(Url::of('http://108.161.135.118:80')); + + $result = ($curl)($request)->match( + static fn($success) => $success, + static fn() => null, + ); + + $this->assertInstanceOf(Success::class, $result); + $body = $result->response()->body()->toString(); + $proxiedIp = \json_decode($body, true)['origin']; + $this->assertNotSame( + $ip, + $proxiedIp, + ); + } } From 029f3c682aa2059830eaea2713f4851a8ee81048 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Thu, 18 Sep 2025 14:57:06 +0200 Subject: [PATCH 2/2] remove unreliable proxy test --- tests/CurlTest.php | 35 ++--------------------------------- 1 file changed, 2 insertions(+), 33 deletions(-) diff --git a/tests/CurlTest.php b/tests/CurlTest.php index 16784f5..8f9bc42 100644 --- a/tests/CurlTest.php +++ b/tests/CurlTest.php @@ -543,37 +543,6 @@ public function testTimeout() // Don't know how to test MalformedResponse, ConnectionFailed, Information and ServerError - public function testProxy() - { - $request = Request::of( - Url::of('http://httpbun.org/ip'), - Method::get, - ProtocolVersion::v11, - ); - - $result = ($this->curl)($request)->match( - static fn($success) => $success, - static fn() => null, - ); - - $this->assertInstanceOf(Success::class, $result); - $body = $result->response()->body()->toString(); - $ip = \json_decode($body, true)['origin']; - - // Proxy taken from https://github.com/proxifly/free-proxy-list?tab=readme-ov-file - $curl = $this->curl->proxy(Url::of('http://108.161.135.118:80')); - - $result = ($curl)($request)->match( - static fn($success) => $success, - static fn() => null, - ); - - $this->assertInstanceOf(Success::class, $result); - $body = $result->response()->body()->toString(); - $proxiedIp = \json_decode($body, true)['origin']; - $this->assertNotSame( - $ip, - $proxiedIp, - ); - } + // Proxies are not tested due to unreliable results on free proxies such as + // https://github.com/proxifly/free-proxy-list?tab=readme-ov-file }