1 /* 2 * Copyright (c) 2015, 2018, 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 26 package jdk.internal.net.http; 27 28 import java.io.IOException; 29 import java.net.MalformedURLException; 30 import java.net.PasswordAuthentication; 31 import java.net.URI; 32 import java.net.InetSocketAddress; 33 import java.net.URISyntaxException; 34 import java.net.URL; 35 import java.util.Base64; 36 import java.util.LinkedList; 37 import java.util.Objects; 38 import java.util.WeakHashMap; 39 import java.net.http.HttpHeaders; 40 import jdk.internal.net.http.common.Log; 41 import jdk.internal.net.http.common.Utils; 42 import static java.net.Authenticator.RequestorType.PROXY; 43 import static java.net.Authenticator.RequestorType.SERVER; 44 import static java.nio.charset.StandardCharsets.ISO_8859_1; 45 46 /** 47 * Implementation of Http Basic authentication. 48 */ 49 class AuthenticationFilter implements HeaderFilter { 50 volatile MultiExchange<?> exchange; 51 private static final Base64.Encoder encoder = Base64.getEncoder(); 52 53 static final int DEFAULT_RETRY_LIMIT = 3; 54 55 static final int retry_limit = Utils.getIntegerNetProperty( 56 "jdk.httpclient.auth.retrylimit", DEFAULT_RETRY_LIMIT); 57 58 static final int UNAUTHORIZED = 401; 59 static final int PROXY_UNAUTHORIZED = 407; 60 61 private static final String BASIC_DUMMY = 62 "Basic " + Base64.getEncoder() 63 .encodeToString("o:o".getBytes(ISO_8859_1)); 64 65 // A public no-arg constructor is required by FilterFactory 66 public AuthenticationFilter() {} 67 68 private PasswordAuthentication getCredentials(String header, 69 boolean proxy, 70 HttpRequestImpl req) 71 throws IOException 72 { 73 HttpClientImpl client = exchange.client(); 74 java.net.Authenticator auth = 75 client.authenticator() 76 .orElseThrow(() -> new IOException("No authenticator set")); 77 URI uri = req.uri(); 78 HeaderParser parser = new HeaderParser(header); 79 String authscheme = parser.findKey(0); 80 81 String realm = parser.findValue("realm"); 82 java.net.Authenticator.RequestorType rtype = proxy ? PROXY : SERVER; 83 URL url = toURL(uri, req.method(), proxy); 84 String host; 85 int port; 86 String protocol; 87 InetSocketAddress proxyAddress; 88 if (proxy && (proxyAddress = req.proxy()) != null) { 89 // request sent to server through proxy 90 proxyAddress = req.proxy(); 91 host = proxyAddress.getHostString(); 92 port = proxyAddress.getPort(); 93 protocol = "http"; // we don't support https connection to proxy 94 } else { 95 // direct connection to server or proxy 96 host = uri.getHost(); 97 port = uri.getPort(); 98 protocol = uri.getScheme(); 99 } 100 101 // needs to be instance method in Authenticator 102 return auth.requestPasswordAuthenticationInstance(host, 103 null, 104 port, 105 protocol, 106 realm, 107 authscheme, 108 url, 109 rtype 110 ); 111 } 112 113 private URL toURL(URI uri, String method, boolean proxy) 114 throws MalformedURLException 115 { 116 if (proxy && "CONNECT".equalsIgnoreCase(method) 117 && "socket".equalsIgnoreCase(uri.getScheme())) { 118 return null; // proxy tunneling 119 } 120 return uri.toURL(); 121 } 122 123 private URI getProxyURI(HttpRequestImpl r) { 124 InetSocketAddress proxy = r.proxy(); 125 if (proxy == null) { 126 return null; 127 } 128 129 // our own private scheme for proxy URLs 130 // eg. proxy.http://host:port/ 131 String scheme = "proxy." + r.uri().getScheme(); 132 try { 133 return new URI(scheme, 134 null, 135 proxy.getHostString(), 136 proxy.getPort(), 137 "/", 138 null, 139 null); 140 } catch (URISyntaxException e) { 141 throw new InternalError(e); 142 } 143 } 144 145 @Override 146 public void request(HttpRequestImpl r, MultiExchange<?> e) throws IOException { 147 // use preemptive authentication if an entry exists. 148 Cache cache = getCache(e); 149 this.exchange = e; 150 151 // Proxy 152 if (exchange.proxyauth == null) { 153 URI proxyURI = getProxyURI(r); 154 if (proxyURI != null) { 155 CacheEntry ca = cache.get(proxyURI, true); 156 if (ca != null) { 157 exchange.proxyauth = new AuthInfo(true, ca.scheme, null, ca); 158 addBasicCredentials(r, true, ca.value); 159 } 160 } 161 } 162 163 // Server 164 if (exchange.serverauth == null) { 165 CacheEntry ca = cache.get(r.uri(), false); 166 if (ca != null) { 167 exchange.serverauth = new AuthInfo(true, ca.scheme, null, ca); 168 addBasicCredentials(r, false, ca.value); 169 } 170 } 171 } 172 173 // TODO: refactor into per auth scheme class 174 private static void addBasicCredentials(HttpRequestImpl r, 175 boolean proxy, 176 PasswordAuthentication pw) { 177 String hdrname = proxy ? "Proxy-Authorization" : "Authorization"; 178 StringBuilder sb = new StringBuilder(128); 179 sb.append(pw.getUserName()).append(':').append(pw.getPassword()); 180 String s = encoder.encodeToString(sb.toString().getBytes(ISO_8859_1)); 181 String value = "Basic " + s; 182 if (proxy) { 183 if (r.isConnect()) { 184 if (!Utils.PROXY_TUNNEL_FILTER.test(hdrname, value)) { 185 Log.logError("{0} disabled", hdrname); 186 return; 187 } 188 } else if (r.proxy() != null) { 189 if (!Utils.PROXY_FILTER.test(hdrname, value)) { 190 Log.logError("{0} disabled", hdrname); 191 return; 192 } 193 } 194 } 195 r.setSystemHeader(hdrname, value); 196 } 197 198 // Information attached to a HttpRequestImpl relating to authentication 199 static class AuthInfo { 200 final boolean fromcache; 201 final String scheme; 202 int retries; 203 PasswordAuthentication credentials; // used in request 204 CacheEntry cacheEntry; // if used 205 206 AuthInfo(boolean fromcache, 207 String scheme, 208 PasswordAuthentication credentials) { 209 this.fromcache = fromcache; 210 this.scheme = scheme; 211 this.credentials = credentials; 212 this.retries = 1; 213 } 214 215 AuthInfo(boolean fromcache, 216 String scheme, 217 PasswordAuthentication credentials, 218 CacheEntry ca) { 219 this(fromcache, scheme, credentials); 220 assert credentials == null || (ca != null && ca.value == null); 221 cacheEntry = ca; 222 } 223 224 AuthInfo retryWithCredentials(PasswordAuthentication pw) { 225 // If the info was already in the cache we need to create a new 226 // instance with fromCache==false so that it's put back in the 227 // cache if authentication succeeds 228 AuthInfo res = fromcache ? new AuthInfo(false, scheme, pw) : this; 229 res.credentials = Objects.requireNonNull(pw); 230 res.retries = retries; 231 return res; 232 } 233 234 } 235 236 @Override 237 public HttpRequestImpl response(Response r) throws IOException { 238 Cache cache = getCache(exchange); 239 int status = r.statusCode(); 240 HttpHeaders hdrs = r.headers(); 241 HttpRequestImpl req = r.request(); 242 243 if (status != UNAUTHORIZED && status != PROXY_UNAUTHORIZED) { 244 // check if any authentication succeeded for first time 245 if (exchange.serverauth != null && !exchange.serverauth.fromcache) { 246 AuthInfo au = exchange.serverauth; 247 cache.store(au.scheme, req.uri(), false, au.credentials); 248 } 249 if (exchange.proxyauth != null && !exchange.proxyauth.fromcache) { 250 AuthInfo au = exchange.proxyauth; 251 URI proxyURI = getProxyURI(req); 252 if (proxyURI != null) { 253 cache.store(au.scheme, proxyURI, true, au.credentials); 254 } 255 } 256 return null; 257 } 258 259 boolean proxy = status == PROXY_UNAUTHORIZED; 260 String authname = proxy ? "Proxy-Authenticate" : "WWW-Authenticate"; 261 String authval = hdrs.firstValue(authname).orElse(null); 262 if (authval == null) { 263 if (exchange.client().authenticator().isPresent()) { 264 throw new IOException(authname + " header missing for response code " + status); 265 } else { 266 // No authenticator? let the caller deal with this. 267 return null; 268 } 269 } 270 271 HeaderParser parser = new HeaderParser(authval); 272 String scheme = parser.findKey(0); 273 274 // TODO: Need to generalise from Basic only. Delegate to a provider class etc. 275 276 if (!scheme.equalsIgnoreCase("Basic")) { 277 return null; // error gets returned to app 278 } 279 280 if (proxy) { 281 if (r.isConnectResponse) { 282 if (!Utils.PROXY_TUNNEL_FILTER 283 .test("Proxy-Authorization", BASIC_DUMMY)) { 284 Log.logError("{0} disabled", "Proxy-Authorization"); 285 return null; 286 } 287 } else if (req.proxy() != null) { 288 if (!Utils.PROXY_FILTER 289 .test("Proxy-Authorization", BASIC_DUMMY)) { 290 Log.logError("{0} disabled", "Proxy-Authorization"); 291 return null; 292 } 293 } 294 } 295 296 AuthInfo au = proxy ? exchange.proxyauth : exchange.serverauth; 297 if (au == null) { 298 // if no authenticator, let the user deal with 407/401 299 if (!exchange.client().authenticator().isPresent()) return null; 300 301 PasswordAuthentication pw = getCredentials(authval, proxy, req); 302 if (pw == null) { 303 throw new IOException("No credentials provided"); 304 } 305 // No authentication in request. Get credentials from user 306 au = new AuthInfo(false, "Basic", pw); 307 if (proxy) { 308 exchange.proxyauth = au; 309 } else { 310 exchange.serverauth = au; 311 } 312 req = HttpRequestImpl.newInstanceForAuthentication(req); 313 addBasicCredentials(req, proxy, pw); 314 return req; 315 } else if (au.retries > retry_limit) { 316 throw new IOException("too many authentication attempts. Limit: " + 317 Integer.toString(retry_limit)); 318 } else { 319 // we sent credentials, but they were rejected 320 if (au.fromcache) { 321 cache.remove(au.cacheEntry); 322 } 323 324 // if no authenticator, let the user deal with 407/401 325 if (!exchange.client().authenticator().isPresent()) return null; 326 327 // try again 328 PasswordAuthentication pw = getCredentials(authval, proxy, req); 329 if (pw == null) { 330 throw new IOException("No credentials provided"); 331 } 332 au = au.retryWithCredentials(pw); 333 if (proxy) { 334 exchange.proxyauth = au; 335 } else { 336 exchange.serverauth = au; 337 } 338 req = HttpRequestImpl.newInstanceForAuthentication(req); 339 addBasicCredentials(req, proxy, au.credentials); 340 au.retries++; 341 return req; 342 } 343 } 344 345 // Use a WeakHashMap to make it possible for the HttpClient to 346 // be garbage collected when no longer referenced. 347 static final WeakHashMap<HttpClientImpl,Cache> caches = new WeakHashMap<>(); 348 349 static synchronized Cache getCache(MultiExchange<?> exchange) { 350 HttpClientImpl client = exchange.client(); 351 Cache c = caches.get(client); 352 if (c == null) { 353 c = new Cache(); 354 caches.put(client, c); 355 } 356 return c; 357 } 358 359 // Note: Make sure that Cache and CacheEntry do not keep any strong 360 // reference to the HttpClient: it would prevent the client being 361 // GC'ed when no longer referenced. 362 static final class Cache { 363 final LinkedList<CacheEntry> entries = new LinkedList<>(); 364 365 Cache() {} 366 367 synchronized CacheEntry get(URI uri, boolean proxy) { 368 for (CacheEntry entry : entries) { 369 if (entry.equalsKey(uri, proxy)) { 370 return entry; 371 } 372 } 373 return null; 374 } 375 376 synchronized void remove(String authscheme, URI domain, boolean proxy) { 377 for (CacheEntry entry : entries) { 378 if (entry.equalsKey(domain, proxy)) { 379 entries.remove(entry); 380 } 381 } 382 } 383 384 synchronized void remove(CacheEntry entry) { 385 entries.remove(entry); 386 } 387 388 synchronized void store(String authscheme, 389 URI domain, 390 boolean proxy, 391 PasswordAuthentication value) { 392 remove(authscheme, domain, proxy); 393 entries.add(new CacheEntry(authscheme, domain, proxy, value)); 394 } 395 } 396 397 static URI normalize(URI uri, boolean isPrimaryKey) { 398 String path = uri.getPath(); 399 if (path == null || path.isEmpty()) { 400 // make sure the URI has a path, ignore query and fragment 401 try { 402 return new URI(uri.getScheme(), uri.getAuthority(), "/", null, null); 403 } catch (URISyntaxException e) { 404 throw new InternalError(e); 405 } 406 } else if (isPrimaryKey || !"/".equals(path)) { 407 // remove extraneous components and normalize path 408 return uri.resolve("."); 409 } else { 410 // path == "/" and the URI is not used to store 411 // the primary key in the cache: nothing to do. 412 return uri; 413 } 414 } 415 416 static final class CacheEntry { 417 final String root; 418 final String scheme; 419 final boolean proxy; 420 final PasswordAuthentication value; 421 422 CacheEntry(String authscheme, 423 URI uri, 424 boolean proxy, 425 PasswordAuthentication value) { 426 this.scheme = authscheme; 427 this.root = normalize(uri, true).toString(); // remove extraneous components 428 this.proxy = proxy; 429 this.value = value; 430 } 431 432 public PasswordAuthentication value() { 433 return value; 434 } 435 436 public boolean equalsKey(URI uri, boolean proxy) { 437 if (this.proxy != proxy) { 438 return false; 439 } 440 String other = String.valueOf(normalize(uri, false)); 441 return other.startsWith(root); 442 } 443 } 444 }