1/* 2 * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26package jdk.incubator.http; 27 28import java.io.IOException; 29import java.net.PasswordAuthentication; 30import java.net.URI; 31import java.net.InetSocketAddress; 32import java.net.URISyntaxException; 33import java.util.Base64; 34import java.util.LinkedList; 35import java.util.Objects; 36import java.util.WeakHashMap; 37import jdk.incubator.http.internal.common.Utils; 38import static java.net.Authenticator.RequestorType.PROXY; 39import static java.net.Authenticator.RequestorType.SERVER; 40import static java.nio.charset.StandardCharsets.ISO_8859_1; 41 42/** 43 * Implementation of Http Basic authentication. 44 */ 45class AuthenticationFilter implements HeaderFilter { 46 volatile MultiExchange<?,?> exchange; 47 private static final Base64.Encoder encoder = Base64.getEncoder(); 48 49 static final int DEFAULT_RETRY_LIMIT = 3; 50 51 static final int retry_limit = Utils.getIntegerNetProperty( 52 "jdk.httpclient.auth.retrylimit", DEFAULT_RETRY_LIMIT); 53 54 static final int UNAUTHORIZED = 401; 55 static final int PROXY_UNAUTHORIZED = 407; 56 57 private PasswordAuthentication getCredentials(String header, 58 boolean proxy, 59 HttpRequestImpl req) 60 throws IOException 61 { 62 HttpClientImpl client = exchange.client(); 63 java.net.Authenticator auth = 64 client.authenticator() 65 .orElseThrow(() -> new IOException("No authenticator set")); 66 URI uri = req.uri(); 67 HeaderParser parser = new HeaderParser(header); 68 String authscheme = parser.findKey(0); 69 70 String realm = parser.findValue("realm"); 71 java.net.Authenticator.RequestorType rtype = proxy ? PROXY : SERVER; 72 73 // needs to be instance method in Authenticator 74 return auth.requestPasswordAuthenticationInstance(uri.getHost(), 75 null, 76 uri.getPort(), 77 uri.getScheme(), 78 realm, 79 authscheme, 80 uri.toURL(), 81 rtype 82 ); 83 } 84 85 private URI getProxyURI(HttpRequestImpl r) { 86 InetSocketAddress proxy = r.proxy(exchange.client()); 87 if (proxy == null) { 88 return null; 89 } 90 91 // our own private scheme for proxy URLs 92 // eg. proxy.http://host:port/ 93 String scheme = "proxy." + r.uri().getScheme(); 94 try { 95 return new URI(scheme, 96 null, 97 proxy.getHostString(), 98 proxy.getPort(), 99 null, 100 null, 101 null); 102 } catch (URISyntaxException e) { 103 throw new InternalError(e); 104 } 105 } 106 107 @Override 108 public void request(HttpRequestImpl r, MultiExchange<?,?> e) throws IOException { 109 // use preemptive authentication if an entry exists. 110 Cache cache = getCache(e); 111 this.exchange = e; 112 113 // Proxy 114 if (exchange.proxyauth == null) { 115 URI proxyURI = getProxyURI(r); 116 if (proxyURI != null) { 117 CacheEntry ca = cache.get(proxyURI, true); 118 if (ca != null) { 119 exchange.proxyauth = new AuthInfo(true, ca.scheme, null, ca); 120 addBasicCredentials(r, true, ca.value); 121 } 122 } 123 } 124 125 // Server 126 if (exchange.serverauth == null) { 127 CacheEntry ca = cache.get(r.uri(), false); 128 if (ca != null) { 129 exchange.serverauth = new AuthInfo(true, ca.scheme, null, ca); 130 addBasicCredentials(r, false, ca.value); 131 } 132 } 133 } 134 135 // TODO: refactor into per auth scheme class 136 private static void addBasicCredentials(HttpRequestImpl r, 137 boolean proxy, 138 PasswordAuthentication pw) { 139 String hdrname = proxy ? "Proxy-Authorization" : "Authorization"; 140 StringBuilder sb = new StringBuilder(128); 141 sb.append(pw.getUserName()).append(':').append(pw.getPassword()); 142 String s = encoder.encodeToString(sb.toString().getBytes(ISO_8859_1)); 143 String value = "Basic " + s; 144 r.setSystemHeader(hdrname, value); 145 } 146 147 // Information attached to a HttpRequestImpl relating to authentication 148 static class AuthInfo { 149 final boolean fromcache; 150 final String scheme; 151 int retries; 152 PasswordAuthentication credentials; // used in request 153 CacheEntry cacheEntry; // if used 154 155 AuthInfo(boolean fromcache, 156 String scheme, 157 PasswordAuthentication credentials) { 158 this.fromcache = fromcache; 159 this.scheme = scheme; 160 this.credentials = credentials; 161 this.retries = 1; 162 } 163 164 AuthInfo(boolean fromcache, 165 String scheme, 166 PasswordAuthentication credentials, 167 CacheEntry ca) { 168 this(fromcache, scheme, credentials); 169 assert credentials == null || (ca != null && ca.value == null); 170 cacheEntry = ca; 171 } 172 173 AuthInfo retryWithCredentials(PasswordAuthentication pw) { 174 // If the info was already in the cache we need to create a new 175 // instance with fromCache==false so that it's put back in the 176 // cache if authentication succeeds 177 AuthInfo res = fromcache ? new AuthInfo(false, scheme, pw) : this; 178 res.credentials = Objects.requireNonNull(pw); 179 res.retries = retries; 180 return res; 181 } 182 183 } 184 185 @Override 186 public HttpRequestImpl response(Response r) throws IOException { 187 Cache cache = getCache(exchange); 188 int status = r.statusCode(); 189 HttpHeaders hdrs = r.headers(); 190 HttpRequestImpl req = r.request(); 191 192 if (status != UNAUTHORIZED && status != PROXY_UNAUTHORIZED) { 193 // check if any authentication succeeded for first time 194 if (exchange.serverauth != null && !exchange.serverauth.fromcache) { 195 AuthInfo au = exchange.serverauth; 196 cache.store(au.scheme, req.uri(), false, au.credentials); 197 } 198 if (exchange.proxyauth != null && !exchange.proxyauth.fromcache) { 199 AuthInfo au = exchange.proxyauth; 200 cache.store(au.scheme, req.uri(), false, au.credentials); 201 } 202 return null; 203 } 204 205 boolean proxy = status == PROXY_UNAUTHORIZED; 206 String authname = proxy ? "Proxy-Authenticate" : "WWW-Authenticate"; 207 String authval = hdrs.firstValue(authname).orElseThrow(() -> { 208 return new IOException("Invalid auth header"); 209 }); 210 HeaderParser parser = new HeaderParser(authval); 211 String scheme = parser.findKey(0); 212 213 // TODO: Need to generalise from Basic only. Delegate to a provider class etc. 214 215 if (!scheme.equalsIgnoreCase("Basic")) { 216 return null; // error gets returned to app 217 } 218 219 String realm = parser.findValue("realm"); 220 AuthInfo au = proxy ? exchange.proxyauth : exchange.serverauth; 221 if (au == null) { 222 PasswordAuthentication pw = getCredentials(authval, proxy, req); 223 if (pw == null) { 224 throw new IOException("No credentials provided"); 225 } 226 // No authentication in request. Get credentials from user 227 au = new AuthInfo(false, "Basic", pw); 228 if (proxy) { 229 exchange.proxyauth = au; 230 } else { 231 exchange.serverauth = au; 232 } 233 addBasicCredentials(req, proxy, pw); 234 return req; 235 } else if (au.retries > retry_limit) { 236 throw new IOException("too many authentication attempts. Limit: " + 237 Integer.toString(retry_limit)); 238 } else { 239 // we sent credentials, but they were rejected 240 if (au.fromcache) { 241 cache.remove(au.cacheEntry); 242 } 243 // try again 244 PasswordAuthentication pw = getCredentials(authval, proxy, req); 245 if (pw == null) { 246 throw new IOException("No credentials provided"); 247 } 248 au = au.retryWithCredentials(pw); 249 if (proxy) { 250 exchange.proxyauth = au; 251 } else { 252 exchange.serverauth = au; 253 } 254 addBasicCredentials(req, proxy, au.credentials); 255 au.retries++; 256 return req; 257 } 258 } 259 260 // Use a WeakHashMap to make it possible for the HttpClient to 261 // be garbaged collected when no longer referenced. 262 static final WeakHashMap<HttpClientImpl,Cache> caches = new WeakHashMap<>(); 263 264 static synchronized Cache getCache(MultiExchange<?,?> exchange) { 265 HttpClientImpl client = exchange.client(); 266 Cache c = caches.get(client); 267 if (c == null) { 268 c = new Cache(); 269 caches.put(client, c); 270 } 271 return c; 272 } 273 274 // Note: Make sure that Cache and CacheEntry do not keep any strong 275 // reference to the HttpClient: it would prevent the client being 276 // GC'ed when no longer referenced. 277 static class Cache { 278 final LinkedList<CacheEntry> entries = new LinkedList<>(); 279 280 synchronized CacheEntry get(URI uri, boolean proxy) { 281 for (CacheEntry entry : entries) { 282 if (entry.equalsKey(uri, proxy)) { 283 return entry; 284 } 285 } 286 return null; 287 } 288 289 synchronized void remove(String authscheme, URI domain, boolean proxy) { 290 for (CacheEntry entry : entries) { 291 if (entry.equalsKey(domain, proxy)) { 292 entries.remove(entry); 293 } 294 } 295 } 296 297 synchronized void remove(CacheEntry entry) { 298 entries.remove(entry); 299 } 300 301 synchronized void store(String authscheme, 302 URI domain, 303 boolean proxy, 304 PasswordAuthentication value) { 305 remove(authscheme, domain, proxy); 306 entries.add(new CacheEntry(authscheme, domain, proxy, value)); 307 } 308 } 309 310 static class CacheEntry { 311 final String root; 312 final String scheme; 313 final boolean proxy; 314 final PasswordAuthentication value; 315 316 CacheEntry(String authscheme, 317 URI uri, 318 boolean proxy, 319 PasswordAuthentication value) { 320 this.scheme = authscheme; 321 this.root = uri.resolve(".").toString(); // remove extraneous components 322 this.proxy = proxy; 323 this.value = value; 324 } 325 326 public PasswordAuthentication value() { 327 return value; 328 } 329 330 public boolean equalsKey(URI uri, boolean proxy) { 331 if (this.proxy != proxy) { 332 return false; 333 } 334 String other = uri.toString(); 335 return other.startsWith(root); 336 } 337 } 338} 339