1 /* 2 * Copyright (c) 2015, 2020, 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 != PROXY_UNAUTHORIZED){ 244 if (exchange.proxyauth != null && !exchange.proxyauth.fromcache) { 245 AuthInfo au = exchange.proxyauth; 246 URI proxyURI = getProxyURI(req); 247 if (proxyURI != null) { 248 exchange.proxyauth = null; 249 cache.store(au.scheme, proxyURI, true, au.credentials); 250 } 251 } 252 if (status != UNAUTHORIZED) { 253 // check if any authentication succeeded for first time 254 if (exchange.serverauth != null && !exchange.serverauth.fromcache) { 255 AuthInfo au = exchange.serverauth; 256 cache.store(au.scheme, req.uri(), false, au.credentials); 257 } 258 } 259 return null; 260 } 261 262 boolean proxy = status == PROXY_UNAUTHORIZED; 263 String authname = proxy ? "Proxy-Authenticate" : "WWW-Authenticate"; 264 String authval = hdrs.firstValue(authname).orElse(null); 265 if (authval == null) { 266 if (exchange.client().authenticator().isPresent()) { 267 throw new IOException(authname + " header missing for response code " + status); 268 } else { 269 // No authenticator? let the caller deal with this. 270 return null; 271 } 272 } 273 274 HeaderParser parser = new HeaderParser(authval); 275 String scheme = parser.findKey(0); 276 277 // TODO: Need to generalise from Basic only. Delegate to a provider class etc. 278 279 if (!scheme.equalsIgnoreCase("Basic")) { 280 return null; // error gets returned to app 281 } 282 283 if (proxy) { 284 if (r.isConnectResponse) { 285 if (!Utils.PROXY_TUNNEL_FILTER 286 .test("Proxy-Authorization", BASIC_DUMMY)) { 287 Log.logError("{0} disabled", "Proxy-Authorization"); 288 return null; 289 } 290 } else if (req.proxy() != null) { 291 if (!Utils.PROXY_FILTER 292 .test("Proxy-Authorization", BASIC_DUMMY)) { 293 Log.logError("{0} disabled", "Proxy-Authorization"); 294 return null; 295 } 296 } 297 } 298 299 AuthInfo au = proxy ? exchange.proxyauth : exchange.serverauth; 300 if (au == null) { 301 // if no authenticator, let the user deal with 407/401 302 if (!exchange.client().authenticator().isPresent()) return null; 303 304 PasswordAuthentication pw = getCredentials(authval, proxy, req); 305 if (pw == null) { 306 throw new IOException("No credentials provided"); 307 } 308 // No authentication in request. Get credentials from user 309 au = new AuthInfo(false, "Basic", pw); 310 if (proxy) { 311 exchange.proxyauth = au; 312 } else { 313 exchange.serverauth = au; 314 } 315 req = HttpRequestImpl.newInstanceForAuthentication(req); 316 addBasicCredentials(req, proxy, pw); 317 return req; 318 } else if (au.retries > retry_limit) { 319 throw new IOException("too many authentication attempts. Limit: " + 320 Integer.toString(retry_limit)); 321 } else { 322 // we sent credentials, but they were rejected 323 if (au.fromcache) { 324 cache.remove(au.cacheEntry); 325 } 326 327 // if no authenticator, let the user deal with 407/401 328 if (!exchange.client().authenticator().isPresent()) return null; 329 330 // try again 331 PasswordAuthentication pw = getCredentials(authval, proxy, req); 332 if (pw == null) { 333 throw new IOException("No credentials provided"); 334 } 335 au = au.retryWithCredentials(pw); 336 if (proxy) { 337 exchange.proxyauth = au; 338 } else { 339 exchange.serverauth = au; 340 } 341 req = HttpRequestImpl.newInstanceForAuthentication(req); 342 addBasicCredentials(req, proxy, au.credentials); 343 au.retries++; 344 return req; 345 } 346 } 347 348 // Use a WeakHashMap to make it possible for the HttpClient to 349 // be garbage collected when no longer referenced. 350 static final WeakHashMap<HttpClientImpl,Cache> caches = new WeakHashMap<>(); 351 352 static synchronized Cache getCache(MultiExchange<?> exchange) { 353 HttpClientImpl client = exchange.client(); 354 Cache c = caches.get(client); 355 if (c == null) { 356 c = new Cache(); 357 caches.put(client, c); 358 } 359 return c; 360 } 361 362 // Note: Make sure that Cache and CacheEntry do not keep any strong 363 // reference to the HttpClient: it would prevent the client being 364 // GC'ed when no longer referenced. 365 static final class Cache { 366 final LinkedList<CacheEntry> entries = new LinkedList<>(); 367 368 Cache() {} 369 370 synchronized CacheEntry get(URI uri, boolean proxy) { 371 for (CacheEntry entry : entries) { 372 if (entry.equalsKey(uri, proxy)) { 373 return entry; 374 } 375 } 376 return null; 377 } 378 379 synchronized void remove(String authscheme, URI domain, boolean proxy) { 380 for (CacheEntry entry : entries) { 381 if (entry.equalsKey(domain, proxy)) { 382 entries.remove(entry); 383 } 384 } 385 } 386 387 synchronized void remove(CacheEntry entry) { 388 entries.remove(entry); 389 } 390 391 synchronized void store(String authscheme, 392 URI domain, 393 boolean proxy, 394 PasswordAuthentication value) { 395 remove(authscheme, domain, proxy); 396 entries.add(new CacheEntry(authscheme, domain, proxy, value)); 397 } 398 } 399 400 static URI normalize(URI uri, boolean isPrimaryKey) { 401 String path = uri.getPath(); 402 if (path == null || path.isEmpty()) { 403 // make sure the URI has a path, ignore query and fragment 404 try { 405 return new URI(uri.getScheme(), uri.getAuthority(), "/", null, null); 406 } catch (URISyntaxException e) { 407 throw new InternalError(e); 408 } 409 } else if (isPrimaryKey || !"/".equals(path)) { 410 // remove extraneous components and normalize path 411 return uri.resolve("."); 412 } else { 413 // path == "/" and the URI is not used to store 414 // the primary key in the cache: nothing to do. 415 return uri; 416 } 417 } 418 419 static final class CacheEntry { 420 final String root; 421 final String scheme; 422 final boolean proxy; 423 final PasswordAuthentication value; 424 425 CacheEntry(String authscheme, 426 URI uri, 427 boolean proxy, 428 PasswordAuthentication value) { 429 this.scheme = authscheme; 430 this.root = normalize(uri, true).toString(); // remove extraneous components 431 this.proxy = proxy; 432 this.value = value; 433 } 434 435 public PasswordAuthentication value() { 436 return value; 437 } 438 439 public boolean equalsKey(URI uri, boolean proxy) { 440 if (this.proxy != proxy) { 441 return false; 442 } 443 String other = String.valueOf(normalize(uri, false)); 444 return other.startsWith(root); 445 } 446 } 447 }