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