| /* |
| * Copyright (c) 2015, 2016, 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 |
| */ |
| package java.net.http; |
| |
| import java.io.IOException; |
| import static java.net.Authenticator.RequestorType.PROXY; |
| import static java.net.Authenticator.RequestorType.SERVER; |
| import java.net.PasswordAuthentication; |
| import java.net.URI; |
| import java.net.InetSocketAddress; |
| import java.net.URISyntaxException; |
| import java.util.Base64; |
| import java.util.HashMap; |
| import java.util.LinkedList; |
| import static java.nio.charset.StandardCharsets.ISO_8859_1; |
| |
| /** |
| * Implementation of Http Basic authentication. |
| */ |
| class AuthenticationFilter implements HeaderFilter { |
| |
| static private final Base64.Encoder encoder = Base64.getEncoder(); |
| |
| static final int DEFAULT_RETRY_LIMIT = 3; |
| |
| static final int retry_limit = Utils.getIntegerNetProperty( |
| "sun.net.httpclient.auth.retrylimit", DEFAULT_RETRY_LIMIT); |
| |
| static final int UNAUTHORIZED = 401; |
| static final int PROXY_UNAUTHORIZED = 407; |
| |
| private PasswordAuthentication getCredentials(String header, |
| boolean proxy, |
| HttpRequestImpl req) |
| throws IOException |
| { |
| HttpClientImpl client = req.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; |
| |
| // needs to be instance method in Authenticator |
| return auth.requestPasswordAuthenticationInstance(uri.getHost(), |
| null, |
| uri.getPort(), |
| uri.getScheme(), |
| realm, |
| authscheme, |
| uri.toURL(), |
| rtype |
| ); |
| } |
| |
| 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, |
| null); |
| } catch (URISyntaxException e) { |
| throw new InternalError(e); |
| } |
| } |
| |
| @Override |
| public void request(HttpRequestImpl r) throws IOException { |
| // use preemptive authentication if an entry exists. |
| Cache cache = getCache(r); |
| |
| // Proxy |
| if (r.exchange.proxyauth == null) { |
| URI proxyURI = getProxyURI(r); |
| if (proxyURI != null) { |
| CacheEntry ca = cache.get(proxyURI, true); |
| if (ca != null) { |
| r.exchange.proxyauth = new AuthInfo(true, ca.scheme, null, ca); |
| addBasicCredentials(r, true, ca.value); |
| } |
| } |
| } |
| |
| // Server |
| if (r.exchange.serverauth == null) { |
| CacheEntry ca = cache.get(r.uri(), false); |
| if (ca != null) { |
| r.exchange.serverauth = new AuthInfo(true, ca.scheme, null, ca); |
| addBasicCredentials(r, false, ca.value); |
| } |
| } |
| } |
| |
| // TODO: refactor into per auth scheme class |
| static private 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; |
| 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; |
| } |
| } |
| |
| @Override |
| public HttpRequestImpl response(HttpResponseImpl r) throws IOException { |
| Cache cache = getCache(r.request); |
| 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 (req.exchange.serverauth != null && !req.exchange.serverauth.fromcache) { |
| AuthInfo au = req.exchange.serverauth; |
| cache.store(au.scheme, req.uri(), false, au.credentials); |
| } |
| if (req.exchange.proxyauth != null && !req.exchange.proxyauth.fromcache) { |
| AuthInfo au = req.exchange.proxyauth; |
| cache.store(au.scheme, req.uri(), false, au.credentials); |
| } |
| return null; |
| } |
| |
| boolean proxy = status == PROXY_UNAUTHORIZED; |
| String authname = proxy ? "Proxy-Authentication" : "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 |
| } |
| |
| String realm = parser.findValue("realm"); |
| AuthInfo au = proxy ? req.exchange.proxyauth : req.exchange.serverauth; |
| if (au == 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) |
| req.exchange.proxyauth = au; |
| else |
| req.exchange.serverauth = au; |
| addBasicCredentials(req, proxy, pw); |
| return req; |
| } else if (au.retries > retry_limit) { |
| throw new IOException("too many authentication attempts"); |
| } else { |
| // we sent credentials, but they were rejected |
| if (au.fromcache) { |
| cache.remove(au.cacheEntry); |
| } |
| // try again |
| au.credentials = getCredentials(authval, proxy, req); |
| addBasicCredentials(req, proxy, au.credentials); |
| au.retries++; |
| return req; |
| } |
| } |
| |
| static final HashMap<HttpClientImpl,Cache> caches = new HashMap<>(); |
| |
| static synchronized Cache getCache(HttpRequestImpl req) { |
| HttpClientImpl client = req.client(); |
| Cache c = caches.get(client); |
| if (c == null) { |
| c = new Cache(); |
| caches.put(client, c); |
| } |
| return c; |
| } |
| |
| static class Cache { |
| final LinkedList<CacheEntry> entries = new LinkedList<>(); |
| |
| 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 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 = uri.resolve(".").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 = uri.toString(); |
| return other.startsWith(root); |
| } |
| } |
| } |