8160838: Better HTTP service
authorchegar
Mon, 18 Jul 2016 08:28:48 +0100
changeset 41579 c0fe2e6364d9
parent 41578 6bc194dea076
child 41580 cc479488428c
8160838: Better HTTP service Reviewed-by: ahgross, alanb, michaelm
jdk/src/java.base/share/classes/sun/net/www/protocol/http/AuthenticationHeader.java
jdk/src/java.base/share/classes/sun/net/www/protocol/http/HttpURLConnection.java
jdk/src/java.base/share/conf/net.properties
jdk/test/sun/net/www/protocol/https/HttpsClient/OriginServer.java
jdk/test/sun/net/www/protocol/https/HttpsClient/ProxyAuthTest.java
jdk/test/sun/net/www/protocol/https/HttpsClient/ProxyTunnelServer.java
jdk/test/sun/net/www/protocol/https/HttpsURLConnection/PostThruProxyWithAuth.sh
--- a/jdk/src/java.base/share/classes/sun/net/www/protocol/http/AuthenticationHeader.java	Fri Jul 15 20:57:41 2016 +0100
+++ b/jdk/src/java.base/share/classes/sun/net/www/protocol/http/AuthenticationHeader.java	Mon Jul 18 08:28:48 2016 +0100
@@ -25,8 +25,11 @@
 
 package sun.net.www.protocol.http;
 
+import java.util.Collections;
 import java.util.Iterator;
 import java.util.HashMap;
+import java.util.Set;
+
 import sun.net.www.*;
 import sun.security.action.GetPropertyAction;
 
@@ -67,8 +70,8 @@
  *      -Dhttp.auth.preference="scheme"
  *
  * which in this case, specifies that "scheme" should be used as the auth scheme when offered
- * disregarding the default prioritisation. If scheme is not offered then the default priority
- * is used.
+ * disregarding the default prioritisation. If scheme is not offered, or explicitly
+ * disabled, by {@code disabledSchemes}, then the default priority is used.
  *
  * Attention: when http.auth.preference is set as SPNEGO or Kerberos, it's actually "Negotiate
  * with SPNEGO" or "Negotiate with Kerberos", which means the user will prefer the Negotiate
@@ -113,17 +116,32 @@
     String hdrname; // Name of the header to look for
 
     /**
-     * parse a set of authentication headers and choose the preferred scheme
-     * that we support for a given host
+     * Parses a set of authentication headers and chooses the preferred scheme
+     * that is supported for a given host.
      */
     public AuthenticationHeader (String hdrname, MessageHeader response,
             HttpCallerInfo hci, boolean dontUseNegotiate) {
+        this(hdrname, response, hci, dontUseNegotiate, Collections.emptySet());
+    }
+
+    /**
+     * Parses a set of authentication headers and chooses the preferred scheme
+     * that is supported for a given host.
+     *
+     * <p> The {@code disabledSchemes} parameter is a, possibly empty, set of
+     * authentication schemes that are disabled.
+     */
+    public AuthenticationHeader(String hdrname,
+                                MessageHeader response,
+                                HttpCallerInfo hci,
+                                boolean dontUseNegotiate,
+                                Set<String> disabledSchemes) {
         this.hci = hci;
         this.dontUseNegotiate = dontUseNegotiate;
-        rsp = response;
+        this.rsp = response;
         this.hdrname = hdrname;
-        schemes = new HashMap<>();
-        parse();
+        this.schemes = new HashMap<>();
+        parse(disabledSchemes);
     }
 
     public HttpCallerInfo getHttpCallerInfo() {
@@ -143,10 +161,11 @@
      * then the last one will be used. The
      * preferred scheme that we support will be used.
      */
-    private void parse () {
+    private void parse(Set<String> disabledSchemes) {
         Iterator<String> iter = rsp.multiValueIterator(hdrname);
         while (iter.hasNext()) {
             String raw = iter.next();
+            // HeaderParser lower cases everything, so can be used case-insensitively
             HeaderParser hp = new HeaderParser(raw);
             Iterator<String> keys = hp.keys();
             int i, lastSchemeIndex;
@@ -156,7 +175,8 @@
                     if (lastSchemeIndex != -1) {
                         HeaderParser hpn = hp.subsequence (lastSchemeIndex, i);
                         String scheme = hpn.findKey(0);
-                        schemes.put (scheme, new SchemeMapValue (hpn, raw));
+                        if (!disabledSchemes.contains(scheme))
+                            schemes.put(scheme, new SchemeMapValue (hpn, raw));
                     }
                     lastSchemeIndex = i;
                 }
@@ -164,7 +184,8 @@
             if (i > lastSchemeIndex) {
                 HeaderParser hpn = hp.subsequence (lastSchemeIndex, i);
                 String scheme = hpn.findKey(0);
-                schemes.put(scheme, new SchemeMapValue (hpn, raw));
+                if (!disabledSchemes.contains(scheme))
+                    schemes.put(scheme, new SchemeMapValue (hpn, raw));
             }
         }
 
--- a/jdk/src/java.base/share/classes/sun/net/www/protocol/http/HttpURLConnection.java	Fri Jul 15 20:57:41 2016 +0100
+++ b/jdk/src/java.base/share/classes/sun/net/www/protocol/http/HttpURLConnection.java	Mon Jul 18 08:28:48 2016 +0100
@@ -25,6 +25,7 @@
 
 package sun.net.www.protocol.http;
 
+import java.security.PrivilegedAction;
 import java.util.Arrays;
 import java.net.URL;
 import java.net.URLConnection;
@@ -109,6 +110,14 @@
     static final boolean validateProxy;
     static final boolean validateServer;
 
+    /** A, possibly empty, set of authentication schemes that are disabled
+     *  when proxying plain HTTP ( not HTTPS ). */
+    static final Set<String> disabledProxyingSchemes;
+
+    /** A, possibly empty, set of authentication schemes that are disabled
+     *  when setting up a tunnel for HTTPS ( HTTP CONNECT ). */
+    static final Set<String> disabledTunnelingSchemes;
+
     private StreamingOutputStream strOutputStream;
     private static final String RETRY_MSG1 =
         "cannot retry due to proxy authentication, in streaming mode";
@@ -206,6 +215,22 @@
         "Via"
     };
 
+    private static String getNetProperty(String name) {
+        PrivilegedAction<String> pa = () -> NetProperties.get(name);
+        return AccessController.doPrivileged(pa);
+    }
+
+    private static Set<String> schemesListToSet(String list) {
+        if (list == null || list.isEmpty())
+            return Collections.emptySet();
+
+        Set<String> s = new HashSet<>();
+        String[] parts = list.split("\\s*,\\s*");
+        for (String part : parts)
+            s.add(part.toLowerCase(Locale.ROOT));
+        return s;
+    }
+
     static {
         Properties props = GetPropertyAction.privilegedGetProperties();
         maxRedirects = GetIntegerAction.privilegedGetProperty(
@@ -218,6 +243,14 @@
             agent = agent + " Java/"+version;
         }
         userAgent = agent;
+
+        // A set of net properties to control the use of authentication schemes
+        // when proxing/tunneling.
+        String p = getNetProperty("jdk.http.auth.tunneling.disabledSchemes");
+        disabledTunnelingSchemes = schemesListToSet(p);
+        p = getNetProperty("jdk.http.auth.proxying.disabledSchemes");
+        disabledProxyingSchemes = schemesListToSet(p);
+
         validateProxy = Boolean.parseBoolean(
                 props.getProperty("http.auth.digest.validateProxy"));
         validateServer = Boolean.parseBoolean(
@@ -1575,10 +1608,13 @@
                     // altered in similar ways.
 
                     AuthenticationHeader authhdr = new AuthenticationHeader (
-                            "Proxy-Authenticate", responses,
-                            new HttpCallerInfo(url, http.getProxyHostUsed(),
-                                http.getProxyPortUsed()),
-                            dontUseNegotiate
+                            "Proxy-Authenticate",
+                            responses,
+                            new HttpCallerInfo(url,
+                                               http.getProxyHostUsed(),
+                                               http.getProxyPortUsed()),
+                            dontUseNegotiate,
+                            disabledProxyingSchemes
                     );
 
                     if (!doingNTLMp2ndStage) {
@@ -2024,11 +2060,14 @@
                         }
                     }
 
-                    AuthenticationHeader authhdr = new AuthenticationHeader (
-                            "Proxy-Authenticate", responses,
-                            new HttpCallerInfo(url, http.getProxyHostUsed(),
-                                http.getProxyPortUsed()),
-                            dontUseNegotiate
+                    AuthenticationHeader authhdr = new AuthenticationHeader(
+                            "Proxy-Authenticate",
+                            responses,
+                            new HttpCallerInfo(url,
+                                               http.getProxyHostUsed(),
+                                               http.getProxyPortUsed()),
+                            dontUseNegotiate,
+                            disabledTunnelingSchemes
                     );
                     if (!doingNTLMp2ndStage) {
                         proxyAuthentication =
--- a/jdk/src/java.base/share/conf/net.properties	Fri Jul 15 20:57:41 2016 +0100
+++ b/jdk/src/java.base/share/conf/net.properties	Mon Jul 18 08:28:48 2016 +0100
@@ -72,3 +72,30 @@
 # value is 10).
 # http.KeepAlive.remainingData=512
 # http.KeepAlive.queuedConnections=10
+
+# Authentication Scheme restrictions for HTTP and HTTPS.
+#
+# In some environments certain authentication schemes may be undesirable
+# when proxying HTTP or HTTPS.  For example, "Basic" results in effectively the
+# cleartext transmission of the user's password over the physical network.
+# This section describes the mechanism for disabling authentication schemes
+# based on the scheme name. Disabled schemes will be treated as if they are not
+# supported by the implementation.
+#
+# The 'jdk.http.auth.tunneling.disabledSchemes' property lists the authentication
+# schemes that will be disabled when tunneling HTTPS over a proxy, HTTP CONNECT.
+# The 'jdk.http.auth.proxying.disabledSchemes' property lists the authentication
+# schemes that will be disabled when proxying HTTP.
+#
+# In both cases the property is a comma-separated list of, case-insensitive,
+# authentication scheme names, as defined by their relevant RFCs. An
+# implementation may, but is not required to, support common schemes whose names
+# include: 'Basic', 'Digest', 'NTLM', 'Kerberos', 'Negotiate'.  A scheme that
+# is not known, or not supported, by the implementation is ignored.
+#
+# Note: This property is currently used by the JDK Reference implementation. It
+# is not guaranteed to be examined and used by other implementations.
+#
+#jdk.http.auth.proxying.disabledSchemes=
+jdk.http.auth.tunneling.disabledSchemes=Basic
+
--- a/jdk/test/sun/net/www/protocol/https/HttpsClient/OriginServer.java	Fri Jul 15 20:57:41 2016 +0100
+++ b/jdk/test/sun/net/www/protocol/https/HttpsClient/OriginServer.java	Mon Jul 18 08:28:48 2016 +0100
@@ -36,10 +36,12 @@
  * Http get request in both clear and secure channel
  */
 
-public abstract class OriginServer implements Runnable {
+public abstract class OriginServer implements Runnable, Closeable {
 
     private ServerSocket server = null;
     Exception serverException = null;
+    private volatile boolean closed;
+
     /**
      * Constructs a OriginServer based on ss and
      * obtains a response data's bytecodes using the method
@@ -53,6 +55,14 @@
             throw serverException;
     }
 
+    @Override
+    public void close() throws IOException {
+        if (closed)
+            return;
+        closed = true;
+        server.close();
+    }
+
     /**
      * Returns an array of bytes containing the bytes for
      * the data sent in the response.
@@ -73,8 +83,10 @@
         try {
             socket = server.accept();
         } catch (IOException e) {
-            System.out.println("Class Server died: " + e.getMessage());
-            serverException = e;
+            if (!closed) {
+                System.out.println("Class Server died: " + e.getMessage());
+                serverException = e;
+            }
             return;
         }
         try {
--- a/jdk/test/sun/net/www/protocol/https/HttpsClient/ProxyAuthTest.java	Fri Jul 15 20:57:41 2016 +0100
+++ b/jdk/test/sun/net/www/protocol/https/HttpsClient/ProxyAuthTest.java	Mon Jul 18 08:28:48 2016 +0100
@@ -23,22 +23,31 @@
 
 /*
  * @test
- * @bug 4323990 4413069
+ * @bug 4323990 4413069 8160838
  * @summary HttpsURLConnection doesn't send Proxy-Authorization on CONNECT
  *     Incorrect checking of proxy server response
  * @modules java.base/sun.net.www
- * @run main/othervm ProxyAuthTest
- *
- *     No way to reserve and restore java.lang.Authenticator, need to run this
- *     test in othervm mode.
+ * @run main/othervm ProxyAuthTest fail
+ * @run main/othervm -Djdk.http.auth.tunneling.disabledSchemes=Basic ProxyAuthTest fail
+ * @run main/othervm -Djdk.http.auth.tunneling.disabledSchemes=Basic, ProxyAuthTest fail
+ * @run main/othervm -Djdk.http.auth.tunneling.disabledSchemes=BAsIc ProxyAuthTest fail
+ * @run main/othervm -Djdk.http.auth.tunneling.disabledSchemes=Basic,Digest ProxyAuthTest fail
+ * @run main/othervm -Djdk.http.auth.tunneling.disabledSchemes=Unknown,bAsIc ProxyAuthTest fail
+ * @run main/othervm -Djdk.http.auth.tunneling.disabledSchemes= ProxyAuthTest succeed
+ * @run main/othervm -Djdk.http.auth.tunneling.disabledSchemes=Digest,NTLM,Negotiate ProxyAuthTest succeed
+ * @run main/othervm -Djdk.http.auth.tunneling.disabledSchemes=UNKNOWN,notKnown ProxyAuthTest succeed
  */
 
+// No way to reserve and restore java.lang.Authenticator, as well as read-once
+// system properties, so this tests needs to run in othervm mode.
+
 import java.io.*;
 import java.net.*;
 import java.security.KeyStore;
 import javax.net.*;
 import javax.net.ssl.*;
 import java.security.cert.*;
+import static java.nio.charset.StandardCharsets.US_ASCII;
 
 /*
  * ProxyAuthTest.java -- includes a simple server that can serve
@@ -75,7 +84,7 @@
          */
         public byte[] getBytes() {
             return "Proxy authentication for tunneling succeeded ..".
-                        getBytes();
+                        getBytes(US_ASCII);
         }
     }
 
@@ -83,6 +92,13 @@
      * Main method to create the server and the client
      */
     public static void main(String args[]) throws Exception {
+        boolean expectSuccess;
+        if (args[0].equals("succeed")) {
+            expectSuccess = true;
+        } else {
+            expectSuccess = false;
+        }
+
         String keyFilename =
             System.getProperty("test.src", "./") + "/" + pathToStores +
                 "/" + keyStoreFile;
@@ -99,12 +115,13 @@
         /*
          * setup the server
          */
+        Closeable server;
         try {
             ServerSocketFactory ssf =
                 ProxyAuthTest.getServerSocketFactory(useSSL);
             ServerSocket ss = ssf.createServerSocket(serverPort);
             serverPort = ss.getLocalPort();
-            new TestServer(ss);
+            server = new TestServer(ss);
         } catch (Exception e) {
             System.out.println("Server side failed:" +
                                 e.getMessage());
@@ -113,9 +130,27 @@
         // trigger the client
         try {
             doClientSide();
-        } catch (Exception e) {
-            System.out.println("Client side failed: " + e.getMessage());
-            throw e;
+            if (!expectSuccess) {
+                throw new RuntimeException(
+                        "Expected exception/failure to connect, but succeeded.");
+            }
+        } catch (IOException e) {
+            if (expectSuccess) {
+                System.out.println("Client side failed: " + e.getMessage());
+                throw e;
+            }
+
+            if (! (e.getMessage().contains("Unable to tunnel through proxy") &&
+                   e.getMessage().contains("407")) ) {
+                throw new RuntimeException(
+                        "Expected exception about cannot tunnel, 407, etc, but got", e);
+            } else {
+                // Informative
+                System.out.println("Caught expected exception: " + e.getMessage());
+            }
+        } finally {
+            if (server != null)
+                server.close();
         }
     }
 
@@ -145,11 +180,11 @@
         }
     }
 
-    static void doClientSide() throws Exception {
+    static void doClientSide() throws IOException {
         /*
          * setup up a proxy with authentication information
          */
-        setupProxy();
+        ProxyTunnelServer ps = setupProxy();
 
         /*
          * we want to avoid URLspoofCheck failures in cases where the cert
@@ -157,18 +192,28 @@
          */
         HttpsURLConnection.setDefaultHostnameVerifier(
                                       new NameVerifier());
+
+        InetSocketAddress paddr = new InetSocketAddress("localhost", ps.getPort());
+        Proxy proxy = new Proxy(Proxy.Type.HTTP, paddr);
+
         URL url = new URL("https://" + "localhost:" + serverPort
                                 + "/index.html");
         BufferedReader in = null;
+        HttpsURLConnection uc = (HttpsURLConnection) url.openConnection(proxy);
         try {
-            in = new BufferedReader(new InputStreamReader(
-                               url.openStream()));
+            in = new BufferedReader(new InputStreamReader(uc.getInputStream()));
             String inputLine;
             System.out.print("Client recieved from the server: ");
             while ((inputLine = in.readLine()) != null)
                 System.out.println(inputLine);
             in.close();
-        } catch (SSLException e) {
+        } catch (IOException e) {
+            // Assert that the error stream is not accessible from the failed
+            // tunnel setup.
+            if (uc.getErrorStream() != null) {
+                throw new RuntimeException("Unexpected error stream.");
+            }
+
             if (in != null)
                 in.close();
             throw e;
@@ -181,7 +226,7 @@
         }
     }
 
-    static void setupProxy() throws IOException {
+    static ProxyTunnelServer setupProxy() throws IOException {
         ProxyTunnelServer pserver = new ProxyTunnelServer();
         /*
          * register a system wide authenticator and setup the proxy for
@@ -194,9 +239,7 @@
         pserver.setUserAuth("Test", "test123");
 
         pserver.start();
-        System.setProperty("https.proxyHost", "localhost");
-        System.setProperty("https.proxyPort", String.valueOf(
-                                        pserver.getPort()));
+        return pserver;
     }
 
     public static class TestAuthenticator extends Authenticator {
--- a/jdk/test/sun/net/www/protocol/https/HttpsClient/ProxyTunnelServer.java	Fri Jul 15 20:57:41 2016 +0100
+++ b/jdk/test/sun/net/www/protocol/https/HttpsClient/ProxyTunnelServer.java	Mon Jul 18 08:28:48 2016 +0100
@@ -65,6 +65,7 @@
           ss = (ServerSocket) ServerSocketFactory.getDefault().
           createServerSocket(0);
         }
+        setDaemon(true);
     }
 
     public void needUserAuth(boolean auth) {
@@ -211,6 +212,7 @@
             this.sockOut = sockOut;
             input = sockIn.getInputStream();
             output = sockOut.getOutputStream();
+            setDaemon(true);
         }
 
         public void run() {
--- a/jdk/test/sun/net/www/protocol/https/HttpsURLConnection/PostThruProxyWithAuth.sh	Fri Jul 15 20:57:41 2016 +0100
+++ b/jdk/test/sun/net/www/protocol/https/HttpsURLConnection/PostThruProxyWithAuth.sh	Mon Jul 18 08:28:48 2016 +0100
@@ -57,5 +57,6 @@
     ${TESTSRC}${FS}ProxyTunnelServer.java \
     ${TESTSRC}${FS}PostThruProxyWithAuth.java
 ${TESTJAVA}${FS}bin${FS}java ${TESTVMOPTS} ${EXTRAOPTS} \
+    -Djdk.http.auth.tunneling.disabledSchemes= \
     PostThruProxyWithAuth ${HOSTNAME} ${TESTSRC}
 exit