001: /**
002: * Copyright (c) 2000/2001 Thomas Kopp
003: * All rights reserved.
004: *
005: * Redistribution and use in source and binary forms, with or without
006: * modification, are permitted provided that the following conditions
007: * are met:
008: * 1. Redistributions of source code must retain the above copyright
009: * notice, this list of conditions and the following disclaimer.
010: * 2. Redistributions in binary form must reproduce the above copyright
011: * notice, this list of conditions and the following disclaimer in the
012: * documentation and/or other materials provided with the distribution.
013: *
014: * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
015: * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
016: * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
017: * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
018: * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
019: * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
020: * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
021: * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
022: * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
023: * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
024: * SUCH DAMAGE.
025: */package org.w3c.jigsaw.https;
026:
027: import java.io.ByteArrayInputStream;
028:
029: import java.lang.reflect.Method;
030:
031: import java.net.MalformedURLException;
032: import java.net.URL;
033:
034: import java.util.StringTokenizer;
035:
036: import java.security.cert.Certificate;
037: import java.security.cert.X509Certificate;
038: import java.security.cert.CertificateFactory;
039:
040: import javax.net.ssl.SSLPeerUnverifiedException;
041: import javax.net.ssl.SSLSession;
042: import javax.net.ssl.SSLSocket;
043:
044: import org.w3c.jigsaw.daemon.ServerHandlerInitException;
045:
046: import org.w3c.jigsaw.auth.AuthFilter;
047:
048: import org.w3c.jigsaw.http.httpd;
049: import org.w3c.jigsaw.http.Client;
050: import org.w3c.jigsaw.http.Request;
051:
052: import org.w3c.jigsaw.https.socket.SSLProperties;
053: import org.w3c.jigsaw.https.socket.SSLSocketClient;
054: import org.w3c.jigsaw.https.socket.SSLSocketClientFactory;
055:
056: import org.w3c.tools.resources.ProtocolException;
057: import org.w3c.tools.resources.RequestInterface;
058:
059: import org.w3c.util.ObservableProperties;
060:
061: /**
062: * @author Thomas Kopp, Dialogika GmbH
063: * @version 1.1, 27 December 2000, 6 February 2004
064: *
065: * This class supplies a Jigsaw SSL daemon adapter
066: * for enabling Jigsaw https support in accordance with the JSSE API
067: * wrapping SSL features in order to solve multiple inheritance problems
068: */
069: public class SSLAdapter {
070:
071: /**
072: * The throwable initCause method via introspection
073: * (for JDK backward compatibility)
074: */
075: private static final Method initCause;
076:
077: /**
078: * The new style certificate support level indicator
079: * (for JSSE backward compatibility)
080: */
081: private static final boolean supportsNewStyleCertificates;
082:
083: /**
084: * The X509 factory for compatible certificate conversions
085: */
086: private static final CertificateFactory x509Factory;
087:
088: static {
089: Class c = java.lang.Throwable.class;
090: Class cp[] = { java.lang.Throwable.class };
091: Method ic = null;
092: try {
093: ic = c.getMethod("initCause", cp);
094: } catch (Exception ex) {
095: ic = null;
096: } finally {
097: initCause = ic;
098: }
099: boolean supported = false;
100: CertificateFactory fact = null;
101: try {
102: supported = (null != javax.net.ssl.SSLSession.class
103: .getMethod("getPeerCertificates", (Class[]) null));
104: } catch (Exception ex) {
105: supported = false;
106: try {
107: fact = CertificateFactory.getInstance("X.509");
108: } catch (Exception sub) {
109: fact = null;
110: }
111: } finally {
112: supportsNewStyleCertificates = supported;
113: x509Factory = fact;
114: }
115: }
116:
117: /**
118: * flag for enabling debug output if applicable
119: */
120: private static boolean debug = false;
121:
122: /**
123: * The internal no laceholder object, which has a different than the
124: * expected type
125: */
126: private static final Object NO_ENTRY = "null";
127:
128: /**
129: * The servlet api spec request attribute name of the cipher suite
130: */
131: private static final String ALGORITHM = "javax.servlet.request.cipher_suite";
132:
133: /**
134: * The servlet api spec request attribute name of the key size
135: */
136: private static final String KEYSIZE = "javax.servlet.request.key_size";
137:
138: /**
139: * The servlet api spec request attribute name of the certificate chain
140: */
141: private static final String CERTCHAIN = "javax.servlet.request.X509Certificate";
142:
143: /**
144: * The servlet api spec attribute value for client authentication
145: */
146: private static final String CLIENT_CERT_AUTH = "CLIENT_CERT";
147:
148: /**
149: * flag indicating TLS support
150: */
151: private boolean ssl_enabled = false;
152:
153: /**
154: * reference to the daemon in question
155: */
156: private httpd daemon = null;
157:
158: /**
159: * uri of the daemon in question
160: */
161: private URL url = null;
162:
163: /**
164: * Fills in the stack trace of a cause if possible with respect to the
165: * api level.
166: *
167: * @param throwable the thowable to be extended
168: * @param cause the cause to be filled in
169: */
170: public static final void fillInStackTrace(Throwable throwable,
171: Throwable cause) {
172: if (null != initCause) {
173: try {
174: Object[] param = { cause };
175: initCause.invoke(throwable, param);
176: } catch (Exception iex) {
177: // ignore
178: }
179: }
180: }
181:
182: /**
183: * Supplies the ssl session attached to the specified request if any.
184: *
185: * @param request the request in question
186: * @return the attached ssl session or null if not applicable
187: */
188: private static final SSLSession getSession(Request request) {
189: Client cl = request.getClient();
190: if (cl instanceof SSLSocketClient) {
191: return ((SSLSocketClient) cl).getSession();
192: }
193: return null;
194: }
195:
196: /**
197: * An ugly way to compute the key size (to be improved)
198: * @param algorithm the algorithm name
199: * @return the key size as an integer object
200: */
201: private static final Integer getKeySize(String algorithm) {
202: if (null != algorithm) {
203: StringTokenizer parser = new StringTokenizer(algorithm, "_");
204: while (parser.hasMoreTokens()) {
205: try {
206: return Integer.valueOf(parser.nextToken());
207: } catch (NumberFormatException ex) {
208: // ignore and continue lookup
209: }
210: }
211: }
212: return null;
213: }
214:
215: /**
216: * Supplies the calculated or cached key size.
217: * @param algorithm the algorithm name
218: * @param session the ssl underlying session
219: * @return the key size as an integer object
220: */
221: private static final Integer getKeySize(String algorithm,
222: SSLSession session) {
223: // FIXME: find a better way to compute the key size
224: // at least this ugly computation gets cached
225: // at the moment
226: Object keysize = session.getValue(KEYSIZE + "." + algorithm);
227: if (keysize instanceof Integer) {
228: return (Integer) keysize;
229: } else {
230: if (null == keysize) {
231: Integer keysize2 = getKeySize(algorithm);
232: if (null != keysize2) {
233: session.putValue(KEYSIZE + "." + algorithm,
234: keysize2);
235: return keysize2;
236: } else {
237: session.putValue(KEYSIZE + "." + algorithm,
238: NO_ENTRY);
239: return null;
240: }
241: } else {
242: return null; // keysize could not be computed,
243: //i.e. equals NO_ENTRY
244: }
245: }
246: }
247:
248: /**
249: * Supplies the peer certificates if available.
250: *
251: * @param session the underlying ssl session
252: * @return the certificate chain or null
253: * @throws SSLPeerUnverifiedException iff certificates cannot be ontained
254: */
255: private static final Certificate[] getPeerCertificates(
256: SSLSession session) throws SSLPeerUnverifiedException {
257: try {
258: // using introspection due to possible compatibility issues
259: if (supportsNewStyleCertificates) {
260: return session.getPeerCertificates();
261: } else {
262: // using deprecated method as a fallback with
263: // explicit certificate conversion
264: if (null != x509Factory) {
265: javax.security.cert.X509Certificate[] oldStyleCerts;
266: oldStyleCerts = session.getPeerCertificateChain();
267: if (null != oldStyleCerts) {
268: int count = oldStyleCerts.length;
269: X509Certificate[] newStyleCerts;
270: newStyleCerts = new X509Certificate[count];
271: for (int i = 0; i < count; i++) {
272: newStyleCerts[i] = (X509Certificate) x509Factory
273: .generateCertificate(new ByteArrayInputStream(
274: oldStyleCerts[i]
275: .getEncoded()));
276: }
277: return newStyleCerts;
278: } else {
279: throw new SSLPeerUnverifiedException("No peer "
280: + "certificates available");
281: }
282: } else {
283: throw new SSLPeerUnverifiedException("No suitable"
284: + " certificate compatibility applicable");
285: }
286: }
287: } catch (SSLPeerUnverifiedException ex) {
288: throw ex;
289: } catch (Exception ex) {
290: SSLPeerUnverifiedException sub;
291: sub = new SSLPeerUnverifiedException(ex.toString());
292: fillInStackTrace(sub, ex);
293: throw sub;
294: }
295: }
296:
297: /**
298: * constructor for a TLS support adapter
299: * @param server reference to the daemon in question
300: */
301: public SSLAdapter(httpd server) {
302: if (null != server) {
303: ssl_enabled = false;
304: daemon = server;
305: url = null;
306: } else {
307: throw new NullPointerException(
308: "No daemon intance supplied for "
309: + " creating SSL adapter");
310: }
311: }
312:
313: /**
314: * method for initializing the properties of a daemon
315: * @exception ServerHandlerInitException thrown if initialization fails
316: */
317: public void initializeProperties()
318: throws ServerHandlerInitException {
319: ObservableProperties props = daemon.getProperties();
320: // default to well-known factory if applicable
321: if (props.getBoolean(SSLProperties.SSL_ENABLED_P, true)) {
322: String factory_class = props.getString(
323: httpd.CLIENT_FACTORY_P, null);
324: if (null != factory_class) {
325: try {
326: Class factory = Class.forName(factory_class);
327: ssl_enabled = (SSLSocketClientFactory.class
328: .isAssignableFrom(factory));
329: } catch (Exception ex) {
330: String error = "Initialization failed";
331: daemon.fatal(ex, error);
332: if (debug) {
333: System.out.println(error);
334: ex.printStackTrace();
335: }
336: ServerHandlerInitException sub;
337: sub = new ServerHandlerInitException(ex
338: .getMessage());
339: fillInStackTrace(sub, ex);
340: throw sub;
341: }
342: } else {
343: throw new ServerHandlerInitException("No socket client"
344: + " factory specified");
345: }
346: } else {
347: ssl_enabled = false;
348: }
349: url = null;
350: }
351:
352: /**
353: * method for preparing a reply interface for a request
354: * @param req the current request to be handled
355: * @exception ProtocolException thrown if the request url is malformed
356: */
357: public void perform(RequestInterface req) throws ProtocolException {
358: Request request = (Request) req;
359: if (ssl_enabled) {
360: // set request protocol to https
361: URL url = request.getURL();
362: try {
363: request.setURL(new URL("https", url.getHost(), url
364: .getPort(), url.getFile()));
365:
366: // tk, 1 February 2004, added SSL client attributes
367: // according to Servlet v2.4 spec
368: SSLSession session = getSession(request);
369: if (null != session) {
370: String algorithm = session.getCipherSuite();
371: request.setState(ALGORITHM, algorithm);
372:
373: Integer keysize = getKeySize(algorithm, session);
374: if (null != keysize) {
375: request.setState(KEYSIZE, keysize);
376: }
377:
378: try {
379: Certificate[] chain = getPeerCertificates(session);
380: if (chain instanceof X509Certificate[]) {
381: X509Certificate[] x509chain;
382: x509chain = (X509Certificate[]) chain;
383: request.setState(CERTCHAIN, x509chain);
384: request.setState(AuthFilter.STATE_AUTHTYPE,
385: CLIENT_CERT_AUTH);
386: if (x509chain.length > 0) {
387: request.setState(
388: AuthFilter.STATE_AUTHUSER,
389: x509chain[0].getSubjectDN()
390: .getName());
391: }
392: }
393: } catch (SSLPeerUnverifiedException ex) {
394: if (debug) {
395: ex.printStackTrace();
396: }
397: // no certificates available, ignore
398: }
399: }
400: } catch (MalformedURLException ex) {
401: String error = "Bad url during switching to https";
402: daemon.fatal(ex, error);
403: if (debug) {
404: System.out.println(error);
405: ex.printStackTrace();
406: }
407: ProtocolException sub = new ProtocolException(ex
408: .getMessage());
409: fillInStackTrace(sub, ex);
410: throw sub;
411: }
412: }
413: }
414:
415: /**
416: * method for supplying a daemon uri
417: * @return uri of the daemon in question
418: */
419: public URL getURL() {
420: if (url == null) {
421: if (ssl_enabled) {
422: try {
423: if (daemon.getPort() != 443) {
424: url = new URL("https", daemon.getHost(), daemon
425: .getPort(), "/");
426: } else {
427: url = new URL("https", daemon.getHost(), "/");
428: }
429: } catch (MalformedURLException ex) {
430: if (debug) {
431: ex.printStackTrace();
432: }
433: throw new RuntimeException("Unable to construct "
434: + "server uri. (" + ex.getMessage() + ")");
435: }
436: } else {
437: try {
438: if (daemon.getPort() != 80) {
439: url = new URL("http", daemon.getHost(), daemon
440: .getPort(), "/");
441: } else {
442: url = new URL("http", daemon.getHost(), "/");
443: }
444: } catch (MalformedURLException ex) {
445: throw new RuntimeException("Unable to construct"
446: + " server uri. (" + ex.getMessage() + ")");
447: }
448: }
449: }
450: return url;
451: }
452:
453: /**
454: * method for indicating TLS support
455: * @return flag for indicating TLS support enabled
456: */
457: public boolean sslEnabled() {
458: return ssl_enabled;
459: }
460: }
|