| /* |
| * Copyright (c) 2015, 2020, 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.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 String BASIC_DUMMY = |
| "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, value)) { |
| Log.logError("{0} disabled", hdrname); |
| return; |
| } |
| } else if (r.proxy() != null) { |
| if (!Utils.PROXY_FILTER.test(hdrname, 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 != PROXY_UNAUTHORIZED) { |
| if (exchange.proxyauth != null && !exchange.proxyauth.fromcache) { |
| AuthInfo au = exchange.proxyauth; |
| URI proxyURI = getProxyURI(req); |
| if (proxyURI != null) { |
| exchange.proxyauth = null; |
| cache.store(au.scheme, proxyURI, true, au.credentials); |
| } |
| } |
| if (status != 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); |
| } |
| return null; |
| } |
| } |
| |
| boolean proxy = status == PROXY_UNAUTHORIZED; |
| String authname = proxy ? "Proxy-Authenticate" : "WWW-Authenticate"; |
| String authval = hdrs.firstValue(authname).orElse(null); |
| if (authval == null) { |
| if (exchange.client().authenticator().isPresent()) { |
| throw new IOException(authname + " header missing for response code " + status); |
| } else { |
| // No authenticator? let the caller deal with this. |
| return null; |
| } |
| } |
| |
| 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<HttpClientImpl,Cache> 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<CacheEntry> 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); |
| } |
| } |
| } |