Skip to content

Add Host Bindings feature#1015

Open
schlessera wants to merge 14 commits intodevelopfrom
add/host-bindings
Open

Add Host Bindings feature#1015
schlessera wants to merge 14 commits intodevelopfrom
add/host-bindings

Conversation

@schlessera
Copy link
Member

Pull Request Type

This is a:

  • Bug fix
  • New feature
  • Documentation improvement
  • Code quality improvement

Context

This PR adds support for binding specific hostnames to specific IP addresses, bypassing DNS resolution.

This is useful for:

  • Testing against specific server instances
  • Load balancing and failover scenarios
  • Connecting to servers via specific IPs while preserving the original hostname in the Host header
  • Development environments where DNS may not be configured
  • Preventing rebinding attacks

Detailed Description

New HostBindings utility class

A value object that validates and stores hostname-to-IP mappings. Key features:

  • Security by default: IP addresses are validated using filter_var(FILTER_VALIDATE_IP) to prevent hostname injection attacks. Accepts both IPv4 and IPv6 addresses (including private/localhost ranges).
  • Opt-out mechanism: For pre-validated inputs or non-standard formats, validation can be skipped via HostBindings::SKIP_IP_VALIDATION (use with caution).
  • Normalization: Whitespace is trimmed from IP addresses.

Methods:

  • has_host($host) - Check if a host has a mapping
  • get_first_ip_for_host($host) - Get the first IP for a host (throws UnknownHost or MissingIpAddress if not available)
  • get_all_ips_for_host($host) - Get all IPs for a host (throws UnknownHost if not found)

New exceptions

  • UnknownHost - Thrown when requesting a host that isn't in the bindings
  • MissingIpAddress - Thrown when a host has no IP addresses configured

New HOST_BINDINGS capability

Added to the Capability interface to allow transport capability testing.

Transport integration

Curl transport (Transport\Curl):

  • Uses CURLOPT_CONNECT_TO (cURL 7.49.0+) as the preferred method
  • Falls back to CURLOPT_RESOLVE (cURL 7.21.3+) for older cURL versions
  • Falls back to URL rewriting for HTTP (not HTTPS) on very old cURL versions
  • Properly handles IPv6 addresses by wrapping them in square brackets
  • Preserves the original hostname in the Host header

Fsockopen transport (Transport\Fsockopen):

  • Replaces the connection host with the mapped IP address
  • Original hostname is preserved in the Host header

Usage

The host_bindings option accepts either an array (with automatic IP validation) or a pre-constructed HostBindings object:

// Using an array (IPs are validated)
$response = Requests::get('https://example.com/api', [], [
    'host_bindings' => [
        'example.com' => ['93.184.216.34', '2606:2800:220:1:248:1893:25c8:1946'],
    ],
]);

// Using a HostBindings object (for advanced control)
$bindings = new HostBindings(
    ['example.com' => ['custom-value']],
    HostBindings::SKIP_IP_VALIDATION
);
$response = Requests::get('https://example.com/api', [], [
    'host_bindings' => $bindings,
]);

Includes

  • Comprehensive unit tests for HostBindings class (constructor validation, all methods)
  • Unit tests for new exceptions
  • Integration tests in Transport\BaseTestCase covering both transports
  • Documentation in Requests::request() docblock

@schlessera schlessera requested a review from jrfnl December 12, 2025 17:43
@schlessera schlessera changed the title Redo initial implementation Add Host Bindings Feature Dec 12, 2025
@schlessera schlessera changed the title Add Host Bindings Feature Add Host Bindings feature Dec 12, 2025
@jrfnl jrfnl added this to the 2.1.0 milestone Mar 10, 2026
@jrfnl
Copy link
Member

jrfnl commented Mar 10, 2026

Prelim remark: how about adding that code example (and maybe some more text) to the documentation for the website ? As in: either add or update one of the existing markdown files in the docs directory ?

@jrfnl
Copy link
Member

jrfnl commented Mar 10, 2026

@schlessera Would you mind also having a look at what's happening with the code coverage ? I would not expect a new feature, which is described as "fully tested" to reduce code coverage.

Copy link
Member

@jrfnl jrfnl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not a complete review (yet). Posting it anyway to allow for further iteration on this PR based on my remarks.

/**
* Requests for PHP, an HTTP library.
*
* @copyright 2012-2023 Requests Contributors
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @copyright 2012-2023 Requests Contributors
* @copyright 2012-2026 Requests Contributors

(here and elsewhere - or should we do this codebase wide after this PR has been merged ?)

* Exception for a missing IP address in the host bindings.
*
* @package Requests\Exceptions
* @since 2.x.x
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @since 2.x.x
* @since 2.1.0

* Instantiate a MissingIpAddress exception for a missing IP address in the host bindings.
*
* @param string $host Host that was requested.
* @return self
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @return self
* @return \WpOrg\Requests\Exception\MissingIpAddress

* requested via HostBindings.
*
* @param string $host Unknown host that was requested.
* @return self
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @return self
* @return \WpOrg\Requests\Exception\UnknownHost

* Exception for an unknown host being requested via HostBindings.
*
* @package Requests\Exceptions
* @since 2.x.x
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @since 2.x.x
* @since 2.1.0

Comment on lines +176 to +177
* @throws UnknownHost If the requested host does not have a mapping.
* @throws MissingIpAddress If the requested host has no IP address available.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @throws UnknownHost If the requested host does not have a mapping.
* @throws MissingIpAddress If the requested host has no IP address available.
*
* @throws \WpOrg\Requests\Exception\UnknownHost If the requested host does not have a mapping.
* @throws \WpOrg\Requests\Exception\MissingIpAddress If the requested host has no IP address available.

Comment on lines +171 to +172
* This throws an exception if the requested host does not have a mapping.
* This also throws an exception if the requested host has no IP addresses available.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks redundant, what with the @throws tags containing the same info.

*
* @param string $host Host to request a mapping for.
* @return array<string> IP address mappings for the host.
* @throws UnknownHost If the requested host does not have a mapping.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @throws UnknownHost If the requested host does not have a mapping.
*
* @throws \WpOrg\Requests\Exception\UnknownHost If the requested host does not have a mapping.

Comment on lines +194 to +196
* This throws an exception if the requested host does not have a mapping.
* Contrary to get_first_ip_for_host(), this method does not throw an exception
* if the host has no IP addresses available. It will simply return an empty array.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just wondering - why still throw an exception if the host doesn't have a mapping ? Why not return an empty array in that case too ?

Comment on lines +464 to +466
// Fallback branches for cURL < 7.49.0 which lacks CURLOPT_CONNECT_TO.
// Cannot be reached in tests as CURLOPT_CONNECT_TO is always defined.
// @codeCoverageIgnoreStart -- The else is never reached in CI because CURLOPT_CONNECT_TO is always defined.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Fallback branches for cURL < 7.49.0 which lacks CURLOPT_CONNECT_TO.
// Cannot be reached in tests as CURLOPT_CONNECT_TO is always defined.
// @codeCoverageIgnoreStart -- The else is never reached in CI because CURLOPT_CONNECT_TO is always defined.
// Fallback branches for cURL < 7.49.0 which lacks CURLOPT_CONNECT_TO.
// Cannot be reached in tests as CURLOPT_CONNECT_TO is always defined.
// The `else` is never reached in CI because CURLOPT_CONNECT_TO is always defined.
// @codeCoverageIgnoreStart

Might need testing, but I have a niggly feeling that the PHPUnit code coverage annotations do not support adding a comment behind the annotation.
That might explain why the comment isn't working as intended when viewing the CodeCov report.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants