-
Notifications
You must be signed in to change notification settings - Fork 501
Add Host Bindings feature #1015
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
48c2bf9
cfd0fc4
95a96a4
2ae16de
5e11426
cadd60f
0fcfc26
dbc773a
8b5318a
fc6e047
5760370
314b231
85070f2
8d5d9d8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,33 @@ | ||||||
| <?php | ||||||
| /** | ||||||
| * Requests for PHP, an HTTP library. | ||||||
| * | ||||||
| * @copyright 2012-2023 Requests Contributors | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
(here and elsewhere - or should we do this codebase wide after this PR has been merged ?) |
||||||
| * @license https://github.com/WordPress/Requests/blob/stable/LICENSE ISC | ||||||
| * @link https://github.com/WordPress/Requests | ||||||
| */ | ||||||
|
|
||||||
| namespace WpOrg\Requests\Exception; | ||||||
|
|
||||||
| use RangeException; | ||||||
|
|
||||||
| /** | ||||||
| * Exception for a missing IP address in the host bindings. | ||||||
| * | ||||||
| * @package Requests\Exceptions | ||||||
| * @since 2.x.x | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| */ | ||||||
| final class MissingIpAddress extends RangeException { | ||||||
|
|
||||||
| /** | ||||||
| * Instantiate a MissingIpAddress exception for a missing IP address in the host bindings. | ||||||
| * | ||||||
| * @param string $host Host that was requested. | ||||||
| * @return self | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| */ | ||||||
| public static function for_host($host) { | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Reason for this suggestion: in other Exceptions introduced over the past few years, we've used the pattern of using |
||||||
| $message = "No IP address was found for host: {$host}"; | ||||||
|
|
||||||
| return new self($message); | ||||||
| } | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,34 @@ | ||||||
| <?php | ||||||
| /** | ||||||
| * Requests for PHP, an HTTP library. | ||||||
| * | ||||||
| * @copyright 2012-2023 Requests Contributors | ||||||
| * @license https://github.com/WordPress/Requests/blob/stable/LICENSE ISC | ||||||
| * @link https://github.com/WordPress/Requests | ||||||
| */ | ||||||
|
|
||||||
| namespace WpOrg\Requests\Exception; | ||||||
|
|
||||||
| use InvalidArgumentException; | ||||||
|
|
||||||
| /** | ||||||
| * Exception for an unknown host being requested via HostBindings. | ||||||
| * | ||||||
| * @package Requests\Exceptions | ||||||
| * @since 2.x.x | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| */ | ||||||
| final class UnknownHost extends InvalidArgumentException { | ||||||
|
|
||||||
| /** | ||||||
| * Instantiate an UnknownHost exception for an unknown host that was | ||||||
| * requested via HostBindings. | ||||||
| * | ||||||
| * @param string $host Unknown host that was requested. | ||||||
| * @return self | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| */ | ||||||
| public static function for_host($host) { | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Reason for this suggestion: in other Exceptions introduced over the past few years, we've used the pattern of using |
||||||
| $message = "Unknown host was requested from the host bindings collection: {$host}"; | ||||||
|
|
||||||
| return new self($message); | ||||||
| } | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -21,6 +21,7 @@ | |||||||||||||||||||||||||
| use WpOrg\Requests\Response; | ||||||||||||||||||||||||||
| use WpOrg\Requests\Transport\Curl; | ||||||||||||||||||||||||||
| use WpOrg\Requests\Transport\Fsockopen; | ||||||||||||||||||||||||||
| use WpOrg\Requests\Utility\HostBindings; | ||||||||||||||||||||||||||
| use WpOrg\Requests\Utility\InputValidator; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||
|
|
@@ -417,6 +418,12 @@ public static function patch($url, $headers, $data = [], $options = []) { | |||||||||||||||||||||||||
| * - `data_format`: How should we send the `$data` parameter? | ||||||||||||||||||||||||||
| * (string, one of 'query' or 'body', default: 'query' for | ||||||||||||||||||||||||||
| * HEAD/GET/DELETE, 'body' for POST/PUT/OPTIONS/PATCH) | ||||||||||||||||||||||||||
| * - `host_bindings`: Bind host names to specific IP addresses. | ||||||||||||||||||||||||||
| * Accepts either: | ||||||||||||||||||||||||||
| * - Array: Keys are domain names, values are arrays of IPv4/IPv6 addresses. | ||||||||||||||||||||||||||
| * IP addresses are validated by default. | ||||||||||||||||||||||||||
| * - HostBindings object: Pre-constructed for advanced control (e.g., skip validation). | ||||||||||||||||||||||||||
| * (array|HostBindings, default: []) | ||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||
| * @param string|\Stringable $url URL to request | ||||||||||||||||||||||||||
| * @param array $headers Extra headers to send with the request | ||||||||||||||||||||||||||
|
|
@@ -460,9 +467,23 @@ public static function request($url, $headers = [], $data = [], $type = self::GE | |||||||||||||||||||||||||
| $transport = new $transport(); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||
| $need_ssl = (stripos($url, 'https://') === 0); | ||||||||||||||||||||||||||
| $capabilities = [Capability::SSL => $need_ssl]; | ||||||||||||||||||||||||||
| $transport = self::get_transport($capabilities); | ||||||||||||||||||||||||||
| $need_ssl = (stripos($url, 'https://') === 0); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| $need_host_bindings = | ||||||||||||||||||||||||||
| array_key_exists(Capability::HOST_BINDINGS, $options) | ||||||||||||||||||||||||||
| && | ||||||||||||||||||||||||||
| ( | ||||||||||||||||||||||||||
| is_array($options[Capability::HOST_BINDINGS]) | ||||||||||||||||||||||||||
| || | ||||||||||||||||||||||||||
| $options[Capability::HOST_BINDINGS] instanceof HostBindings | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
|
Comment on lines
+472
to
+479
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| $capabilities = [ | ||||||||||||||||||||||||||
| Capability::HOST_BINDINGS => $need_host_bindings, | ||||||||||||||||||||||||||
| Capability::SSL => $need_ssl, | ||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| $transport = self::get_transport($capabilities); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| $response = $transport->request($url, $headers, $data, $options); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -17,6 +17,8 @@ | |||||
| use WpOrg\Requests\Exception\Transport\Curl as CurlException; | ||||||
| use WpOrg\Requests\Requests; | ||||||
| use WpOrg\Requests\Transport; | ||||||
| use WpOrg\Requests\Utility\CaseInsensitiveDictionary; | ||||||
| use WpOrg\Requests\Utility\HostBindings; | ||||||
| use WpOrg\Requests\Utility\InputValidator; | ||||||
|
|
||||||
| /** | ||||||
|
|
@@ -385,8 +387,10 @@ public function &get_subrequest_handle($url, $headers, $data, $options) { | |||||
| private function setup_handle($url, $headers, $data, $options) { | ||||||
| $options['hooks']->dispatch('curl.before_request', [&$this->handle]); | ||||||
|
|
||||||
| $case_insensitive_headers = new CaseInsensitiveDictionary($headers); | ||||||
|
|
||||||
| // Force closing the connection for old versions of cURL (<7.22). | ||||||
| if (!isset($headers['Connection'])) { | ||||||
| if (!isset($case_insensitive_headers['Connection'])) { | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
If you are making it case-insensitive.... ? |
||||||
| $headers['Connection'] = 'close'; | ||||||
| } | ||||||
|
|
||||||
|
|
@@ -405,8 +409,6 @@ private function setup_handle($url, $headers, $data, $options) { | |||||
| $headers['Expect'] = $this->get_expect_header($data); | ||||||
| } | ||||||
|
|
||||||
| $headers = Requests::flatten($headers); | ||||||
|
|
||||||
| if (!empty($data)) { | ||||||
| $data_format = $options['data_format']; | ||||||
|
|
||||||
|
|
@@ -418,6 +420,103 @@ private function setup_handle($url, $headers, $data, $options) { | |||||
| } | ||||||
| } | ||||||
|
|
||||||
| $exec_url = $url; | ||||||
| $parsed = parse_url($url); | ||||||
| $host = $parsed['host']; | ||||||
|
|
||||||
| $host_bindings_input = isset($options[Capability::HOST_BINDINGS]) ? $options[Capability::HOST_BINDINGS] : []; | ||||||
|
|
||||||
| // We allow the application to pass in a pre-constructed HostBindings object | ||||||
| // in case they need to skip IP address validation. | ||||||
| if ($host_bindings_input instanceof HostBindings) { | ||||||
| $host_bindings = $host_bindings_input; | ||||||
| } elseif (is_array($host_bindings_input)) { | ||||||
| $host_bindings = new HostBindings($host_bindings_input); | ||||||
| } elseif (empty($host_bindings_input) === false) { | ||||||
| throw InvalidArgument::create( | ||||||
| 4, | ||||||
| 'options[host_bindings]', | ||||||
| 'array or HostBindings object', | ||||||
| gettype($host_bindings_input) | ||||||
| ); | ||||||
| } else { | ||||||
| $host_bindings = new HostBindings([]); | ||||||
| } | ||||||
|
|
||||||
| if ($host_bindings->has_host($host)) { | ||||||
| if (isset($parsed['port'])) { | ||||||
| $port = $parsed['port']; | ||||||
| $normalized_port = $port; | ||||||
| } else { | ||||||
| $port = ''; | ||||||
| $normalized_port = ($parsed['scheme'] === 'http' ? 80 : 443); | ||||||
| } | ||||||
|
|
||||||
| $exec_ip = $host_bindings->get_first_ip_for_host($host); | ||||||
| // Use square brackets for IPv6 addresses. | ||||||
| $exec_ip = strpos($exec_ip, ':') === false ? $exec_ip : "[{$exec_ip}]"; | ||||||
|
|
||||||
| // @TODO: Extract connect_to/resolve handling into separate method. | ||||||
| if (defined('CURLOPT_CONNECT_TO')) { | ||||||
| $connect_to_string = "{$host}:{$normalized_port}:{$exec_ip}:{$normalized_port}"; | ||||||
| // phpcs:ignore PHPCompatibility.Constants.NewConstants.curlopt_connecttoFound | ||||||
| curl_setopt($this->handle, CURLOPT_CONNECT_TO, [$connect_to_string]); // phpcs:ignore PHPCompatibility.Constants.NewConstants.curlopt_connect_toFound | ||||||
| // CURLOPT_CONNECT_TO (cURL 7.49.0 / PHP 7.0.7) is always defined, so this | ||||||
| // older fallback using CURLOPT_RESOLVE is never reached. | ||||||
| // @codeCoverageIgnoreStart | ||||||
| } elseif (defined('CURLOPT_RESOLVE')) { | ||||||
| if (defined('CURLOPT_DNS_USE_GLOBAL_CACHE')) { | ||||||
| // Set to true in PHP's source for most installations. | ||||||
| // Deprecated as of cURL 7.68.0, which removes our need to set this. | ||||||
| curl_setopt($this->handle, CURLOPT_DNS_USE_GLOBAL_CACHE, false); | ||||||
| } | ||||||
|
|
||||||
| curl_setopt($this->handle, CURLOPT_RESOLVE, ["{$host}:{$normalized_port}:{$exec_ip}"]); | ||||||
| // @codeCoverageIgnoreEnd | ||||||
| // Both CURLOPT_CONNECT_TO and CURLOPT_RESOLVE are always defined, so this | ||||||
| // last-resort HTTP URL rewriting fallback is never reached. | ||||||
| // @TODO: Extract into a utility class to make this testable independently. | ||||||
| // @codeCoverageIgnoreStart | ||||||
| } elseif ($parsed['scheme'] === 'http') { | ||||||
| $exec_url = $parsed['scheme'] . '://'; | ||||||
| if (isset($parsed['user'])) { | ||||||
| $exec_url .= $parsed['user']; | ||||||
| if (isset($parsed['pass'])) { | ||||||
| $exec_url .= ':' . $parsed['pass']; | ||||||
| } | ||||||
|
|
||||||
| $exec_url .= '@'; | ||||||
| } | ||||||
|
|
||||||
| $exec_url .= $exec_ip; | ||||||
| if ($port) { | ||||||
| $exec_url .= ':' . $port; | ||||||
| } | ||||||
|
|
||||||
| if (isset($parsed['path'])) { | ||||||
| $exec_url .= $parsed['path']; | ||||||
| } | ||||||
|
|
||||||
| if (isset($parsed['query'])) { | ||||||
| $exec_url .= '?' . $parsed['query']; | ||||||
| } | ||||||
|
|
||||||
| if (isset($parsed['fragment'])) { | ||||||
| $exec_url .= '#' . $parsed['fragment']; | ||||||
| } | ||||||
|
|
||||||
| if (!isset($case_insensitive_headers['Host'])) { | ||||||
| $headers['Host'] = $host; | ||||||
| } | ||||||
|
|
||||||
| // @codeCoverageIgnoreEnd | ||||||
| } | ||||||
|
|
||||||
| // Otherwise, there's nothing we can do. | ||||||
| } | ||||||
|
|
||||||
| $headers = Requests::flatten($headers); | ||||||
|
|
||||||
| switch ($options['type']) { | ||||||
| case Requests::POST: | ||||||
| curl_setopt($this->handle, CURLOPT_POST, true); | ||||||
|
|
@@ -463,7 +562,7 @@ private function setup_handle($url, $headers, $data, $options) { | |||||
| curl_setopt($this->handle, CURLOPT_CONNECTTIMEOUT_MS, round($options['connect_timeout'] * 1000)); | ||||||
| } | ||||||
|
|
||||||
| curl_setopt($this->handle, CURLOPT_URL, $url); | ||||||
| curl_setopt($this->handle, CURLOPT_URL, $exec_url); | ||||||
| curl_setopt($this->handle, CURLOPT_USERAGENT, $options['useragent']); | ||||||
| if (!empty($headers)) { | ||||||
| curl_setopt($this->handle, CURLOPT_HTTPHEADER, $headers); | ||||||
|
|
@@ -632,6 +731,19 @@ public static function test($capabilities = []) { | |||||
| return false; | ||||||
| } | ||||||
|
|
||||||
| // If needed, check that our installed curl version supports host bindings | ||||||
| if (isset($capabilities[Capability::HOST_BINDINGS]) && $capabilities[Capability::HOST_BINDINGS]) { | ||||||
| /* | ||||||
| * CURLOPT_RESOLVE - Added in 7.21.3. | ||||||
| * - Removal support added in 7.42.0. | ||||||
| * - Support for providing multiple IP addresses per entry was added in 7.59.0. | ||||||
| * CURLOPT_CONNECT_TO - Added in 7.49.0. | ||||||
| */ | ||||||
| if (defined('CURLOPT_RESOLVE') === false && defined('CURLOPT_CONNECT_TO') === false) { | ||||||
| return false; | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| // If needed, check that our installed curl version supports SSL | ||||||
| if (isset($capabilities[Capability::SSL]) && $capabilities[Capability::SSL]) { | ||||||
| $curl_version = curl_version(); | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.