diff --git a/composer.json b/composer.json index 5876bee..3a0be46 100644 --- a/composer.json +++ b/composer.json @@ -4,11 +4,12 @@ "require": { "php": ">=7.3", "guzzlehttp/guzzle": "^7.2", - "psr/log": "^1.1" + "psr/log": "^1.1", + "symfony/process": "^5.2" }, "require-dev": { "phpunit/phpunit": "^9.4.3", - "pestphp/pest": "^0.3.14", + "pestphp/pest": "^1.0", "phpstan/phpstan": "^0.12.58" }, "autoload": { diff --git a/src/Formatter/CurlFormatter.php b/src/Formatter/CurlFormatter.php index 12b2639..4339aac 100644 --- a/src/Formatter/CurlFormatter.php +++ b/src/Formatter/CurlFormatter.php @@ -5,6 +5,7 @@ use GuzzleHttp\Cookie\CookieJarInterface; use GuzzleHttp\Cookie\SetCookie; use Psr\Http\Message\RequestInterface; +use Symfony\Component\Process\Process; class CurlFormatter { @@ -140,7 +141,7 @@ protected function extractBodyArgument(RequestInterface $request) if ($contents) { // clean input of null bytes $contents = str_replace(chr(0), '', $contents); - $this->addOption('d', escapeshellarg($contents)); + $this->addOption('d', $this->escapeShellArgument($contents)); } //if get request has data Add G otherwise curl will make a post request @@ -176,7 +177,7 @@ protected function extractCookiesArgument(RequestInterface $request, array $opti } if ($values) { - $this->addOption('b', escapeshellarg(implode('; ', $values))); + $this->addOption('b', $this->escapeShellArgument(implode('; ', $values))); } } @@ -193,12 +194,12 @@ protected function extractHeadersArgument(RequestInterface $request) } if ('user-agent' === strtolower($name)) { - $this->addOption('A', escapeshellarg($header[0])); + $this->addOption('A', $this->escapeShellArgument($header[0])); continue; } foreach ((array)$header as $headerValue) { - $this->addOption('H', escapeshellarg("{$name}: {$headerValue}")); + $this->addOption('H', $this->escapeShellArgument("{$name}: {$headerValue}")); } } } @@ -245,6 +246,13 @@ protected function extractArguments(RequestInterface $request, array $options) */ protected function extractUrlArgument(RequestInterface $request) { - $this->addCommandPart(escapeshellarg((string)$request->getUri()->withFragment(''))); + $this->addCommandPart($this->escapeShellArgument((string)$request->getUri()->withFragment(''))); + } + + protected function escapeShellArgument($argument) + { + $process = new Process([$argument]); + $escaped = $process->getCommandLine(); + return $escaped; } } diff --git a/tests/Client/RequestTest.php b/tests/Client/RequestTest.php index 32430a2..1d6accd 100644 --- a/tests/Client/RequestTest.php +++ b/tests/Client/RequestTest.php @@ -18,22 +18,22 @@ $jar = CookieJar::fromArray(['Foo' => 'Bar', 'identity' => 'xyz'], 'local.example'); $curl = $this->curlFormatter->format($request, ['cookies' => $jar]); - expect($curl)->not()->toContain("-H 'Host: local.example'"); - expect($curl)->toContain("-b 'Foo=Bar; identity=xyz'"); + expect(str_replace('"', '\'', $curl))->not()->toContain("-H 'Host: local.example'"); + expect(str_replace('"', '\'', $curl))->toContain("-b 'Foo=Bar; identity=xyz'"); }); test('POST', function () { $request = new Request('POST', 'http://local.example', [], Utils::streamFor('foo=bar&hello=world')); $curl = $this->curlFormatter->format($request); - expect($curl)->toContain("-d 'foo=bar&hello=world'"); + expect(str_replace('"', '\'', $curl))->toContain("-d 'foo=bar&hello=world'"); }); test('PUT', function () { $request = new Request('PUT', 'http://local.example', [], Utils::streamFor('foo=bar&hello=world')); $curl = $this->curlFormatter->format($request); - expect($curl)->toContain("-d 'foo=bar&hello=world'"); + expect(str_replace('"', '\'', $curl))->toContain("-d 'foo=bar&hello=world'"); expect($curl)->toContain('-X PUT'); }); @@ -48,7 +48,7 @@ $request = new Request('HEAD', 'http://local.example'); $curl = $this->curlFormatter->format($request); - expect($curl)->toContain("curl 'http://local.example' --head"); + expect(str_replace('"', '\'', $curl))->toContain("curl 'http://local.example' --head"); }); test('OPTIONS', function () { @@ -56,4 +56,4 @@ $curl = $this->curlFormatter->format($request); expect($curl)->toContain('-X OPTIONS'); -}); \ No newline at end of file +}); diff --git a/tests/Formatter/CurlFormatterTest.php b/tests/Formatter/CurlFormatterTest.php index 5bc46e4..ee5dfd4 100644 --- a/tests/Formatter/CurlFormatterTest.php +++ b/tests/Formatter/CurlFormatterTest.php @@ -21,47 +21,47 @@ $request = new Request('GET', 'http://example.local'); $curl = $this->curlFormatter->format($request); - $this->assertEquals("curl 'http://example.local'", $curl); + $this->assertEquals("curl 'http://example.local'", str_replace('"', '\'', $curl)); }); test('simple get', function () { $request = new Request('GET', 'http://example.local'); $curl = $this->curlFormatter->format($request); - $this->assertEquals("curl 'http://example.local'", $curl); + $this->assertEquals("curl 'http://example.local'", str_replace('"', '\'', $curl)); }); test('simple GET with header', function () { $request = new Request('GET', 'http://example.local', ['foo' => 'bar']); $curl = $this->curlFormatter->format($request); - $this->assertEquals("curl 'http://example.local' -H 'foo: bar'", $curl); + $this->assertEquals("curl 'http://example.local' -H 'foo: bar'", str_replace('"', '\'', $curl)); }); test('simple GET with multiple header', function () { $request = new Request('GET', 'http://example.local', ['foo' => 'bar', 'Accept-Encoding' => 'gzip,deflate,sdch']); $curl = $this->curlFormatter->format($request); - $this->assertEquals("curl 'http://example.local' -H 'foo: bar' -H 'Accept-Encoding: gzip,deflate,sdch'", $curl); + $this->assertEquals("curl 'http://example.local' -H 'foo: bar' -H 'Accept-Encoding: gzip,deflate,sdch'", str_replace('"', '\'', $curl)); }); test('GET With Query String', function () { $request = new Request('GET', 'http://example.local?foo=bar'); $curl = $this->curlFormatter->format($request); - $this->assertEquals("curl 'http://example.local?foo=bar'", $curl); + $this->assertEquals("curl 'http://example.local?foo=bar'", str_replace('"', '\'', $curl)); $request = new Request('GET', 'http://example.local?foo=bar'); $curl = $this->curlFormatter->format($request); - $this->assertEquals("curl 'http://example.local?foo=bar'", $curl); + $this->assertEquals("curl 'http://example.local?foo=bar'", str_replace('"', '\'', $curl)); $body = Utils::streamFor(http_build_query(['foo' => 'bar', 'hello' => 'world'], '', '&')); $request = new Request('GET', 'http://example.local',[],$body); $curl = $this->curlFormatter->format($request); - $this->assertEquals("curl 'http://example.local' -G -d 'foo=bar&hello=world'",$curl); + $this->assertEquals("curl 'http://example.local' -G -d 'foo=bar&hello=world'", str_replace('"', '\'', $curl)); }); test('POST', function () { @@ -71,7 +71,19 @@ $curl = $this->curlFormatter->format($request); expect($curl)->not()->toContain(" -G "); - expect($curl)->toContain("-d 'foo=bar&hello=world'"); + expect(str_replace('"', '\'', $curl))->toContain("-d 'foo=bar&hello=world'"); + +}); + +test('large POST request', function () { + ini_set('memory_limit', -1); + + $body = str_repeat('A', 1024*1024*64); + + $request = new Request('POST', 'http://example.local', [], \GuzzleHttp\Psr7\stream_for($body)); + $curl = $this->curlFormatter->format($request); + + expect($curl)->not()->toBeNull(); }); test('HEAD', function () { @@ -99,7 +111,7 @@ $request = new Request('PUT', 'http://example.local', [], Utils::streamFor('foo=bar&hello=world')); $curl = $this->curlFormatter->format($request); - expect($curl)->toContain("-d 'foo=bar&hello=world'"); + expect(str_replace('"', '\'', $curl))->toContain("-d 'foo=bar&hello=world'"); expect($curl)->toContain("-X PUT"); }); @@ -107,7 +119,7 @@ $request = new Request('PUT', 'http://example.local', [], Utils::streamFor('foo=bar&hello=world')); $curl = $this->curlFormatter->format($request); - expect($curl)->toContain("-d 'foo=bar&hello=world'"); + expect(str_replace('"', '\'', $curl))->toContain("-d 'foo=bar&hello=world'"); expect($curl)->toContain("-X PUT"); }); @@ -124,4 +136,4 @@ ['X-Foo' => 'Bar'], chr(0). 'foo=bar&hello=world', ] -]); \ No newline at end of file +]);