Skip to content

Commit 4a962c6

Browse files
authored
Merge pull request #221 from php-http/fix-redirect-host
redirection to different domain must not keep previous port
2 parents 76730e3 + 509e513 commit 4a962c6

19 files changed

+108
-76
lines changed

.github/workflows/static.yml

+15
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ name: static
22

33
on:
44
push:
5+
branches:
6+
- master
57
pull_request:
68

79
jobs:
@@ -19,3 +21,16 @@ jobs:
1921
REQUIRE_DEV: false
2022
with:
2123
args: analyze --no-progress
24+
25+
php-cs-fixer:
26+
name: PHP-CS-Fixer
27+
runs-on: ubuntu-latest
28+
29+
steps:
30+
- name: Checkout code
31+
uses: actions/checkout@v2
32+
33+
- name: PHP-CS-Fixer
34+
uses: docker://oskarstark/php-cs-fixer-ga
35+
with:
36+
args: --dry-run --diff

.github/workflows/tests.yml

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ name: tests
22

33
on:
44
push:
5+
branches:
6+
- master
57
pull_request:
68

79
jobs:

.gitignore

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
/.php_cs
2-
/.php_cs.cache
1+
/.php-cs-fixer.php
2+
/.php-cs-fixer.cache
33
/behat.yml
44
/build/
55
/composer.lock

.php-cs-fixer.dist.php

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
$finder = PhpCsFixer\Finder::create()
4+
->in(__DIR__.'/src')
5+
->in(__DIR__.'/tests')
6+
->name('*.php')
7+
;
8+
9+
$config = new PhpCsFixer\Config();
10+
11+
return $config
12+
->setRiskyAllowed(true)
13+
->setRules([
14+
'@Symfony' => true,
15+
'single_line_throw' => false,
16+
])
17+
->setFinder($finder)
18+
;

CHANGELOG.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
### Fixed
66

7-
- Fixes false positive circular detection in RedirectPlugin in cases when target location does not contain path
7+
- [RedirectPlugin] Fixed handling of redirection to different domain with default port
8+
- [RedirectPlugin] Fixed false positive circular detection in RedirectPlugin in cases when target location does not contain path
89

910
## 2.5.0 - 2021-11-26
1011

spec/Plugin/RedirectPluginSpec.php

-50
Original file line numberDiff line numberDiff line change
@@ -506,54 +506,4 @@ public function it_throws_circular_redirection_exception_on_alternating_redirect
506506
$promise->shouldReturnAnInstanceOf(HttpRejectedPromise::class);
507507
$promise->shouldThrow(CircularRedirectionException::class)->duringWait();
508508
}
509-
510-
public function it_redirects_http_to_https(
511-
UriInterface $uri,
512-
UriInterface $uriRedirect,
513-
RequestInterface $request,
514-
ResponseInterface $responseRedirect,
515-
RequestInterface $modifiedRequest,
516-
ResponseInterface $finalResponse,
517-
Promise $promise
518-
) {
519-
$responseRedirect->getStatusCode()->willReturn(302);
520-
$responseRedirect->hasHeader('Location')->willReturn(true);
521-
$responseRedirect->getHeaderLine('Location')->willReturn('https://my-site.com/original');
522-
523-
$request->getUri()->willReturn($uri);
524-
$request->withUri($uriRedirect)->willReturn($modifiedRequest);
525-
$uri->__toString()->willReturn('http://my-site.com/original');
526-
$uri->withPath('/original')->willReturn($uri);
527-
$uri->withFragment('')->willReturn($uri);
528-
$uri->withQuery('')->willReturn($uri);
529-
530-
$uri->withScheme('https')->willReturn($uriRedirect);
531-
$uriRedirect->withHost('my-site.com')->willReturn($uriRedirect);
532-
$uriRedirect->withPath('/original')->willReturn($uriRedirect);
533-
$uriRedirect->withFragment('')->willReturn($uriRedirect);
534-
$uriRedirect->withQuery('')->willReturn($uriRedirect);
535-
$uriRedirect->__toString()->willReturn('https://my-site.com/original');
536-
537-
$modifiedRequest->getUri()->willReturn($uriRedirect);
538-
$modifiedRequest->getMethod()->willReturn('GET');
539-
540-
$next = function (RequestInterface $receivedRequest) use ($request, $responseRedirect) {
541-
if (Argument::is($request->getWrappedObject())->scoreArgument($receivedRequest)) {
542-
return new HttpFulfilledPromise($responseRedirect->getWrappedObject());
543-
}
544-
};
545-
546-
$first = function (RequestInterface $receivedRequest) use ($modifiedRequest, $promise) {
547-
if (Argument::is($modifiedRequest->getWrappedObject())->scoreArgument($receivedRequest)) {
548-
return $promise->getWrappedObject();
549-
}
550-
};
551-
552-
$promise->getState()->willReturn(Promise::FULFILLED);
553-
$promise->wait()->shouldBeCalled()->willReturn($finalResponse);
554-
555-
$finalPromise = $this->handleRequest($request, $next, $first);
556-
$finalPromise->shouldReturnAnInstanceOf(HttpFulfilledPromise::class);
557-
$finalPromise->wait()->shouldReturn($finalResponse);
558-
}
559509
}

src/Deferred.php

+4-1
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,10 @@ public function wait($unwrap = true)
146146
return $this->value;
147147
}
148148

149-
/** @var ClientExceptionInterface */
149+
if (null === $this->failure) {
150+
throw new \RuntimeException('Internal Error: Promise is not fulfilled but has no exception stored');
151+
}
152+
150153
throw $this->failure;
151154
}
152155
}

src/HttpClientPool/HttpClientPool.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,9 @@ public function addHttpClient($client): void
4646
/**
4747
* Return an http client given a specific strategy.
4848
*
49-
* @throws HttpClientNotFoundException When no http client has been found into the pool
50-
*
5149
* @return HttpClientPoolItem Return a http client that can do both sync or async
50+
*
51+
* @throws HttpClientNotFoundException When no http client has been found into the pool
5252
*/
5353
abstract protected function chooseHttpClient(): HttpClientPoolItem;
5454

src/Plugin/AddHostPlugin.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ final class AddHostPlugin implements Plugin
3131
* @param array{'replace'?: bool} $config
3232
*
3333
* Configuration options:
34-
* - replace: True will replace all hosts, false will only add host when none is specified.
34+
* - replace: True will replace all hosts, false will only add host when none is specified
3535
*/
3636
public function __construct(UriInterface $host, array $config = [])
3737
{

src/Plugin/AddPathPlugin.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ public function handleRequest(RequestInterface $request, callable $next, callabl
7070
if (substr($path, 0, strlen($prepend)) !== $prepend) {
7171
$request = $request->withUri($request->getUri()
7272
->withPath($prepend.$path)
73-
);
73+
);
7474
}
7575

7676
return $next($request);

src/Plugin/ContentTypePlugin.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ final class ContentTypePlugin implements Plugin
3939
*
4040
* Configuration options:
4141
* - skip_detection: true skip detection if stream size is bigger than $size_limit
42-
* - size_limit: size stream limit for which the detection as to be skipped.
42+
* - size_limit: size stream limit for which the detection as to be skipped
4343
*/
4444
public function __construct(array $config = [])
4545
{

src/Plugin/DecoderPlugin.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ final class DecoderPlugin implements Plugin
3434
* @param array{'use_content_encoding'?: bool} $config
3535
*
3636
* Configuration options:
37-
* - use_content_encoding: Whether this plugin should look at the Content-Encoding header first or only at the Transfer-Encoding (defaults to true).
37+
* - use_content_encoding: Whether this plugin should look at the Content-Encoding header first or only at the Transfer-Encoding (defaults to true)
3838
*/
3939
public function __construct(array $config = [])
4040
{

src/Plugin/ErrorPlugin.php

+3-3
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ final class ErrorPlugin implements Plugin
4040
* @param array{'only_server_exception'?: bool} $config
4141
*
4242
* Configuration options:
43-
* - only_server_exception: Whether this plugin should only throw 5XX Exceptions (default to false).
43+
* - only_server_exception: Whether this plugin should only throw 5XX Exceptions (default to false)
4444
*/
4545
public function __construct(array $config = [])
4646
{
@@ -72,10 +72,10 @@ public function handleRequest(RequestInterface $request, callable $next, callabl
7272
* @param RequestInterface $request Request of the call
7373
* @param ResponseInterface $response Response of the call
7474
*
75+
* @return ResponseInterface If status code is not in 4xx or 5xx return response
76+
*
7577
* @throws ClientErrorException If response status code is a 4xx
7678
* @throws ServerErrorException If response status code is a 5xx
77-
*
78-
* @return ResponseInterface If status code is not in 4xx or 5xx return response
7979
*/
8080
private function transformResponseToException(RequestInterface $request, ResponseInterface $response): ResponseInterface
8181
{

src/Plugin/RedirectPlugin.php

+32-4
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ final class RedirectPlugin implements Plugin
107107
* Configuration options:
108108
* - preserve_header: True keeps all headers, false remove all of them, an array is interpreted as a list of header names to keep
109109
* - use_default_for_multiple: Whether the location header must be directly used for a multiple redirection status code (300)
110-
* - strict: When true, redirect codes 300, 301, 302 will not modify request method and body.
110+
* - strict: When true, redirect codes 300, 301, 302 will not modify request method and body
111111
*/
112112
public function __construct(array $config = [])
113113
{
@@ -226,13 +226,39 @@ private function createUri(ResponseInterface $redirectResponse, RequestInterface
226226
$location = $redirectResponse->getHeaderLine('Location');
227227
$parsedLocation = parse_url($location);
228228

229-
if (false === $parsedLocation) {
230-
throw new HttpException(sprintf('Location %s could not be parsed', $location), $originalRequest, $redirectResponse);
229+
if (false === $parsedLocation || '' === $location) {
230+
throw new HttpException(sprintf('Location "%s" could not be parsed', $location), $originalRequest, $redirectResponse);
231231
}
232232

233233
$uri = $originalRequest->getUri();
234+
235+
// Redirections can either use an absolute uri or a relative reference https://www.rfc-editor.org/rfc/rfc3986#section-4.2
236+
// If relative, we need to check if we have an absolute path or not
237+
238+
$path = array_key_exists('path', $parsedLocation) ? $parsedLocation['path'] : '';
239+
if (!array_key_exists('host', $parsedLocation) && '/' !== $location[0]) {
240+
// the target is a relative-path reference, we need to merge it with the base path
241+
$originalPath = $uri->getPath();
242+
if ('' === $path) {
243+
$path = $originalPath;
244+
} elseif (($pos = strrpos($originalPath, '/')) !== false) {
245+
$path = substr($originalPath, 0, $pos + 1).$path;
246+
} else {
247+
$path = '/'.$path;
248+
}
249+
/* replace '/./' or '/foo/../' with '/' */
250+
$re = ['#(/\./)#', '#/(?!\.\.)[^/]+/\.\./#'];
251+
for ($n = 1; $n > 0; $path = preg_replace($re, '/', $path, -1, $n)) {
252+
if (null === $path) {
253+
throw new HttpException(sprintf('Failed to resolve Location %s', $location), $originalRequest, $redirectResponse);
254+
}
255+
}
256+
}
257+
if (null === $path) {
258+
throw new HttpException(sprintf('Failed to resolve Location %s', $location), $originalRequest, $redirectResponse);
259+
}
234260
$uri = $uri
235-
->withPath(array_key_exists('path', $parsedLocation) ? $parsedLocation['path'] : '')
261+
->withPath($path)
236262
->withQuery(array_key_exists('query', $parsedLocation) ? $parsedLocation['query'] : '')
237263
->withFragment(array_key_exists('fragment', $parsedLocation) ? $parsedLocation['fragment'] : '')
238264
;
@@ -247,6 +273,8 @@ private function createUri(ResponseInterface $redirectResponse, RequestInterface
247273

248274
if (array_key_exists('port', $parsedLocation)) {
249275
$uri = $uri->withPort($parsedLocation['port']);
276+
} elseif (array_key_exists('host', $parsedLocation)) {
277+
$uri = $uri->withPort(null);
250278
}
251279

252280
return $uri;

src/PluginChain.php

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Http\Client\Common;
66

77
use function array_reverse;
8+
89
use Http\Client\Common\Exception\LoopException;
910
use Http\Promise\Promise;
1011
use Psr\Http\Message\RequestInterface;

src/PluginClient.php

-1
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,6 @@ private function configure(array $options = []): array
124124
*/
125125
private function createPluginChain(array $plugins, callable $clientCallable): callable
126126
{
127-
/** @var callable(RequestInterface): Promise */
128127
return new PluginChain($plugins, $clientCallable, $this->options);
129128
}
130129
}

src/PluginClientFactory.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,11 @@ public static function setFactory(callable $factory): void
3939
/**
4040
* @param ClientInterface|HttpAsyncClient $client
4141
* @param Plugin[] $plugins
42-
* @param array{'client_name'?: string} $options
42+
* @param array{'client_name'?: string} $options
4343
*
4444
* Configuration options:
4545
* - client_name: to give client a name which may be used when displaying client information
46-
* like in the HTTPlugBundle profiler.
46+
* like in the HTTPlugBundle profiler
4747
*
4848
* @see PluginClient constructor for PluginClient specific $options.
4949
*/

tests/Plugin/RedirectPluginTest.php

+20-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<?php
2+
23
declare(strict_types=1);
34

45
namespace Plugin;
@@ -26,10 +27,26 @@ function () {}
2627
)->wait();
2728
}
2829

30+
public function provideRedirections(): array
31+
{
32+
return [
33+
'no path on target' => ['https://example.com/path?query=value', 'https://example.com?query=value', 'https://example.com?query=value'],
34+
'root path on target' => ['https://example.com/path?query=value', 'https://example.com/?query=value', 'https://example.com/?query=value'],
35+
'redirect to query' => ['https://example.com', 'https://example.com?query=value', 'https://example.com?query=value'],
36+
'redirect to different domain without port' => ['https://example.com:8000', 'https://foo.com?query=value', 'https://foo.com?query=value'],
37+
'network-path redirect, preserve scheme' => ['https://example.com:8000', '//foo.com/path?query=value', 'https://foo.com/path?query=value'],
38+
'absolute-path redirect, preserve host' => ['https://example.com:8000', '/path?query=value', 'https://example.com:8000/path?query=value'],
39+
'relative-path redirect, append' => ['https://example.com:8000/path/', 'sub/path?query=value', 'https://example.com:8000/path/sub/path?query=value'],
40+
'relative-path on non-folder' => ['https://example.com:8000/path/foo', 'sub/path?query=value', 'https://example.com:8000/path/sub/path?query=value'],
41+
'relative-path moving up' => ['https://example.com:8000/path/', '../other?query=value', 'https://example.com:8000/other?query=value'],
42+
'relative-path with ./' => ['https://example.com:8000/path/', './other?query=value', 'https://example.com:8000/path/other?query=value'],
43+
'relative-path with //' => ['https://example.com:8000/path/', 'other//sub?query=value', 'https://example.com:8000/path/other//sub?query=value'],
44+
'relative-path redirect with only query' => ['https://example.com:8000/path', '?query=value', 'https://example.com:8000/path?query=value'],
45+
];
46+
}
47+
2948
/**
30-
* @testWith ["https://example.com/path?query=value", "https://example.com?query=value", "https://example.com?query=value"]
31-
* ["https://example.com/path?query=value", "https://example.com/?query=value", "https://example.com/?query=value"]
32-
* ["https://example.com", "https://example.com?query=value", "https://example.com?query=value"]
49+
* @dataProvider provideRedirections
3350
*/
3451
public function testTargetUriMappingFromLocationHeader(string $originalUri, string $locationUri, string $targetUri): void
3552
{

tests/PluginChainTest.php

+1-3
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,13 @@
99
use Http\Client\Common\PluginChain;
1010
use Http\Promise\Promise;
1111
use PHPUnit\Framework\TestCase;
12-
use Prophecy\Argument;
1312
use Psr\Http\Message\RequestInterface;
1413

1514
class PluginChainTest extends TestCase
1615
{
1716
private function createPlugin(callable $func): Plugin
1817
{
19-
return new class ($func) implements Plugin
20-
{
18+
return new class($func) implements Plugin {
2119
public $func;
2220

2321
public function __construct(callable $func)

0 commit comments

Comments
 (0)