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