The OpenJDK net-dev mailing list is the best place to bring this. There
was discussion about SOCKS when the HTTP client was developed, I thought
JEP 321 had a summary on this but it seems not. I'm sure others on
net-dev can say more on this.
-Alan
On 12/05/2024 22:58, Alessandro Autiero wrote:
Hello, my name is Alessandro Autiero and I'd like to propose three
enhancements for the java core libraries to better support proxies in
network components of the JDK.
There are three classes in the java.net <http://java.net> package that
have proxy support:
* java.net.Socket
Introduced in Java 1.0, supports HTTP(S)/SOCKS proxies modelled by
java.net.Proxy through the java.net.Socket(java.net.Proxy) constructor
* java.net.HttpURLConnection
Introduced in Java 1.1, supports HTTP(S)/SOCKS proxies modelled by
java.net.Proxy through the
java.net.URL#openConnection(java.net.Proxy) public method
* java.net.HttpClient (Introduced in Java 11)
Introduced in Java 11, supports HTTP(S) proxies modelled by
java.net.ProxySelector through the public
proxy(java.net.ProxySelector) method in its builder or the default
java.net.ProxySelector, which can be set by calling
java.net.ProxySelector#setDefault(java.net.ProxySelector)
While most proxies provide support for both the HTTP and SOCKS scheme,
considering that the older HTTP client API had support for both,
developers might choose to use the older api, or to use an external
one, if they need or want to provide support for this feature. A quick
Google search for a recommendation on which Http Client to use on a
modern Java version yields many results that list SOCKS support as a
feature to keep in mind when making a choice. While this is not
necessarily indicative of the average Java developer sentiment about
the feature, I think that it should be considered, alongside a couple
of issues that were opened on StackOverFlow
<https://stackoverflow.com/questions/70011046/how-to-use-a-socks-proxy-with-java-jdk-httpclient>
asking about support for this feature. Accordingly, I propose adding
support for SOCKS proxies in java.net.HttpClient. If the change is
allowed, consider that the default java.net.ProxySelector is an
instance of sun.net.spi.DefaultProxySelector, which supports SOCKS
proxies, but this implementation cannot be initialized by the user as
it's not exposed by the module system. Starting from Java 9,
ProxySelector#of(InetSocketAddress) was introduced, which returns an
instance of java.net.ProxySelector$StaticProxySelector
<https://github.com/openjdk/jdk/blob/5053b70a7fc67ce9b73dbeecbdd88fbc34d45e04/src/java.base/share/classes/java/net/ProxySelector.java#L194>,
a static inner class of ProxySelector introduced in Java 9 which only
implements support for HTTP(S) proxies. StaticProxySelector's
constructor could be modified from
StaticProxySelector(java.net.InetSocketAddress) to
StaticProxySelector(java.net.Proxy$Type, java.net.InetSocketAddress)
to initialize the java.net.Proxy instance with a specified proxy type
instead of hard coding HTTP. Then we could overload the method
ProxySelector#of(InetSocketAddress) with
ProxySelector#of(java.net.Proxy$Type, InetSocketAddress) method which
would invoke the constructor we defined earlier. This change would not
be breaking as StaticProxySelector is package private, no public
methods would be deleted and the default scheme would still be HTTP.
jdk.internal.net.http.HttpRequestImpl uses the ProxySelector in its
retrieveProxy method, but checks if the proxy is an HTTP proxy: this
would need to be changed as well. Finally, considering that unlike
HttpURLConnection, HttpClient doesn't delegate the underlying
connection to java.net.Socket, the java.net.http module would need to
be enhanced to support SOCKS authentication, which could take more effort.
Another observation that I've made is about authentication. If a proxy
requires basic authentication, that is authentication through a
username and optionally a password, a developer can implement the
java.net.Authenticator class and override the
getPasswordAuthentication method. While basic authentication is still
the norm for most proxies, it's disabled by default in the JDK since
Java 8. Though, it's possible to enable it by overriding the net
properties jdk.http.auth.proxying.disabledSchemes and
jdk.http.auth.tunneling.disabledSchemes using System.setProperty. I
couldn't find an explanation about why this change was implemented, so
I can only speculate that it was done to incentivize Java developers
to use an IP whitelist instead of basic auth to improve security,
assuming that the connection isn't secure(HTTP). The problem though is
that the net properties that need to be changed to allow basic proxy
authentication are only read only one time in the static initializer
of sun.net.www.protocol.http.HttpURLConnection class, the underlying
implementation of java.net.HttpURLConnection. So, if for example a
library loads this class before the developer calls
System.setProperty, the change will have no effect and the
authentication will subsequently fail. This may seem like an edge
case, but for example in a Spring Boot environment, this exact issue
will arise if the property isn't set before calling
SpringApplication.run. I think that the best solution would be to
remove the disabledTunnelingSchemes and disabledProxyingSchemes static
fields from sun.net.www.protocol.http.HttpURLConnection and read the
net properties when they are used instead of caching them. This
solution is not a breaking change and should be very easy to implement
as both fields are only referenced when initializing a
AuthenticationHeader in the same class. Additionally, if we can agree
on the fact that basic authentication is still the predominant way to
provide authentication capabilities for a proxy and/or that disabling
it doesn't provide a direct security benefit, I propose to also set
jdk.http.auth.proxying.disabledSchemes and
jdk.http.auth.tunneling.disabledSchemes to an empty String so no
schemes are disabled by default. This change could be breaking for
Applications developed starting from Java 8 that expect basic
authentication to be disabled, but I think that the scope of the
impact would be much smaller, if there would be any at all, than when
these flags were introduced breaking basic authentication for existing
applications upgrading to Java 8.
The last issue I've noticed is also about authentication. If a
developer wants to set an instance of Authenticator instead of relying
on the default one, which can be set using
Authenticator.setAuthenticator, this may not be possible depending on
the implementation:
* java.net.Socket
Not supported
* java.net.HttpURLConnection
Supported through the setAuthenticator(Authenticator) method
introduced in Java 9
* java.net.HttpClient
Supported through the authenticator(Authenticator) method in its
builder
The reason why a developer might want to provide an instance of
Authenticator instead of relying on the default one is that, for
example, in a concurrent environment where multiple sockets exist,
each using a different proxy, if the proxy host and port are the same,
but each Socket instance is expected to use a different pair of
username and passwords, the default Authenticator cannot determine
which pair of credentials should be assigned to a given authentication
request as it lacks the scope to make this decision. This change is
not breaking as the default authenticator of a Socket would still be
the default authenticator, which is the current behaviour. If the
change is allowed, a possible solution would be to add a private field
named authenticator and a public method
setAuthenticator(Authenticator) to java.net.Socket. Then
HttpConnectSocketImpl, the socket implementation for a socket using an
http(s) proxy, would need to use the authenticator of its delegating
socket instead of the default one in the doTunnel method: this is a
one line of code change as HttpURLConnection already has support for
setAuthenticator from Java 9. Finally, SocksSocketImpl, the socket
implementation for a socket using a socks4/5 proxy, would need to use
the authenticator of the delegating socket in the authenticate method.
instead of calling Authenticator.requestPasswordAuthentication, which
uses the default authenticator.
I am happy to work on all, if any, of the enhancements that are
considered feasible.
I'm also looking forward to any possible criticism and feedback.
Thanks in advance.
I