001: /*
002: * $Header: /home/cvs/jakarta-tomcat-4.0/catalina/src/share/org/apache/catalina/realm/RealmBase.java,v 1.12 2002/06/09 02:19:43 remm Exp $
003: * $Revision: 1.12 $
004: * $Date: 2002/06/09 02:19:43 $
005: *
006: * ====================================================================
007: *
008: * The Apache Software License, Version 1.1
009: *
010: * Copyright (c) 1999 The Apache Software Foundation. All rights
011: * reserved.
012: *
013: * Redistribution and use in source and binary forms, with or without
014: * modification, are permitted provided that the following conditions
015: * are met:
016: *
017: * 1. Redistributions of source code must retain the above copyright
018: * notice, this list of conditions and the following disclaimer.
019: *
020: * 2. Redistributions in binary form must reproduce the above copyright
021: * notice, this list of conditions and the following disclaimer in
022: * the documentation and/or other materials provided with the
023: * distribution.
024: *
025: * 3. The end-user documentation included with the redistribution, if
026: * any, must include the following acknowlegement:
027: * "This product includes software developed by the
028: * Apache Software Foundation (http://www.apache.org/)."
029: * Alternately, this acknowlegement may appear in the software itself,
030: * if and wherever such third-party acknowlegements normally appear.
031: *
032: * 4. The names "The Jakarta Project", "Tomcat", and "Apache Software
033: * Foundation" must not be used to endorse or promote products derived
034: * from this software without prior written permission. For written
035: * permission, please contact apache@apache.org.
036: *
037: * 5. Products derived from this software may not be called "Apache"
038: * nor may "Apache" appear in their names without prior written
039: * permission of the Apache Group.
040: *
041: * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
042: * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
043: * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
044: * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
045: * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
046: * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
047: * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
048: * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
049: * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
050: * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
051: * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
052: * SUCH DAMAGE.
053: * ====================================================================
054: *
055: * This software consists of voluntary contributions made by many
056: * individuals on behalf of the Apache Software Foundation. For more
057: * information on the Apache Software Foundation, please see
058: * <http://www.apache.org/>.
059: *
060: * [Additional notices, if required by prior licensing conditions]
061: *
062: */
063:
064: package org.apache.catalina.realm;
065:
066: import java.beans.PropertyChangeListener;
067: import java.beans.PropertyChangeSupport;
068: import java.security.Principal;
069: import java.security.MessageDigest;
070: import java.security.NoSuchAlgorithmException;
071: import java.security.cert.X509Certificate;
072: import java.io.File;
073: import org.apache.catalina.Container;
074: import org.apache.catalina.Lifecycle;
075: import org.apache.catalina.LifecycleEvent;
076: import org.apache.catalina.LifecycleException;
077: import org.apache.catalina.LifecycleListener;
078: import org.apache.catalina.Logger;
079: import org.apache.catalina.Realm;
080: import org.apache.catalina.util.HexUtils;
081: import org.apache.catalina.util.LifecycleSupport;
082: import org.apache.catalina.util.StringManager;
083: import org.apache.catalina.util.MD5Encoder;
084:
085: /**
086: * Simple implementation of <b>Realm</b> that reads an XML file to configure
087: * the valid users, passwords, and roles. The file format (and default file
088: * location) are identical to those currently supported by Tomcat 3.X.
089: *
090: * @author Craig R. McClanahan
091: * @version $Revision: 1.12 $ $Date: 2002/06/09 02:19:43 $
092: */
093:
094: public abstract class RealmBase implements Lifecycle, Realm {
095:
096: // ----------------------------------------------------- Instance Variables
097:
098: /**
099: * The Container with which this Realm is associated.
100: */
101: protected Container container = null;
102:
103: /**
104: * The debugging detail level for this component.
105: */
106: protected int debug = 0;
107:
108: /**
109: * Digest algorithm used in storing passwords in a non-plaintext format.
110: * Valid values are those accepted for the algorithm name by the
111: * MessageDigest class, or <code>null</code> if no digesting should
112: * be performed.
113: */
114: protected String digest = null;
115:
116: /**
117: * Descriptive information about this Realm implementation.
118: */
119: protected static final String info = "org.apache.catalina.realm.RealmBase/1.0";
120:
121: /**
122: * The lifecycle event support for this component.
123: */
124: protected LifecycleSupport lifecycle = new LifecycleSupport(this );
125:
126: /**
127: * The MessageDigest object for digesting user credentials (passwords).
128: */
129: protected MessageDigest md = null;
130:
131: /**
132: * The MD5 helper object for this class.
133: */
134: protected static final MD5Encoder md5Encoder = new MD5Encoder();
135:
136: /**
137: * MD5 message digest provider.
138: */
139: protected static MessageDigest md5Helper;
140:
141: /**
142: * The string manager for this package.
143: */
144: protected static StringManager sm = StringManager
145: .getManager(Constants.Package);
146:
147: /**
148: * Has this component been started?
149: */
150: protected boolean started = false;
151:
152: /**
153: * The property change support for this component.
154: */
155: protected PropertyChangeSupport support = new PropertyChangeSupport(
156: this );
157:
158: /**
159: * Should we validate client certificate chains when they are presented?
160: */
161: protected boolean validate = true;
162:
163: // ------------------------------------------------------------- Properties
164:
165: /**
166: * Return the Container with which this Realm has been associated.
167: */
168: public Container getContainer() {
169:
170: return (container);
171:
172: }
173:
174: /**
175: * Set the Container with which this Realm has been associated.
176: *
177: * @param container The associated Container
178: */
179: public void setContainer(Container container) {
180:
181: Container oldContainer = this .container;
182: this .container = container;
183: support.firePropertyChange("container", oldContainer,
184: this .container);
185:
186: }
187:
188: /**
189: * Return the debugging detail level for this component.
190: */
191: public int getDebug() {
192:
193: return (this .debug);
194:
195: }
196:
197: /**
198: * Set the debugging detail level for this component.
199: *
200: * @param debug The new debugging detail level
201: */
202: public void setDebug(int debug) {
203:
204: this .debug = debug;
205:
206: }
207:
208: /**
209: * Return the digest algorithm used for storing credentials.
210: */
211: public String getDigest() {
212:
213: return digest;
214:
215: }
216:
217: /**
218: * Set the digest algorithm used for storing credentials.
219: *
220: * @param digest The new digest algorithm
221: */
222: public void setDigest(String digest) {
223:
224: this .digest = digest;
225:
226: }
227:
228: /**
229: * Return descriptive information about this Realm implementation and
230: * the corresponding version number, in the format
231: * <code><description>/<version></code>.
232: */
233: public String getInfo() {
234:
235: return info;
236:
237: }
238:
239: /**
240: * Return the "validate certificate chains" flag.
241: */
242: public boolean getValidate() {
243:
244: return (this .validate);
245:
246: }
247:
248: /**
249: * Set the "validate certificate chains" flag.
250: *
251: * @param validate The new validate certificate chains flag
252: */
253: public void setValidate(boolean validate) {
254:
255: this .validate = validate;
256:
257: }
258:
259: // --------------------------------------------------------- Public Methods
260:
261: /**
262: * Add a property change listener to this component.
263: *
264: * @param listener The listener to add
265: */
266: public void addPropertyChangeListener(
267: PropertyChangeListener listener) {
268:
269: support.addPropertyChangeListener(listener);
270:
271: }
272:
273: /**
274: * Return the Principal associated with the specified username and
275: * credentials, if there is one; otherwise return <code>null</code>.
276: *
277: * @param username Username of the Principal to look up
278: * @param credentials Password or other credentials to use in
279: * authenticating this username
280: */
281: public Principal authenticate(String username, String credentials) {
282:
283: String serverCredentials = getPassword(username);
284:
285: if ((serverCredentials == null)
286: || (!serverCredentials.equals(credentials)))
287: return null;
288:
289: return getPrincipal(username);
290:
291: }
292:
293: /**
294: * Return the Principal associated with the specified username and
295: * credentials, if there is one; otherwise return <code>null</code>.
296: *
297: * @param username Username of the Principal to look up
298: * @param credentials Password or other credentials to use in
299: * authenticating this username
300: */
301: public Principal authenticate(String username, byte[] credentials) {
302:
303: return (authenticate(username, credentials.toString()));
304:
305: }
306:
307: /**
308: * Return the Principal associated with the specified username, which
309: * matches the digest calculated using the given parameters using the
310: * method described in RFC 2069; otherwise return <code>null</code>.
311: *
312: * @param username Username of the Principal to look up
313: * @param clientDigest Digest which has been submitted by the client
314: * @param nOnce Unique (or supposedly unique) token which has been used
315: * for this request
316: * @param realm Realm name
317: * @param md5a2 Second MD5 digest used to calculate the digest :
318: * MD5(Method + ":" + uri)
319: */
320: public Principal authenticate(String username, String clientDigest,
321: String nOnce, String nc, String cnonce, String qop,
322: String realm, String md5a2) {
323:
324: /*
325: System.out.println("Digest : " + clientDigest);
326:
327: System.out.println("************ Digest info");
328: System.out.println("Username:" + username);
329: System.out.println("ClientSigest:" + clientDigest);
330: System.out.println("nOnce:" + nOnce);
331: System.out.println("nc:" + nc);
332: System.out.println("cnonce:" + cnonce);
333: System.out.println("qop:" + qop);
334: System.out.println("realm:" + realm);
335: System.out.println("md5a2:" + md5a2);
336: */
337:
338: String md5a1 = getDigest(username, realm);
339: if (md5a1 == null)
340: return null;
341: String serverDigestValue = md5a1 + ":" + nOnce + ":" + nc + ":"
342: + cnonce + ":" + qop + ":" + md5a2;
343: String serverDigest = md5Encoder.encode(md5Helper
344: .digest(serverDigestValue.getBytes()));
345: //System.out.println("Server digest : " + serverDigest);
346:
347: if (serverDigest.equals(clientDigest))
348: return getPrincipal(username);
349: else
350: return null;
351: }
352:
353: /**
354: * Return the Principal associated with the specified chain of X509
355: * client certificates. If there is none, return <code>null</code>.
356: *
357: * @param certs Array of client certificates, with the first one in
358: * the array being the certificate of the client itself.
359: */
360: public Principal authenticate(X509Certificate certs[]) {
361:
362: if ((certs == null) || (certs.length < 1))
363: return (null);
364:
365: // Check the validity of each certificate in the chain
366: if (debug >= 1)
367: log("Authenticating client certificate chain");
368: if (validate) {
369: for (int i = 0; i < certs.length; i++) {
370: if (debug >= 2)
371: log(" Checking validity for '"
372: + certs[i].getSubjectDN().getName() + "'");
373: try {
374: certs[i].checkValidity();
375: } catch (Exception e) {
376: if (debug >= 2)
377: log(" Validity exception", e);
378: return (null);
379: }
380: }
381: }
382:
383: // Check the existence of the client Principal in our database
384: return (getPrincipal(certs[0].getSubjectDN().getName()));
385:
386: }
387:
388: /**
389: * Return <code>true</code> if the specified Principal has the specified
390: * security role, within the context of this Realm; otherwise return
391: * <code>false</code>. This method can be overridden by Realm
392: * implementations, but the default is adequate when an instance of
393: * <code>GenericPrincipal</code> is used to represent authenticated
394: * Principals from this Realm.
395: *
396: * @param principal Principal for whom the role is to be checked
397: * @param role Security role to be checked
398: */
399: public boolean hasRole(Principal principal, String role) {
400:
401: if ((principal == null) || (role == null)
402: || !(principal instanceof GenericPrincipal))
403: return (false);
404: GenericPrincipal gp = (GenericPrincipal) principal;
405: if (!(gp.getRealm() == this ))
406: return (false);
407: boolean result = gp.hasRole(role);
408: if (debug >= 2) {
409: String name = principal.getName();
410: if (result)
411: log(sm
412: .getString("realmBase.hasRoleSuccess", name,
413: role));
414: else
415: log(sm
416: .getString("realmBase.hasRoleFailure", name,
417: role));
418: }
419: return (result);
420:
421: }
422:
423: /**
424: * Remove a property change listener from this component.
425: *
426: * @param listener The listener to remove
427: */
428: public void removePropertyChangeListener(
429: PropertyChangeListener listener) {
430:
431: support.removePropertyChangeListener(listener);
432:
433: }
434:
435: // ------------------------------------------------------ Lifecycle Methods
436:
437: /**
438: * Add a lifecycle event listener to this component.
439: *
440: * @param listener The listener to add
441: */
442: public void addLifecycleListener(LifecycleListener listener) {
443:
444: lifecycle.addLifecycleListener(listener);
445:
446: }
447:
448: /**
449: * Get the lifecycle listeners associated with this lifecycle. If this
450: * Lifecycle has no listeners registered, a zero-length array is returned.
451: */
452: public LifecycleListener[] findLifecycleListeners() {
453:
454: return lifecycle.findLifecycleListeners();
455:
456: }
457:
458: /**
459: * Remove a lifecycle event listener from this component.
460: *
461: * @param listener The listener to remove
462: */
463: public void removeLifecycleListener(LifecycleListener listener) {
464:
465: lifecycle.removeLifecycleListener(listener);
466:
467: }
468:
469: /**
470: * Prepare for the beginning of active use of the public methods of this
471: * component. This method should be called before any of the public
472: * methods of this component are utilized. It should also send a
473: * LifecycleEvent of type START_EVENT to any registered listeners.
474: *
475: * @exception LifecycleException if this component detects a fatal error
476: * that prevents this component from being used
477: */
478: public void start() throws LifecycleException {
479:
480: // Validate and update our current component state
481: if (started)
482: throw new LifecycleException(sm
483: .getString("realmBase.alreadyStarted"));
484: lifecycle.fireLifecycleEvent(START_EVENT, null);
485: started = true;
486:
487: // Create a MessageDigest instance for credentials, if desired
488: if (digest != null) {
489: try {
490: md = MessageDigest.getInstance(digest);
491: } catch (NoSuchAlgorithmException e) {
492: throw new LifecycleException(sm.getString(
493: "realmBase.algorithm", digest), e);
494: }
495: }
496:
497: }
498:
499: /**
500: * Gracefully terminate the active use of the public methods of this
501: * component. This method should be the last one called on a given
502: * instance of this component. It should also send a LifecycleEvent
503: * of type STOP_EVENT to any registered listeners.
504: *
505: * @exception LifecycleException if this component detects a fatal error
506: * that needs to be reported
507: */
508: public void stop() throws LifecycleException {
509:
510: // Validate and update our current component state
511: if (!started)
512: throw new LifecycleException(sm
513: .getString("realmBase.notStarted"));
514: lifecycle.fireLifecycleEvent(STOP_EVENT, null);
515: started = false;
516:
517: // Clean up allocated resources
518: md = null;
519:
520: }
521:
522: // ------------------------------------------------------ Protected Methods
523:
524: /**
525: * Digest the password using the specified algorithm and
526: * convert the result to a corresponding hexadecimal string.
527: * If exception, the plain credentials string is returned.
528: *
529: * <strong>IMPLEMENTATION NOTE</strong> - This implementation is
530: * synchronized because it reuses the MessageDigest instance.
531: * This should be faster than cloning the instance on every request.
532: *
533: * @param credentials Password or other credentials to use in
534: * authenticating this username
535: */
536: protected String digest(String credentials) {
537:
538: // If no MessageDigest instance is specified, return unchanged
539: if (hasMessageDigest() == false)
540: return (credentials);
541:
542: // Digest the user credentials and return as hexadecimal
543: synchronized (this ) {
544: try {
545: md.reset();
546: md.update(credentials.getBytes());
547: return (HexUtils.convert(md.digest()));
548: } catch (Exception e) {
549: log(sm.getString("realmBase.digest"), e);
550: return (credentials);
551: }
552: }
553:
554: }
555:
556: protected boolean hasMessageDigest() {
557: return !(md == null);
558: }
559:
560: /**
561: * Return the digest associated with given principal's user name.
562: */
563: protected String getDigest(String username, String realmName) {
564: if (md5Helper == null) {
565: try {
566: md5Helper = MessageDigest.getInstance("MD5");
567: } catch (NoSuchAlgorithmException e) {
568: e.printStackTrace();
569: throw new IllegalStateException();
570: }
571: }
572: String digestValue = username + ":" + realmName + ":"
573: + getPassword(username);
574: byte[] digest = md5Helper.digest(digestValue.getBytes());
575: return md5Encoder.encode(digest);
576: }
577:
578: /**
579: * Return a short name for this Realm implementation, for use in
580: * log messages.
581: */
582: protected abstract String getName();
583:
584: /**
585: * Return the password associated with the given principal's user name.
586: */
587: protected abstract String getPassword(String username);
588:
589: /**
590: * Return the Principal associated with the given user name.
591: */
592: protected abstract Principal getPrincipal(String username);
593:
594: /**
595: * Log a message on the Logger associated with our Container (if any)
596: *
597: * @param message Message to be logged
598: */
599: protected void log(String message) {
600:
601: Logger logger = null;
602: String name = null;
603: if (container != null) {
604: logger = container.getLogger();
605: name = container.getName();
606: }
607:
608: if (logger != null) {
609: logger.log(getName() + "[" + name + "]: " + message);
610: } else {
611: System.out
612: .println(getName() + "[" + name + "]: " + message);
613: }
614:
615: }
616:
617: /**
618: * Log a message on the Logger associated with our Container (if any)
619: *
620: * @param message Message to be logged
621: * @param throwable Associated exception
622: */
623: protected void log(String message, Throwable throwable) {
624:
625: Logger logger = null;
626: String name = null;
627: if (container != null) {
628: logger = container.getLogger();
629: name = container.getName();
630: }
631:
632: if (logger != null) {
633: logger.log(getName() + "[" + name + "]: " + message,
634: throwable);
635: } else {
636: System.out
637: .println(getName() + "[" + name + "]: " + message);
638: throwable.printStackTrace(System.out);
639: }
640: }
641:
642: // --------------------------------------------------------- Static Methods
643:
644: /**
645: * Digest password using the algorithm especificied and
646: * convert the result to a corresponding hex string.
647: * If exception, the plain credentials string is returned
648: *
649: * @param credentials Password or other credentials to use in
650: * authenticating this username
651: * @param algorithm Algorithm used to do th digest
652: */
653: public final static String Digest(String credentials,
654: String algorithm) {
655:
656: try {
657: // Obtain a new message digest with "digest" encryption
658: MessageDigest md = (MessageDigest) MessageDigest
659: .getInstance(algorithm).clone();
660: // encode the credentials
661: md.update(credentials.getBytes());
662: // Digest the credentials and return as hexadecimal
663: return (HexUtils.convert(md.digest()));
664: } catch (Exception ex) {
665: ex.printStackTrace();
666: return credentials;
667: }
668:
669: }
670:
671: /**
672: * Digest password using the algorithm especificied and
673: * convert the result to a corresponding hex string.
674: * If exception, the plain credentials string is returned
675: */
676: public static void main(String args[]) {
677:
678: if (args.length > 2 && args[0].equalsIgnoreCase("-a")) {
679: for (int i = 2; i < args.length; i++) {
680: System.out.print(args[i] + ":");
681: System.out.println(Digest(args[i], args[1]));
682: }
683: } else {
684: System.out
685: .println("Usage: RealmBase -a <algorithm> <credentials>");
686: }
687:
688: }
689:
690: }
|