From 676e3fb2176b8746521b194d998c33301efad712 Mon Sep 17 00:00:00 2001 From: flashwave Date: Wed, 26 Feb 2025 14:57:19 +0000 Subject: [PATCH] Switched Xrpc client to Guzzle. --- composer.json | 4 +- composer.lock | 288 +++++++++++++++++++++++++++++++++++-- src/ATProto/XrpcClient.php | 139 ++++++------------ 3 files changed, 316 insertions(+), 115 deletions(-) diff --git a/composer.json b/composer.json index 084e13d4..264cd34e 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,6 @@ { "require": { "php": ">=8.4", - "ext-curl": "*", "ext-mbstring": "*", "flashwave/index": "^0.2501", "flashii/rpcii": "~4.0", @@ -13,7 +12,8 @@ "nesbot/carbon": "~3.8", "vlucas/phpdotenv": "~5.6", "filp/whoops": "~2.17", - "phpseclib/phpseclib": "~3.0" + "phpseclib/phpseclib": "~3.0", + "guzzlehttp/guzzle": "~7.0" }, "autoload": { "classmap": [ diff --git a/composer.lock b/composer.lock index 8d96b2ee..56c42778 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "42e27bedc2ac1251eca61bf7df4f0342", + "content-hash": "1045c8f605203fae19361d659fb64c54", "packages": [ { "name": "carbonphp/carbon-doctrine-types", @@ -655,6 +655,215 @@ ], "time": "2024-07-20T21:45:45+00:00" }, + { + "name": "guzzlehttp/guzzle", + "version": "7.9.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "d281ed313b989f213357e3be1a179f02196ac99b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/d281ed313b989f213357e3be1a179f02196ac99b", + "reference": "d281ed313b989f213357e3be1a179f02196ac99b", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.5.3 || ^2.0.3", + "guzzlehttp/psr7": "^2.7.0", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "3.0.2", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.9.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2024-07-24T11:22:20+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/f9c436286ab2892c7db7be8c8da4ef61ccf7b455", + "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2024-10-17T10:06:22+00:00" + }, { "name": "guzzlehttp/psr7", "version": "2.7.0", @@ -1512,6 +1721,58 @@ }, "time": "2019-01-08T18:20:26+00:00" }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, { "name": "psr/http-factory", "version": "1.1.0", @@ -2237,16 +2498,16 @@ }, { "name": "symfony/mime", - "version": "v7.2.3", + "version": "v7.2.4", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "2fc3b4bd67e4747e45195bc4c98bea4628476204" + "reference": "87ca22046b78c3feaff04b337f33b38510fd686b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/2fc3b4bd67e4747e45195bc4c98bea4628476204", - "reference": "2fc3b4bd67e4747e45195bc4c98bea4628476204", + "url": "https://api.github.com/repos/symfony/mime/zipball/87ca22046b78c3feaff04b337f33b38510fd686b", + "reference": "87ca22046b78c3feaff04b337f33b38510fd686b", "shasum": "" }, "require": { @@ -2301,7 +2562,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.2.3" + "source": "https://github.com/symfony/mime/tree/v7.2.4" }, "funding": [ { @@ -2317,7 +2578,7 @@ "type": "tidelift" } ], - "time": "2025-01-27T11:08:17+00:00" + "time": "2025-02-19T08:51:20+00:00" }, { "name": "symfony/options-resolver", @@ -2950,16 +3211,16 @@ }, { "name": "symfony/translation", - "version": "v7.2.2", + "version": "v7.2.4", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "e2674a30132b7cc4d74540d6c2573aa363f05923" + "reference": "283856e6981286cc0d800b53bd5703e8e363f05a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/e2674a30132b7cc4d74540d6c2573aa363f05923", - "reference": "e2674a30132b7cc4d74540d6c2573aa363f05923", + "url": "https://api.github.com/repos/symfony/translation/zipball/283856e6981286cc0d800b53bd5703e8e363f05a", + "reference": "283856e6981286cc0d800b53bd5703e8e363f05a", "shasum": "" }, "require": { @@ -3025,7 +3286,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v7.2.2" + "source": "https://github.com/symfony/translation/tree/v7.2.4" }, "funding": [ { @@ -3041,7 +3302,7 @@ "type": "tidelift" } ], - "time": "2024-12-07T08:18:10+00:00" + "time": "2025-02-13T10:27:23+00:00" }, { "name": "symfony/translation-contracts", @@ -3420,7 +3681,6 @@ "prefer-lowest": false, "platform": { "php": ">=8.4", - "ext-curl": "*", "ext-mbstring": "*" }, "platform-dev": {}, diff --git a/src/ATProto/XrpcClient.php b/src/ATProto/XrpcClient.php index 594b8272..385a2f7b 100644 --- a/src/ATProto/XrpcClient.php +++ b/src/ATProto/XrpcClient.php @@ -1,36 +1,28 @@ handle = $handle; - $this->userAgent = sprintf('Misuzu/%s cURL/%s', Misuzu::version(), $version['version']); - } - - public function __destruct() { - curl_close($this->handle); + $this->httpClient = new HttpClient([ + 'allow_redirects' => true, + 'base_uri' => sprintf('%s/xrpc/', rtrim($service, '/')), + 'timeout' => 10, + 'headers' => [ + 'User-Agent' => sprintf('Misuzu/%s', Misuzu::version()), + ], + ]); } /** @@ -46,98 +38,47 @@ class XrpcClient { array $params, array|object|string|null $data ): object { - $url = sprintf('%s/xrpc/%s', $this->service, $nsid); - if(!empty($params)) - $url = sprintf('%s?%s', $url, http_build_query($params, encoding_type: PHP_QUERY_RFC3986)); + $request = new HttpRequest($method, $nsid, $headers); + $options = [ + 'query' => $params, + ]; - $headers = (function($original) { - $sanitised = []; - foreach($original as $name => $value) { - $name = strtolower($name); - if(!array_key_exists($name, $sanitised)) - $sanitised[$name] = $value; - } + if(is_object($data) || is_array($data)) + $options['json'] = $data; + elseif(is_string($data)) + $options['body'] = $data; - return $sanitised; - })($headers); - - if(is_object($data) || is_array($data)) { - $headers['content-type'] = 'application/json'; - $data = json_encode($data); + try { + $response = $this->httpClient->send($request, $options); + $statusCode = $response->getStatusCode(); + } catch(RequestException $ex) { + $response = $ex->getResponse(); + $statusCode = $ex->getCode(); } - curl_reset($this->handle); - curl_setopt_array($this->handle, [ - CURLOPT_AUTOREFERER => true, - CURLOPT_CUSTOMREQUEST => $method, - CURLOPT_FAILONERROR => false, - CURLOPT_FOLLOWLOCATION => true, - CURLOPT_HEADER => true, - CURLOPT_HTTPHEADER => (function($headers) { - $lines = []; - foreach($headers as $name => $value) - $lines[] = sprintf('%s: %s', $name, $value); - return $lines; - })($headers), - CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_TCP_FASTOPEN => true, - CURLOPT_CONNECTTIMEOUT => 5, - CURLOPT_MAXREDIRS => 5, - CURLOPT_POSTFIELDS => $data, - CURLOPT_PROTOCOLS => CURLPROTO_HTTPS, - CURLOPT_TIMEOUT => 10, - CURLOPT_URL => $url, - CURLOPT_USERAGENT => $this->userAgent, - ]); - - $response = curl_exec($this->handle); - if($response === false) - throw new RuntimeException(curl_error($this->handle), curl_errno($this->handle)); - if($response === true) - throw new UnexpectedValueException('Request executed successfully but cURL returned no data.'); - - $parts = explode("\r\n\r\n", $response, 2); - - $headerLines = array_reverse(explode("\r\n", $parts[0])); - $statusLine = array_pop($headerLines); - if(!is_string($statusLine)) - throw new RuntimeException('Unable to read status header.'); - - $statusLineScan = sscanf($statusLine, 'HTTP/%s %d %[^\t\r\n]'); - if($statusLineScan === null) - throw new RuntimeException('Failed to decode HTTP status line.'); - [$_, $statusCode, $statusLine] = $statusLineScan; - - $statusCode = (int)$statusCode; - - $headers = []; - while(($headerLine = array_pop($headerLines)) !== null) { - $headerLine = trim((string)$headerLine); - if($headerLine === '') - continue; - - $headerParts = explode(':', $headerLine, 2); - $headers[strtolower(trim($headerParts[0]))] = count($headerParts) > 1 ? trim($headerParts[1]) : ''; - } + $headers = (function(array $raw) { + $headers = []; + foreach($raw as $name => $value) + $headers[strtolower($name)] = implode(', ', $value); + return $headers; + })($response->getHeaders()); + $body = (string)$response->getBody(); if(array_key_exists('content-type', $headers) && str_starts_with($headers['content-type'], 'application/json')) - $data = json_decode($parts[1]); - else - $data = $parts[1]; + $body = json_decode($body); if($statusCode !== 200) { - if(is_object($data) - && isset($data->error) && is_string($data->error) - && isset($data->message) && is_string($data->message)) - throw new RuntimeException(sprintf('%s: %s', $data->error, $data->message), $statusCode); + if(is_object($body) + && isset($body->error) && is_string($body->error) + && isset($body->message) && is_string($body->message)) + throw new RuntimeException(sprintf('%s: %s', $body->error, $body->message), $statusCode); throw new RuntimeException(sprintf('HTTP %03d', $statusCode), $statusCode); } return (object)[ 'headers' => $headers, - 'data' => $data, + 'data' => $body, ]; }