001: /*
002: * @(#)AuthenticationInfo.java 1.30 06/10/10
003: *
004: * Copyright 1990-2006 Sun Microsystems, Inc. All Rights Reserved.
005: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
006: *
007: * This program is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU General Public License version
009: * 2 only, as published by the Free Software Foundation.
010: *
011: * This program is distributed in the hope that it will be useful, but
012: * WITHOUT ANY WARRANTY; without even the implied warranty of
013: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
014: * General Public License version 2 for more details (a copy is
015: * included at /legal/license.txt).
016: *
017: * You should have received a copy of the GNU General Public License
018: * version 2 along with this work; if not, write to the Free Software
019: * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
020: * 02110-1301 USA
021: *
022: * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa
023: * Clara, CA 95054 or visit www.sun.com if you need additional
024: * information or have any questions.
025: *
026: */
027:
028: package sun.net.www.protocol.http;
029:
030: import java.io.IOException;
031: import java.net.URL;
032: import java.util.Hashtable;
033: import java.util.LinkedList;
034: import java.util.ListIterator;
035: import java.util.Enumeration;
036: import java.util.HashMap;
037:
038: import sun.net.www.HeaderParser;
039:
040: /**
041: * AuthenticationInfo: Encapsulate the information needed to
042: * authenticate a user to a server.
043: *
044: * @version 1.19, 11/06/00
045: */
046: // It would be nice if this class understood about partial matching.
047: // If you're authorized for foo.com, chances are high you're also
048: // authorized for baz.foo.com.
049: // NB: When this gets implemented, be careful about the uncaching
050: // policy in HttpURLConnection. A failure on baz.foo.com shouldn't
051: // uncache foo.com!
052: abstract class AuthenticationInfo implements Cloneable {
053:
054: // Constants saying what kind of authroization this is. This determines
055: // the namespace in the hash table lookup.
056: static final char SERVER_AUTHENTICATION = 's';
057: static final char PROXY_AUTHENTICATION = 'p';
058:
059: /**
060: * Caches authentication info entered by user. See cacheKey()
061: */
062: static private PathMap cache = new PathMap();
063:
064: /**
065: * If true, then simultaneous authentication requests to the same realm/proxy
066: * are serialized, in order to avoid a user having to type the same username/passwords
067: * repeatedly, via the Authenticator. Default is false, which means that this
068: * behavior is switched off.
069: */
070: static boolean serializeAuth;
071:
072: static {
073: serializeAuth = ((Boolean) java.security.AccessController
074: .doPrivileged(new sun.security.action.GetBooleanAction(
075: "http.auth.serializeRequests"))).booleanValue();
076: }
077:
078: /**
079: * requests is used to ensure that interaction with the
080: * Authenticator for a particular realm is single threaded.
081: * ie. if multiple threads need to get credentials from the user
082: * at the same time, then all but the first will block until
083: * the first completes its authentication.
084: */
085: static private HashMap requests = new HashMap();
086:
087: /* check if a request for this destination is in progress
088: * return false immediately if not. Otherwise block until
089: * request is finished and return true
090: */
091: static private boolean requestIsInProgress(String key) {
092: if (!serializeAuth) {
093: /* behavior is disabled. Revert to concurrent requests */
094: return false;
095: }
096: synchronized (requests) {
097: Thread t, c;
098: c = Thread.currentThread();
099: if ((t = (Thread) requests.get(key)) == null) {
100: requests.put(key, c);
101: return false;
102: }
103: if (t == c) {
104: return false;
105: }
106: while (requests.containsKey(key)) {
107: try {
108: requests.wait();
109: } catch (InterruptedException e) {
110: }
111: }
112: }
113: /* entry may be in cache now. */
114: return true;
115: }
116:
117: /* signal completion of an authentication (whether it succeeded or not)
118: * so that other threads can continue.
119: */
120: static private void requestCompleted(String key) {
121: synchronized (requests) {
122: boolean waspresent = requests.remove(key) != null;
123: // Uncomment this line if assertions are always
124: // turned on for libraries
125: //assert waspresent;
126: requests.notifyAll();
127: }
128: }
129:
130: static void printCache() {
131: cache.print();
132: }
133:
134: //public String toString () {
135: //return ("{"+type+":"+authType+":"+protocol+":"+host+":"+port+":"+realm+":"+path+"}");
136: //}
137:
138: // TODO: This cache just grows forever. We should put in a bounded
139: // cache, or maybe something using WeakRef's.
140:
141: /** The type (server/proxy) of authentication this is. Used for key lookup */
142: char type;
143:
144: /** The authentication type (basic/digest). Also used for key lookup */
145: char authType;
146:
147: /** The protocol/scheme (i.e. http or https ). Need to keep the caches
148: * logically separate for the two protocols. This field is only used
149: * when constructed with a URL (the normal case for server authentication)
150: * For proxy authentication the protocol is not relevant.
151: */
152: String protocol;
153:
154: /** The host we're authenticating against. */
155: String host;
156:
157: /** The port on the host we're authenticating against. */
158: int port;
159:
160: /** The realm we're authenticating against. */
161: String realm;
162:
163: /** The shortest path from the URL we authenticated against. */
164: String path;
165:
166: /** Use this constructor only for proxy entries */
167: AuthenticationInfo(char type, char authType, String host, int port,
168: String realm) {
169: this .type = type;
170: this .authType = authType;
171: this .protocol = "";
172: this .host = host.toLowerCase();
173: this .port = port;
174: this .realm = realm;
175: this .path = null;
176: }
177:
178: public Object clone() {
179: try {
180: return super .clone();
181: } catch (CloneNotSupportedException e) {
182: // Cannot happen because Cloneable implemented by AuthenticationInfo
183: return null;
184: }
185: }
186:
187: /*
188: * Constructor used to limit the authorization to the path within
189: * the URL. Use this constructor for origin server entries.
190: */
191: AuthenticationInfo(char type, char authType, URL url, String realm) {
192: this .type = type;
193: this .authType = authType;
194: this .protocol = url.getProtocol().toLowerCase();
195: this .host = url.getHost().toLowerCase();
196: this .port = url.getPort();
197: if (this .port == -1) {
198: this .port = url.getDefaultPort();
199: }
200: this .realm = realm;
201:
202: String urlPath = url.getPath();
203: if (urlPath.length() == 0)
204: this .path = urlPath;
205: else {
206: this .path = reducePath(urlPath);
207: }
208:
209: }
210:
211: /*
212: * reduce the path to the root of where we think the
213: * authorization begins. This could get shorter as
214: * the url is traversed up following a successful challenge.
215: */
216: static String reducePath(String urlPath) {
217: int sepIndex = urlPath.lastIndexOf('/');
218: int targetSuffixIndex = urlPath.lastIndexOf('.');
219: if (sepIndex != -1)
220: if (sepIndex < targetSuffixIndex)
221: return urlPath.substring(0, sepIndex + 1);
222: else
223: return urlPath;
224: else
225: return urlPath;
226: }
227:
228: /**
229: * Returns info for the URL, for an HTTP server auth. Used when we
230: * don't yet know the realm
231: * (i.e. when we're preemptively setting the auth).
232: */
233: static AuthenticationInfo getServerAuth(URL url) {
234: int port = url.getPort();
235: if (port == -1) {
236: port = url.getDefaultPort();
237: }
238: String key = SERVER_AUTHENTICATION + ":"
239: + url.getProtocol().toLowerCase() + ":"
240: + url.getHost().toLowerCase() + ":" + port;
241: return getAuth(key, url);
242: }
243:
244: /**
245: * Returns info for the URL, for an HTTP server auth. Used when we
246: * do know the realm (i.e. when we're responding to a challenge).
247: * In this case we do not use the path because the protection space
248: * is identified by the host:port:realm only
249: */
250: static AuthenticationInfo getServerAuth(URL url, String realm,
251: char atype) {
252: int port = url.getPort();
253: if (port == -1) {
254: port = url.getDefaultPort();
255: }
256: String key = SERVER_AUTHENTICATION + ":" + atype + ":"
257: + url.getProtocol().toLowerCase() + ":"
258: + url.getHost().toLowerCase() + ":" + port + ":"
259: + realm;
260: AuthenticationInfo cached = getAuth(key, null);
261: if ((cached == null) && requestIsInProgress(key)) {
262: /* check the cache again, it might contain an entry */
263: cached = getAuth(key, null);
264: }
265: return cached;
266: }
267:
268: /**
269: * Return the AuthenticationInfo object from the cache if it's path is
270: * a substring of the supplied URLs path.
271: */
272: static AuthenticationInfo getAuth(String key, URL url) {
273: if (url == null) {
274: return cache.get(key, null);
275: } else {
276: return cache.get(key, url.getPath());
277: }
278: }
279:
280: /**
281: * Returns a firewall authentication, for the given host/port. Used
282: * for preemptive header-setting. Note, the protocol field is always
283: * blank for proxies.
284: */
285: static AuthenticationInfo getProxyAuth(String host, int port) {
286: String key = PROXY_AUTHENTICATION + "::" + host.toLowerCase()
287: + ":" + port;
288: AuthenticationInfo result = (AuthenticationInfo) cache.get(key,
289: null);
290: return result;
291: }
292:
293: /**
294: * Returns a firewall authentication, for the given host/port and realm.
295: * Used in response to a challenge. Note, the protocol field is always
296: * blank for proxies.
297: */
298: static AuthenticationInfo getProxyAuth(String host, int port,
299: String realm, char atype) {
300: String key = PROXY_AUTHENTICATION + ":" + atype + "::"
301: + host.toLowerCase() + ":" + port + ":" + realm;
302: AuthenticationInfo cached = cache.get(key, null);
303: if ((cached == null) && requestIsInProgress(key)) {
304: /* check the cache again, it might contain an entry */
305: cached = cache.get(key, null);
306: }
307: return cached;
308: }
309:
310: /**
311: * Add this authentication to the cache
312: */
313: void addToCache() {
314: cache.put(cacheKey(true), this );
315: if (supportsPreemptiveAuthorization()) {
316: cache.put(cacheKey(false), this );
317: }
318: endAuthRequest();
319: }
320:
321: void endAuthRequest() {
322: if (!serializeAuth) {
323: return;
324: }
325: synchronized (requests) {
326: requestCompleted(cacheKey(true));
327: }
328: }
329:
330: /**
331: * Remove this authentication from the cache
332: */
333: void removeFromCache() {
334: cache.remove(cacheKey(true), this );
335: if (supportsPreemptiveAuthorization()) {
336: cache.remove(cacheKey(false), this );
337: }
338: }
339:
340: /**
341: * @return true if this authentication supports preemptive authorization
342: */
343: abstract boolean supportsPreemptiveAuthorization();
344:
345: /**
346: * @return the name of the HTTP header this authentication wants set.
347: * This is used for preemptive authorization.
348: */
349: abstract String getHeaderName();
350:
351: /**
352: * Calculates and returns the authentication header value based
353: * on the stored authentication parameters. If the calculation does not depend
354: * on the URL or the request method then these parameters are ignored.
355: * @param url The URL
356: * @param method The request method
357: * @return the value of the HTTP header this authentication wants set.
358: * Used for preemptive authorization.
359: */
360: abstract String getHeaderValue(URL url, String method);
361:
362: /**
363: * Set header(s) on the given connection. Subclasses must override
364: * This will only be called for
365: * definitive (i.e. non-preemptive) authorization.
366: * @param conn The connection to apply the header(s) to
367: * @param p A source of header values for this connection, if needed.
368: * @param raw The raw header field (if needed)
369: * @return true if all goes well, false if no headers were set.
370: */
371: abstract boolean setHeaders(HttpURLConnection conn, HeaderParser p,
372: String raw);
373:
374: /**
375: * Check if the header indicates that the current auth. parameters are stale.
376: * If so, then replace the relevant field with the new value
377: * and return true. Otherwise return false.
378: * returning true means the request can be retried with the same userid/password
379: * returning false means we have to go back to the user to ask for a new
380: * username password.
381: */
382: abstract boolean isAuthorizationStale(String header);
383:
384: /**
385: * Check for any expected authentication information in the response
386: * from the server
387: */
388: abstract void checkResponse(String header, String method, URL url)
389: throws IOException;
390:
391: /**
392: * Give a key for hash table lookups.
393: * @param includeRealm if you want the realm considered. Preemptively
394: * setting an authorization is done before the realm is known.
395: */
396: String cacheKey(boolean includeRealm) {
397: // This must be kept in sync with the getXXXAuth() methods in this
398: // class.
399: if (includeRealm) {
400: return type + ":" + authType + ":" + protocol + ":" + host
401: + ":" + port + ":" + realm;
402: } else {
403: return type + ":" + protocol + ":" + host + ":" + port;
404: }
405: }
406: }
407:
408: /*
409: * Pathmap stores AuthenticationInfo instances, which are keyed
410: * by the combination of:
411: *
412: * 1) the protocol+host+port field of the URL where the resource is located
413: * plus optionally the name of the authentication realm.
414: *
415: * 2) the "known" abs_path root of the protection space. For digest
416: * authentication the root is generally known because it is returned
417: * explicitly by the server. For basic authentication, this is not the
418: * case and we start with the abs_path in the request. If a subsequent
419: * authentication succeeds for a path higher in the same hierarchy then
420: * the shorter pathname replaces the longer one.
421: *
422: * If the scheme does not support pre-emptive authentication, then
423: * only one entry is created (per successful authentication), which
424: * does not inlclude the realm in the primary 1) key.
425: *
426: * If the scheme does support pre-emptive authentication (which is the
427: * case for both basic and digest) then two entries are created for
428: * each successful authentication. One, with the realm and one without.
429: * The one without the realm is used for pre-emptive header setting
430: * because at this time the realm is not known.
431: */
432:
433: class PathMap {
434: Hashtable hashtable;
435:
436: PathMap() {
437: hashtable = new Hashtable();
438: }
439:
440: // put a value in map according to primary key + secondary key which
441: // is the path field of AuthenticationInfo
442:
443: void print() {
444: Enumeration keys = hashtable.keys();
445: while (keys.hasMoreElements()) {
446: String key = (String) keys.nextElement();
447: LinkedList list = (LinkedList) (hashtable.get(key));
448: System.out.print("pkey = " + key + " ");
449: ListIterator iter = list.listIterator();
450: while (iter.hasNext()) {
451: AuthenticationInfo inf = (AuthenticationInfo) iter
452: .next();
453: System.out.print(inf.path + " ");
454: }
455: System.out.println(" ");
456: }
457: }
458:
459: synchronized void put(String pkey, AuthenticationInfo value) {
460: LinkedList list = (LinkedList) hashtable.get(pkey);
461: String skey = value.path;
462: if (list == null) {
463: list = new LinkedList();
464: hashtable.put(pkey, list);
465: }
466: // Check if the path already exists or a super-set of it exists
467: ListIterator iter = list.listIterator();
468: while (iter.hasNext()) {
469: AuthenticationInfo inf = (AuthenticationInfo) iter.next();
470: if (inf.path == null || inf.path.startsWith(skey)) {
471: iter.remove();
472: }
473: }
474: iter.add(value);
475: }
476:
477: // get a value from map checking both primary
478: // and secondary (urlpath) key
479:
480: synchronized AuthenticationInfo get(String pkey, String skey) {
481: LinkedList list = (LinkedList) hashtable.get(pkey);
482: if (list == null || list.size() == 0) {
483: return null;
484: }
485: if (skey == null) {
486: // list should contain only one element
487: return (AuthenticationInfo) list.get(0);
488: }
489: ListIterator iter = list.listIterator();
490: while (iter.hasNext()) {
491: AuthenticationInfo inf = (AuthenticationInfo) iter.next();
492: if (skey.startsWith(inf.path)) {
493: return inf;
494: }
495: }
496: return null;
497: }
498:
499: synchronized void remove(String pkey, AuthenticationInfo entry) {
500: LinkedList list = (LinkedList) hashtable.get(pkey);
501: if (list == null) {
502: return;
503: }
504: if (entry == null) {
505: list.clear();
506: return;
507: }
508: ListIterator iter = list.listIterator();
509: while (iter.hasNext()) {
510: AuthenticationInfo inf = (AuthenticationInfo) iter.next();
511: if (entry.equals(inf)) {
512: iter.remove();
513: }
514: }
515: }
516: }
|