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