diff -r f7fd051519ac -r ee6f7a61f3a5 src/java.net.http/share/classes/jdk/internal/net/http/AuthenticationFilter.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.net.http/share/classes/jdk/internal/net/http/AuthenticationFilter.java Tue Apr 17 08:54:17 2018 -0700 @@ -0,0 +1,440 @@ +/* + * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.internal.net.http; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.PasswordAuthentication; +import java.net.URI; +import java.net.InetSocketAddress; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Base64; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.WeakHashMap; +import java.net.http.HttpHeaders; +import jdk.internal.net.http.common.Log; +import jdk.internal.net.http.common.Utils; +import static java.net.Authenticator.RequestorType.PROXY; +import static java.net.Authenticator.RequestorType.SERVER; +import static java.nio.charset.StandardCharsets.ISO_8859_1; + +/** + * Implementation of Http Basic authentication. + */ +class AuthenticationFilter implements HeaderFilter { + volatile MultiExchange exchange; + private static final Base64.Encoder encoder = Base64.getEncoder(); + + static final int DEFAULT_RETRY_LIMIT = 3; + + static final int retry_limit = Utils.getIntegerNetProperty( + "jdk.httpclient.auth.retrylimit", DEFAULT_RETRY_LIMIT); + + static final int UNAUTHORIZED = 401; + static final int PROXY_UNAUTHORIZED = 407; + + private static final List BASIC_DUMMY = + List.of("Basic " + Base64.getEncoder() + .encodeToString("o:o".getBytes(ISO_8859_1))); + + // A public no-arg constructor is required by FilterFactory + public AuthenticationFilter() {} + + private PasswordAuthentication getCredentials(String header, + boolean proxy, + HttpRequestImpl req) + throws IOException + { + HttpClientImpl client = exchange.client(); + java.net.Authenticator auth = + client.authenticator() + .orElseThrow(() -> new IOException("No authenticator set")); + URI uri = req.uri(); + HeaderParser parser = new HeaderParser(header); + String authscheme = parser.findKey(0); + + String realm = parser.findValue("realm"); + java.net.Authenticator.RequestorType rtype = proxy ? PROXY : SERVER; + URL url = toURL(uri, req.method(), proxy); + String host; + int port; + String protocol; + InetSocketAddress proxyAddress; + if (proxy && (proxyAddress = req.proxy()) != null) { + // request sent to server through proxy + proxyAddress = req.proxy(); + host = proxyAddress.getHostString(); + port = proxyAddress.getPort(); + protocol = "http"; // we don't support https connection to proxy + } else { + // direct connection to server or proxy + host = uri.getHost(); + port = uri.getPort(); + protocol = uri.getScheme(); + } + + // needs to be instance method in Authenticator + return auth.requestPasswordAuthenticationInstance(host, + null, + port, + protocol, + realm, + authscheme, + url, + rtype + ); + } + + private URL toURL(URI uri, String method, boolean proxy) + throws MalformedURLException + { + if (proxy && "CONNECT".equalsIgnoreCase(method) + && "socket".equalsIgnoreCase(uri.getScheme())) { + return null; // proxy tunneling + } + return uri.toURL(); + } + + private URI getProxyURI(HttpRequestImpl r) { + InetSocketAddress proxy = r.proxy(); + if (proxy == null) { + return null; + } + + // our own private scheme for proxy URLs + // eg. proxy.http://host:port/ + String scheme = "proxy." + r.uri().getScheme(); + try { + return new URI(scheme, + null, + proxy.getHostString(), + proxy.getPort(), + "/", + null, + null); + } catch (URISyntaxException e) { + throw new InternalError(e); + } + } + + @Override + public void request(HttpRequestImpl r, MultiExchange e) throws IOException { + // use preemptive authentication if an entry exists. + Cache cache = getCache(e); + this.exchange = e; + + // Proxy + if (exchange.proxyauth == null) { + URI proxyURI = getProxyURI(r); + if (proxyURI != null) { + CacheEntry ca = cache.get(proxyURI, true); + if (ca != null) { + exchange.proxyauth = new AuthInfo(true, ca.scheme, null, ca); + addBasicCredentials(r, true, ca.value); + } + } + } + + // Server + if (exchange.serverauth == null) { + CacheEntry ca = cache.get(r.uri(), false); + if (ca != null) { + exchange.serverauth = new AuthInfo(true, ca.scheme, null, ca); + addBasicCredentials(r, false, ca.value); + } + } + } + + // TODO: refactor into per auth scheme class + private static void addBasicCredentials(HttpRequestImpl r, + boolean proxy, + PasswordAuthentication pw) { + String hdrname = proxy ? "Proxy-Authorization" : "Authorization"; + StringBuilder sb = new StringBuilder(128); + sb.append(pw.getUserName()).append(':').append(pw.getPassword()); + String s = encoder.encodeToString(sb.toString().getBytes(ISO_8859_1)); + String value = "Basic " + s; + if (proxy) { + if (r.isConnect()) { + if (!Utils.PROXY_TUNNEL_FILTER + .test(hdrname, List.of(value))) { + Log.logError("{0} disabled", hdrname); + return; + } + } else if (r.proxy() != null) { + if (!Utils.PROXY_FILTER + .test(hdrname, List.of(value))) { + Log.logError("{0} disabled", hdrname); + return; + } + } + } + r.setSystemHeader(hdrname, value); + } + + // Information attached to a HttpRequestImpl relating to authentication + static class AuthInfo { + final boolean fromcache; + final String scheme; + int retries; + PasswordAuthentication credentials; // used in request + CacheEntry cacheEntry; // if used + + AuthInfo(boolean fromcache, + String scheme, + PasswordAuthentication credentials) { + this.fromcache = fromcache; + this.scheme = scheme; + this.credentials = credentials; + this.retries = 1; + } + + AuthInfo(boolean fromcache, + String scheme, + PasswordAuthentication credentials, + CacheEntry ca) { + this(fromcache, scheme, credentials); + assert credentials == null || (ca != null && ca.value == null); + cacheEntry = ca; + } + + AuthInfo retryWithCredentials(PasswordAuthentication pw) { + // If the info was already in the cache we need to create a new + // instance with fromCache==false so that it's put back in the + // cache if authentication succeeds + AuthInfo res = fromcache ? new AuthInfo(false, scheme, pw) : this; + res.credentials = Objects.requireNonNull(pw); + res.retries = retries; + return res; + } + + } + + @Override + public HttpRequestImpl response(Response r) throws IOException { + Cache cache = getCache(exchange); + int status = r.statusCode(); + HttpHeaders hdrs = r.headers(); + HttpRequestImpl req = r.request(); + + if (status != UNAUTHORIZED && status != PROXY_UNAUTHORIZED) { + // check if any authentication succeeded for first time + if (exchange.serverauth != null && !exchange.serverauth.fromcache) { + AuthInfo au = exchange.serverauth; + cache.store(au.scheme, req.uri(), false, au.credentials); + } + if (exchange.proxyauth != null && !exchange.proxyauth.fromcache) { + AuthInfo au = exchange.proxyauth; + URI proxyURI = getProxyURI(req); + if (proxyURI != null) { + cache.store(au.scheme, proxyURI, true, au.credentials); + } + } + return null; + } + + boolean proxy = status == PROXY_UNAUTHORIZED; + String authname = proxy ? "Proxy-Authenticate" : "WWW-Authenticate"; + String authval = hdrs.firstValue(authname).orElseThrow(() -> { + return new IOException("Invalid auth header"); + }); + HeaderParser parser = new HeaderParser(authval); + String scheme = parser.findKey(0); + + // TODO: Need to generalise from Basic only. Delegate to a provider class etc. + + if (!scheme.equalsIgnoreCase("Basic")) { + return null; // error gets returned to app + } + + if (proxy) { + if (r.isConnectResponse) { + if (!Utils.PROXY_TUNNEL_FILTER + .test("Proxy-Authorization", BASIC_DUMMY)) { + Log.logError("{0} disabled", "Proxy-Authorization"); + return null; + } + } else if (req.proxy() != null) { + if (!Utils.PROXY_FILTER + .test("Proxy-Authorization", BASIC_DUMMY)) { + Log.logError("{0} disabled", "Proxy-Authorization"); + return null; + } + } + } + + AuthInfo au = proxy ? exchange.proxyauth : exchange.serverauth; + if (au == null) { + // if no authenticator, let the user deal with 407/401 + if (!exchange.client().authenticator().isPresent()) return null; + + PasswordAuthentication pw = getCredentials(authval, proxy, req); + if (pw == null) { + throw new IOException("No credentials provided"); + } + // No authentication in request. Get credentials from user + au = new AuthInfo(false, "Basic", pw); + if (proxy) { + exchange.proxyauth = au; + } else { + exchange.serverauth = au; + } + req = HttpRequestImpl.newInstanceForAuthentication(req); + addBasicCredentials(req, proxy, pw); + return req; + } else if (au.retries > retry_limit) { + throw new IOException("too many authentication attempts. Limit: " + + Integer.toString(retry_limit)); + } else { + // we sent credentials, but they were rejected + if (au.fromcache) { + cache.remove(au.cacheEntry); + } + + // if no authenticator, let the user deal with 407/401 + if (!exchange.client().authenticator().isPresent()) return null; + + // try again + PasswordAuthentication pw = getCredentials(authval, proxy, req); + if (pw == null) { + throw new IOException("No credentials provided"); + } + au = au.retryWithCredentials(pw); + if (proxy) { + exchange.proxyauth = au; + } else { + exchange.serverauth = au; + } + req = HttpRequestImpl.newInstanceForAuthentication(req); + addBasicCredentials(req, proxy, au.credentials); + au.retries++; + return req; + } + } + + // Use a WeakHashMap to make it possible for the HttpClient to + // be garbage collected when no longer referenced. + static final WeakHashMap caches = new WeakHashMap<>(); + + static synchronized Cache getCache(MultiExchange exchange) { + HttpClientImpl client = exchange.client(); + Cache c = caches.get(client); + if (c == null) { + c = new Cache(); + caches.put(client, c); + } + return c; + } + + // Note: Make sure that Cache and CacheEntry do not keep any strong + // reference to the HttpClient: it would prevent the client being + // GC'ed when no longer referenced. + static final class Cache { + final LinkedList entries = new LinkedList<>(); + + Cache() {} + + synchronized CacheEntry get(URI uri, boolean proxy) { + for (CacheEntry entry : entries) { + if (entry.equalsKey(uri, proxy)) { + return entry; + } + } + return null; + } + + synchronized void remove(String authscheme, URI domain, boolean proxy) { + for (CacheEntry entry : entries) { + if (entry.equalsKey(domain, proxy)) { + entries.remove(entry); + } + } + } + + synchronized void remove(CacheEntry entry) { + entries.remove(entry); + } + + synchronized void store(String authscheme, + URI domain, + boolean proxy, + PasswordAuthentication value) { + remove(authscheme, domain, proxy); + entries.add(new CacheEntry(authscheme, domain, proxy, value)); + } + } + + static URI normalize(URI uri, boolean isPrimaryKey) { + String path = uri.getPath(); + if (path == null || path.isEmpty()) { + // make sure the URI has a path, ignore query and fragment + try { + return new URI(uri.getScheme(), uri.getAuthority(), "/", null, null); + } catch (URISyntaxException e) { + throw new InternalError(e); + } + } else if (isPrimaryKey || !"/".equals(path)) { + // remove extraneous components and normalize path + return uri.resolve("."); + } else { + // path == "/" and the URI is not used to store + // the primary key in the cache: nothing to do. + return uri; + } + } + + static final class CacheEntry { + final String root; + final String scheme; + final boolean proxy; + final PasswordAuthentication value; + + CacheEntry(String authscheme, + URI uri, + boolean proxy, + PasswordAuthentication value) { + this.scheme = authscheme; + this.root = normalize(uri, true).toString(); // remove extraneous components + this.proxy = proxy; + this.value = value; + } + + public PasswordAuthentication value() { + return value; + } + + public boolean equalsKey(URI uri, boolean proxy) { + if (this.proxy != proxy) { + return false; + } + String other = String.valueOf(normalize(uri, false)); + return other.startsWith(root); + } + } +}