1 /* 2 * Copyright (c) 2015, 2017, 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.incubator.http; 27 28 import java.io.IOException; 29 import java.net.PasswordAuthentication; 30 import java.net.URI; 31 import java.net.InetSocketAddress; 32 import java.net.URISyntaxException; 33 import java.util.Base64; 34 import java.util.LinkedList; 35 import java.util.Objects; 36 import java.util.WeakHashMap; 37 import jdk.incubator.http.internal.common.Utils; 38 import static java.net.Authenticator.RequestorType.PROXY; 39 import static java.net.Authenticator.RequestorType.SERVER; 40 import static java.nio.charset.StandardCharsets.ISO_8859_1; 41 42 /** 43 * Implementation of Http Basic authentication. 44 */ 45 class 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 // A public no-arg constructor is required by FilterFactory 58 public AuthenticationFilter() {} 59 60 private PasswordAuthentication getCredentials(String header, 61 boolean proxy, 62 HttpRequestImpl req) 63 throws IOException 64 { 65 HttpClientImpl client = exchange.client(); 66 java.net.Authenticator auth = 67 client.authenticator() 68 .orElseThrow(() -> new IOException("No authenticator set")); 69 URI uri = req.uri(); 70 HeaderParser parser = new HeaderParser(header); 71 String authscheme = parser.findKey(0); 72 73 String realm = parser.findValue("realm"); 74 java.net.Authenticator.RequestorType rtype = proxy ? PROXY : SERVER; 75 76 // needs to be instance method in Authenticator 77 return auth.requestPasswordAuthenticationInstance(uri.getHost(), 78 null, 79 uri.getPort(), 80 uri.getScheme(), 81 realm, 82 authscheme, 83 uri.toURL(), 84 rtype 85 ); 86 } 87 88 private URI getProxyURI(HttpRequestImpl r) { 89 InetSocketAddress proxy = r.proxy(); 90 if (proxy == null) { 91 return null; 92 } 93 94 // our own private scheme for proxy URLs 95 // eg. proxy.http://host:port/ 96 String scheme = "proxy." + r.uri().getScheme(); 97 try { 98 return new URI(scheme, 99 null, 100 proxy.getHostString(), 101 proxy.getPort(), 102 null, 103 null, 104 null); 105 } catch (URISyntaxException e) { 106 throw new InternalError(e); 107 } 108 } 109 110 @Override 111 public void request(HttpRequestImpl r, MultiExchange<?,?> e) throws IOException { 112 // use preemptive authentication if an entry exists. 113 Cache cache = getCache(e); 114 this.exchange = e; 115 116 // Proxy 117 if (exchange.proxyauth == null) { 118 URI proxyURI = getProxyURI(r); 119 if (proxyURI != null) { 120 CacheEntry ca = cache.get(proxyURI, true); 121 if (ca != null) { 122 exchange.proxyauth = new AuthInfo(true, ca.scheme, null, ca); 123 addBasicCredentials(r, true, ca.value); 124 } 125 } 126 } 127 128 // Server 129 if (exchange.serverauth == null) { 130 CacheEntry ca = cache.get(r.uri(), false); 131 if (ca != null) { 132 exchange.serverauth = new AuthInfo(true, ca.scheme, null, ca); 133 addBasicCredentials(r, false, ca.value); 134 } 135 } 136 } 137 138 // TODO: refactor into per auth scheme class 139 private static void addBasicCredentials(HttpRequestImpl r, 140 boolean proxy, 141 PasswordAuthentication pw) { 142 String hdrname = proxy ? "Proxy-Authorization" : "Authorization"; 143 StringBuilder sb = new StringBuilder(128); 144 sb.append(pw.getUserName()).append(':').append(pw.getPassword()); 145 String s = encoder.encodeToString(sb.toString().getBytes(ISO_8859_1)); 146 String value = "Basic " + s; 147 r.setSystemHeader(hdrname, value); 148 } 149 150 // Information attached to a HttpRequestImpl relating to authentication 151 static class AuthInfo { 152 final boolean fromcache; 153 final String scheme; 154 int retries; 155 PasswordAuthentication credentials; // used in request 156 CacheEntry cacheEntry; // if used 157 158 AuthInfo(boolean fromcache, 159 String scheme, 160 PasswordAuthentication credentials) { 161 this.fromcache = fromcache; 162 this.scheme = scheme; 163 this.credentials = credentials; 164 this.retries = 1; 165 } 166 167 AuthInfo(boolean fromcache, 168 String scheme, 169 PasswordAuthentication credentials, 170 CacheEntry ca) { 171 this(fromcache, scheme, credentials); 172 assert credentials == null || (ca != null && ca.value == null); 173 cacheEntry = ca; 174 } 175 176 AuthInfo retryWithCredentials(PasswordAuthentication pw) { 177 // If the info was already in the cache we need to create a new 178 // instance with fromCache==false so that it's put back in the 179 // cache if authentication succeeds 180 AuthInfo res = fromcache ? new AuthInfo(false, scheme, pw) : this; 181 res.credentials = Objects.requireNonNull(pw); 182 res.retries = retries; 183 return res; 184 } 185 186 } 187 188 @Override 189 public HttpRequestImpl response(Response r) throws IOException { 190 Cache cache = getCache(exchange); 191 int status = r.statusCode(); 192 HttpHeaders hdrs = r.headers(); 193 HttpRequestImpl req = r.request(); 194 195 if (status != UNAUTHORIZED && status != PROXY_UNAUTHORIZED) { 196 // check if any authentication succeeded for first time 197 if (exchange.serverauth != null && !exchange.serverauth.fromcache) { 198 AuthInfo au = exchange.serverauth; 199 cache.store(au.scheme, req.uri(), false, au.credentials); 200 } 201 if (exchange.proxyauth != null && !exchange.proxyauth.fromcache) { 202 AuthInfo au = exchange.proxyauth; 203 cache.store(au.scheme, req.uri(), false, au.credentials); 204 } 205 return null; 206 } 207 208 boolean proxy = status == PROXY_UNAUTHORIZED; 209 String authname = proxy ? "Proxy-Authenticate" : "WWW-Authenticate"; 210 String authval = hdrs.firstValue(authname).orElseThrow(() -> { 211 return new IOException("Invalid auth header"); 212 }); 213 HeaderParser parser = new HeaderParser(authval); 214 String scheme = parser.findKey(0); 215 216 // TODO: Need to generalise from Basic only. Delegate to a provider class etc. 217 218 if (!scheme.equalsIgnoreCase("Basic")) { 219 return null; // error gets returned to app 220 } 221 222 AuthInfo au = proxy ? exchange.proxyauth : exchange.serverauth; 223 if (au == null) { 224 PasswordAuthentication pw = getCredentials(authval, proxy, req); 225 if (pw == null) { 226 throw new IOException("No credentials provided"); 227 } 228 // No authentication in request. Get credentials from user 229 au = new AuthInfo(false, "Basic", pw); 230 if (proxy) { 231 exchange.proxyauth = au; 232 } else { 233 exchange.serverauth = au; 234 } 235 addBasicCredentials(req, proxy, pw); 236 return req; 237 } else if (au.retries > retry_limit) { 238 throw new IOException("too many authentication attempts. Limit: " + 239 Integer.toString(retry_limit)); 240 } else { 241 // we sent credentials, but they were rejected 242 if (au.fromcache) { 243 cache.remove(au.cacheEntry); 244 } 245 // try again 246 PasswordAuthentication pw = getCredentials(authval, proxy, req); 247 if (pw == null) { 248 throw new IOException("No credentials provided"); 249 } 250 au = au.retryWithCredentials(pw); 251 if (proxy) { 252 exchange.proxyauth = au; 253 } else { 254 exchange.serverauth = au; 255 } 256 addBasicCredentials(req, proxy, au.credentials); 257 au.retries++; 258 return req; 259 } 260 } 261 262 // Use a WeakHashMap to make it possible for the HttpClient to 263 // be garbaged collected when no longer referenced. 264 static final WeakHashMap<HttpClientImpl,Cache> caches = new WeakHashMap<>(); 265 266 static synchronized Cache getCache(MultiExchange<?,?> exchange) { 267 HttpClientImpl client = exchange.client(); 268 Cache c = caches.get(client); 269 if (c == null) { 270 c = new Cache(); 271 caches.put(client, c); 272 } 273 return c; 274 } 275 276 // Note: Make sure that Cache and CacheEntry do not keep any strong 277 // reference to the HttpClient: it would prevent the client being 278 // GC'ed when no longer referenced. 279 static class Cache { 280 final LinkedList<CacheEntry> entries = new LinkedList<>(); 281 282 synchronized CacheEntry get(URI uri, boolean proxy) { 283 for (CacheEntry entry : entries) { 284 if (entry.equalsKey(uri, proxy)) { 285 return entry; 286 } 287 } 288 return null; 289 } 290 291 synchronized void remove(String authscheme, URI domain, boolean proxy) { 292 for (CacheEntry entry : entries) { 293 if (entry.equalsKey(domain, proxy)) { 294 entries.remove(entry); 295 } 296 } 297 } 298 299 synchronized void remove(CacheEntry entry) { 300 entries.remove(entry); 301 } 302 303 synchronized void store(String authscheme, 304 URI domain, 305 boolean proxy, 306 PasswordAuthentication value) { 307 remove(authscheme, domain, proxy); 308 entries.add(new CacheEntry(authscheme, domain, proxy, value)); 309 } 310 } 311 312 static class CacheEntry { 313 final String root; 314 final String scheme; 315 final boolean proxy; 316 final PasswordAuthentication value; 317 318 CacheEntry(String authscheme, 319 URI uri, 320 boolean proxy, 321 PasswordAuthentication value) { 322 this.scheme = authscheme; 323 this.root = uri.resolve(".").toString(); // remove extraneous components 324 this.proxy = proxy; 325 this.value = value; 326 } 327 328 public PasswordAuthentication value() { 329 return value; 330 } 331 332 public boolean equalsKey(URI uri, boolean proxy) { 333 if (this.proxy != proxy) { 334 return false; 335 } 336 String other = uri.toString(); 337 return other.startsWith(root); 338 } 339 } 340 }