Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -801,6 +801,10 @@ public void proxy() throws Exception

// Do not proxy requests for localhost:8080.
proxy.getExcludedAddresses().add("localhost:8080");
// Do not proxy requests to internal services using wildcard pattern.
proxy.getExcludedAddresses().add("*.internal.corp");
// Do not proxy requests to private network using CIDR notation.
proxy.getExcludedAddresses().add("192.168.0.0/16");

// Add the new proxy to the list of proxies already registered.
ProxyConfiguration proxyConfig = httpClient.getProxyConfiguration();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -922,6 +922,34 @@ include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/ht

You specify the proxy host and proxy port, and optionally also the addresses that you do not want to be proxied, and then add the proxy configuration on the `ProxyConfiguration` instance.

The included and excluded address sets support patterns for flexible matching:

* *Exact hostname*: `example.com` matches only that exact hostname
* *Wildcard prefix*: `*.example.com` matches any subdomain of example.com, e.g., `www.example.com`, `api.example.com`, as well as `example.com` itself
* *Wildcard suffix*: `internal.*` matches any hostname starting with `internal.`, e.g., `internal.corp`, `internal.local`, `internal.evil.corp`
* *CIDR notation*: `192.168.0.0/16` matches any IP address in the subnet
* *IP range*: `10.0.0.1-10.0.0.10` matches IP addresses within the inclusive range
* *Port specification*: Any pattern can include a port, e.g., `*.example.com:8080` to match only on that specific port. Patterns without a port match any port.

[NOTE]
====
*Pattern Limitations:*

* Wildcards (`*`) are only supported at the start (`*.example.com`) or end (`internal.*`), not in the middle
* The dot in wildcard patterns is a literal dot, not a regex "any character"
* Patterns cannot contain `@` (userinfo) or `/` (path components)
====

[IMPORTANT]
====
*DNS Resolution:*

IP-based patterns (CIDR notation like `192.168.0.0/16` and IP ranges like `10.0.0.1-10.0.0.10`)
require DNS resolution when matching against hostnames. This adds network latency and will
fail to match if DNS is unavailable. For better performance, prefer hostname wildcards
(`*.internal.corp`) over CIDR patterns when possible.
====

Configured in this way, `HttpClient` makes requests to the HTTP proxy (for plain-text HTTP requests) or establishes a tunnel via HTTP `CONNECT` (for encrypted HTTPS requests).

Proxying is supported for any version of the HTTP protocol.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
package org.eclipse.jetty.client;

import java.net.URI;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
Expand All @@ -23,6 +22,8 @@
import org.eclipse.jetty.io.ClientConnectionFactory;
import org.eclipse.jetty.util.BlockingArrayQueue;
import org.eclipse.jetty.util.HostPort;
import org.eclipse.jetty.util.HostPortSet;
import org.eclipse.jetty.util.IncludeExcludeSet;
import org.eclipse.jetty.util.ssl.SslContextFactory;

/**
Expand Down Expand Up @@ -86,9 +87,7 @@ public Proxy match(Origin origin)

public abstract static class Proxy
{
// TODO use InetAddressSet? Or IncludeExcludeSet?
private final Set<String> included = new HashSet<>();
private final Set<String> excluded = new HashSet<>();
private final IncludeExcludeSet<String, HostPort> _addresses = new IncludeExcludeSet<>(HostPortSet.class);
private final Origin origin;
private final SslContextFactory.Client sslContextFactory;

Expand Down Expand Up @@ -141,23 +140,45 @@ public Origin.Protocol getProtocol()
}

/**
* <p>Returns the set of origins that must be proxied.</p>
* <p>The set supports patterns including:</p>
* <ul>
* <li>Exact hostname match: {@code example.com}</li>
* <li>Hostname wildcard prefix: {@code *.example.com}</li>
* <li>Hostname wildcard suffix: {@code internal.*}</li>
* <li>CIDR notation: {@code 192.168.0.0/16}</li>
* <li>IP range: {@code 10.0.0.1-10.0.0.10}</li>
* <li>Port specification: {@code example.com:8080}</li>
* </ul>
*
* @return the set of origins that must be proxied
* @see #matches(Origin)
* @see #getExcludedAddresses()
*/
public Set<String> getIncludedAddresses()
{
return included;
return _addresses.getIncluded();
}

/**
* @return the set of origins that must not be proxied.
* <p>Returns the set of origins that must not be proxied.</p>
* <p>The set supports patterns including:</p>
* <ul>
* <li>Exact hostname match: {@code example.com}</li>
* <li>Hostname wildcard prefix: {@code *.example.com}</li>
* <li>Hostname wildcard suffix: {@code internal.*}</li>
* <li>CIDR notation: {@code 192.168.0.0/16}</li>
* <li>IP range: {@code 10.0.0.1-10.0.0.10}</li>
* <li>Port specification: {@code example.com:8080}</li>
* </ul>
*
* @return the set of origins that must not be proxied
* @see #matches(Origin)
* @see #getIncludedAddresses()
*/
public Set<String> getExcludedAddresses()
{
return excluded;
return _addresses.getExcluded();
}

/**
Expand All @@ -180,34 +201,9 @@ public boolean matches(Origin origin)
if (getAddress().equals(origin.getAddress()))
return false;

boolean result = included.isEmpty();
Origin.Address address = origin.getAddress();
for (String included : this.included)
{
if (matches(address, included))
{
result = true;
break;
}
}
for (String excluded : this.excluded)
{
if (matches(address, excluded))
{
result = false;
break;
}
}
return result;
}

private boolean matches(Origin.Address address, String pattern)
{
// TODO: add support for CIDR notation like 192.168.0.0/24, see DoSFilter
HostPort hostPort = new HostPort(pattern);
String host = hostPort.getHost();
int port = hostPort.getPort();
return host.equals(address.getHost()) && (port <= 0 || port == address.getPort());
HostPort hostPort = new HostPort(address.getHost(), address.getPort());
return _addresses.test(hostPort);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,105 @@ public void testProxyMatchesWithIncludesAndExcludesIPv6()
assertTrue(proxy.matches(new Origin("http", "[1::2:3:4]", 0)));
assertFalse(proxy.matches(new Origin("http", "[1::2:3:4]", 5)));
}

@Test
public void testProxyMatchesWithWildcardPrefix()
{
HttpProxy proxy = new HttpProxy("host", 0);
proxy.getExcludedAddresses().add("*.internal.corp");

assertTrue(proxy.matches(new Origin("http", "example.com", 80)));
assertFalse(proxy.matches(new Origin("http", "api.internal.corp", 80)));
assertFalse(proxy.matches(new Origin("http", "db.internal.corp", 443)));
// Also matches the domain itself
assertFalse(proxy.matches(new Origin("http", "internal.corp", 80)));
}

@Test
public void testProxyMatchesWithWildcardSuffix()
{
HttpProxy proxy = new HttpProxy("host", 0);
proxy.getExcludedAddresses().add("localhost.*");

assertTrue(proxy.matches(new Origin("http", "example.com", 80)));
assertFalse(proxy.matches(new Origin("http", "localhost.local", 80)));
assertFalse(proxy.matches(new Origin("http", "localhost.corp", 443)));
}

@Test
public void testProxyMatchesWithCidr()
{
HttpProxy proxy = new HttpProxy("host", 0);
proxy.getExcludedAddresses().add("192.168.0.0/16");

assertTrue(proxy.matches(new Origin("http", "10.0.0.1", 80)));
assertFalse(proxy.matches(new Origin("http", "192.168.1.1", 80)));
assertFalse(proxy.matches(new Origin("http", "192.168.255.255", 443)));
}

@Test
public void testProxyMatchesWithIpRange()
{
HttpProxy proxy = new HttpProxy("host", 0);
proxy.getExcludedAddresses().add("10.0.0.1-10.0.0.10");

assertTrue(proxy.matches(new Origin("http", "10.0.0.11", 80)));
assertFalse(proxy.matches(new Origin("http", "10.0.0.1", 80)));
assertFalse(proxy.matches(new Origin("http", "10.0.0.5", 80)));
assertFalse(proxy.matches(new Origin("http", "10.0.0.10", 80)));
}

@Test
public void testProxyMatchesWithWildcardAndPort()
{
HttpProxy proxy = new HttpProxy("host", 0);
proxy.getExcludedAddresses().add("*.internal.corp:8080");

assertTrue(proxy.matches(new Origin("http", "api.internal.corp", 80)));
assertTrue(proxy.matches(new Origin("http", "api.internal.corp", 443)));
assertFalse(proxy.matches(new Origin("http", "api.internal.corp", 8080)));
}

@Test
public void testProxyMatchesWithMultiplePatterns()
{
HttpProxy proxy = new HttpProxy("host", 0);
proxy.getExcludedAddresses().add("*.internal.corp");
proxy.getExcludedAddresses().add("192.168.0.0/16");
proxy.getExcludedAddresses().add("localhost");

assertTrue(proxy.matches(new Origin("http", "example.com", 80)));
assertTrue(proxy.matches(new Origin("http", "10.0.0.1", 80)));

assertFalse(proxy.matches(new Origin("http", "api.internal.corp", 80)));
assertFalse(proxy.matches(new Origin("http", "192.168.1.1", 80)));
assertFalse(proxy.matches(new Origin("http", "localhost", 8080)));
}

@Test
public void testProxyMatchesWithIncludeWildcardExcludeSpecific()
{
HttpProxy proxy = new HttpProxy("host", 0);
proxy.getIncludedAddresses().add("*.example.com");
proxy.getExcludedAddresses().add("admin.example.com");

assertFalse(proxy.matches(new Origin("http", "other.com", 80)));
assertTrue(proxy.matches(new Origin("http", "www.example.com", 80)));
assertTrue(proxy.matches(new Origin("http", "api.example.com", 443)));
assertFalse(proxy.matches(new Origin("http", "admin.example.com", 80)));
}

@Test
public void testProxyMatchesWithCidrIncludeAndExclude()
{
HttpProxy proxy = new HttpProxy("host", 0);
proxy.getIncludedAddresses().add("10.0.0.0/8");
proxy.getExcludedAddresses().add("10.10.0.0/16");

assertFalse(proxy.matches(new Origin("http", "192.168.1.1", 80)));
assertTrue(proxy.matches(new Origin("http", "10.0.0.1", 80)));
assertTrue(proxy.matches(new Origin("http", "10.255.255.255", 80)));
assertFalse(proxy.matches(new Origin("http", "10.10.0.1", 80)));
assertFalse(proxy.matches(new Origin("http", "10.10.255.255", 80)));
}
}
Loading