View Javadoc

1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
25   *
26   */
27  
28  package org.apache.http.nio.reactor.ssl;
29  
30  import org.apache.http.annotation.ThreadSafe;
31  import org.apache.http.nio.reactor.EventMask;
32  import org.apache.http.nio.reactor.IOSession;
33  import org.apache.http.nio.reactor.SocketAccessor;
34  import org.apache.http.nio.reactor.SessionBufferStatus;
35  
36  import javax.net.ssl.SSLContext;
37  import javax.net.ssl.SSLEngine;
38  import javax.net.ssl.SSLEngineResult;
39  import javax.net.ssl.SSLSession;
40  import javax.net.ssl.SSLEngineResult.HandshakeStatus;
41  import javax.net.ssl.SSLEngineResult.Status;
42  import javax.net.ssl.SSLException;
43  import java.io.IOException;
44  import java.net.InetSocketAddress;
45  import java.net.Socket;
46  import java.net.SocketAddress;
47  import java.nio.ByteBuffer;
48  import java.nio.channels.ByteChannel;
49  import java.nio.channels.SelectionKey;
50  
51  /**
52   * <tt>SSLIOSession</tt> is a decorator class intended to transparently extend
53   * an {@link IOSession} with transport layer security capabilities based on
54   * the SSL/TLS protocol.
55   * <p/>
56   * The resultant instance of <tt>SSLIOSession</tt> must be added to the original
57   * I/O session as an attribute with the {@link #SESSION_KEY} key.
58   * <pre>
59   *  SSLContext sslcontext = SSLContext.getInstance("SSL");
60   *  sslcontext.init(null, null, null);
61   *  SSLIOSession sslsession = new SSLIOSession(
62   *      iosession, SSLMode.CLIENT, sslcontext, null);
63   *  iosession.setAttribute(SSLIOSession.SESSION_KEY, sslsession);
64   * </pre>
65   *
66   * @since 4.2
67   */
68  @ThreadSafe
69  public class SSLIOSession implements IOSession, SessionBufferStatus, SocketAccessor {
70  
71      /**
72       * Name of the context attribute key, which can be used to obtain the
73       * SSL session.
74       */
75      public static final String SESSION_KEY = "http.session.ssl";
76  
77      private final IOSession session;
78      private final SSLMode defaultMode;
79      private final SSLEngine sslEngine;
80      private final ByteBuffer inEncrypted;
81      private final ByteBuffer outEncrypted;
82      private final ByteBuffer inPlain;
83      private final ByteBuffer outPlain;
84      private final InternalByteChannel channel;
85      private final SSLSetupHandler handler;
86  
87      private int appEventMask;
88      private SessionBufferStatus appBufferStatus;
89  
90      private boolean endOfStream;
91      private volatile int status;
92      private volatile boolean initialized;
93  
94      /**
95       * Creates new instance of <tt>SSLIOSession</tt> class.
96       *
97       * @param session I/O session to be decorated with the TLS/SSL capabilities.
98       * @param defaultMode default mode (client or server)
99       * @param sslContext SSL context to use for this I/O session.
100      * @param handler optional SSL setup handler. May be <code>null</code>.
101      */
102     public SSLIOSession(
103             final IOSession session,
104             final SSLMode defaultMode,
105             final SSLContext sslContext,
106             final SSLSetupHandler handler) {
107         super();
108         if (session == null) {
109             throw new IllegalArgumentException("IO session may not be null");
110         }
111         if (sslContext == null) {
112             throw new IllegalArgumentException("SSL context may not be null");
113         }
114         this.session = session;
115         this.defaultMode = defaultMode;
116         this.appEventMask = session.getEventMask();
117         this.channel = new InternalByteChannel();
118         this.handler = handler;
119 
120         // Override the status buffer interface
121         this.session.setBufferStatus(this);
122 
123         SocketAddress address = session.getRemoteAddress();
124         if (address instanceof InetSocketAddress) {
125             String hostname = ((InetSocketAddress) address).getHostName();
126             int port = ((InetSocketAddress) address).getPort();
127             this.sslEngine = sslContext.createSSLEngine(hostname, port);
128         } else {
129             this.sslEngine = sslContext.createSSLEngine();
130         }
131 
132         // Allocate buffers for network (encrypted) data
133         int netBuffersize = this.sslEngine.getSession().getPacketBufferSize();
134         this.inEncrypted = ByteBuffer.allocate(netBuffersize);
135         this.outEncrypted = ByteBuffer.allocate(netBuffersize);
136 
137         // Allocate buffers for application (unencrypted) data
138         int appBuffersize = this.sslEngine.getSession().getApplicationBufferSize();
139         this.inPlain = ByteBuffer.allocate(appBuffersize);
140         this.outPlain = ByteBuffer.allocate(appBuffersize);
141     }
142 
143     protected SSLSetupHandler getSSLSetupHandler() {
144         return this.handler;
145     }
146 
147     /**
148      * Returns <code>true</code> is the session has been fully initialized,
149      * <code>false</code> otherwise.
150      */
151     public boolean isInitialized() {
152         return this.initialized;
153     }
154 
155     /**
156      * Initializes the session in the given {@link SSLMode}. This method
157      * invokes the {@link SSLSetupHandler#initalize(SSLEngine)} callback
158      * if an instance of {@link SSLSetupHandler} was specified at
159      * the construction time.
160      *
161      * @param mode mode of operation (client or server).
162      * @throws SSLException in case of a SSL protocol exception.
163      * @throws IllegalStateException if the session has already been initialized.
164      */
165     public synchronized void initialize(final SSLMode mode) throws SSLException {
166         if (this.initialized) {
167             throw new IllegalStateException("SSL I/O session already initialized");
168         }
169         if (this.status >= IOSession.CLOSING) {
170             return;
171         }
172         switch (mode) {
173         case CLIENT:
174             this.sslEngine.setUseClientMode(true);
175             break;
176         case SERVER:
177             this.sslEngine.setUseClientMode(false);
178             break;
179         }
180         if (this.handler != null) {
181             this.handler.initalize(this.sslEngine);
182         }
183         this.initialized = true;
184         this.sslEngine.beginHandshake();
185         doHandshake();
186     }
187 
188     /**
189      * Initializes the session in the default operation mode. This method
190      * invokes the {@link SSLSetupHandler#initalize(SSLEngine)} callback
191      * if an instance of {@link SSLSetupHandler} was specified at
192      * the construction time.
193      *
194      * @throws SSLException in case of a SSL protocol exception.
195      * @throws IllegalStateException if the session has already been initialized.
196      */
197     public void initialize() throws SSLException {
198         initialize(this.defaultMode);
199     }
200 
201     public synchronized SSLSession getSSLSession() {
202         return this.sslEngine.getSession();
203     }
204 
205     // A works-around for exception handling craziness in Sun/Oracle's SSLEngine
206     // implementation.
207     //
208     // sun.security.pkcs11.wrapper.PKCS11Exception is re-thrown as
209     // plain RuntimeException in sun.security.ssl.Handshaker#checkThrown
210     private SSLException convert(final RuntimeException ex) throws SSLException {
211         Throwable cause = ex.getCause();
212         if (cause == null) {
213             cause = ex;
214         }
215         return new SSLException(cause);
216     }
217 
218     private SSLEngineResult doWrap(final ByteBuffer src, final ByteBuffer dst) throws SSLException {
219         try {
220             return this.sslEngine.wrap(src, dst);
221         } catch (RuntimeException ex) {
222             throw convert(ex);
223         }
224     }
225 
226     private SSLEngineResult doUnwrap(final ByteBuffer src, final ByteBuffer dst) throws SSLException {
227         try {
228             return this.sslEngine.unwrap(src, dst);
229         } catch (RuntimeException ex) {
230             throw convert(ex);
231         }
232     }
233 
234     private void doRunTask() throws SSLException {
235         try {
236             Runnable r = this.sslEngine.getDelegatedTask();
237             if (r != null) {
238                 r.run();
239             }
240         } catch (RuntimeException ex) {
241             throw convert(ex);
242         }
243     }
244 
245     private void doHandshake() throws SSLException {
246         boolean handshaking = true;
247 
248         SSLEngineResult result = null;
249         while (handshaking) {
250             switch (this.sslEngine.getHandshakeStatus()) {
251             case NEED_WRAP:
252                 // Generate outgoing handshake data
253                 this.outPlain.flip();
254                 result = doWrap(this.outPlain, this.outEncrypted);
255                 this.outPlain.compact();
256                 if (result.getStatus() != Status.OK) {
257                     handshaking = false;
258                 }
259                 break;
260             case NEED_UNWRAP:
261                 // Process incoming handshake data
262                 this.inEncrypted.flip();
263                 result = doUnwrap(this.inEncrypted, this.inPlain);
264                 this.inEncrypted.compact();
265                 if (result.getStatus() != Status.OK) {
266                     handshaking = false;
267                 }
268                 break;
269             case NEED_TASK:
270                 doRunTask();
271                 break;
272             case NOT_HANDSHAKING:
273                 handshaking = false;
274                 break;
275             case FINISHED:
276                 break;
277             }
278         }
279 
280         // The SSLEngine has just finished handshaking. This value is only generated by a call
281         // to SSLEngine.wrap()/unwrap() when that call finishes a handshake.
282         // It is never generated by SSLEngine.getHandshakeStatus().
283         if (result != null && result.getHandshakeStatus() == HandshakeStatus.FINISHED) {
284             if (this.handler != null) {
285                 this.handler.verify(this.session, this.sslEngine.getSession());
286             }
287         }
288     }
289 
290     private void updateEventMask() {
291         // Graceful session termination
292         if (this.status == CLOSING && this.sslEngine.isOutboundDone()
293                 && (this.endOfStream || this.sslEngine.isInboundDone())) {
294             this.status = CLOSED;
295         }
296         // Abnormal session termination
297         if (this.status == ACTIVE && this.endOfStream
298                 && this.sslEngine.getHandshakeStatus() == HandshakeStatus.NEED_UNWRAP) {
299             this.status = CLOSED;
300         }
301         if (this.status == CLOSED) {
302             this.session.close();
303             return;
304         }
305         // Need to toggle the event mask for this channel?
306         int oldMask = this.session.getEventMask();
307         int newMask = oldMask;
308         switch (this.sslEngine.getHandshakeStatus()) {
309         case NEED_WRAP:
310             newMask = EventMask.READ_WRITE;
311             break;
312         case NEED_UNWRAP:
313             newMask = EventMask.READ;
314             break;
315         case NOT_HANDSHAKING:
316             newMask = this.appEventMask;
317             break;
318         case NEED_TASK:
319             break;
320         case FINISHED:
321             break;
322         }
323 
324         // Do we have encrypted data ready to be sent?
325         if (this.outEncrypted.position() > 0) {
326             newMask = newMask | EventMask.WRITE;
327         }
328 
329         // Update the mask if necessary
330         if (oldMask != newMask) {
331             this.session.setEventMask(newMask);
332         }
333     }
334 
335     private int sendEncryptedData() throws IOException {
336         this.outEncrypted.flip();
337         int bytesWritten = this.session.channel().write(this.outEncrypted);
338         this.outEncrypted.compact();
339         return bytesWritten;
340     }
341 
342     private int receiveEncryptedData() throws IOException {
343         if (this.endOfStream) {
344             return -1;
345         }
346         return this.session.channel().read(this.inEncrypted);
347     }
348 
349     private boolean decryptData() throws SSLException {
350         boolean decrypted = false;
351         while (this.inEncrypted.position() > 0) {
352             this.inEncrypted.flip();
353             SSLEngineResult result = doUnwrap(this.inEncrypted, this.inPlain);
354             this.inEncrypted.compact();
355             if (result.getStatus() == Status.OK) {
356                 decrypted = true;
357             } else {
358                 break;
359             }
360             if (result.getHandshakeStatus() != HandshakeStatus.NOT_HANDSHAKING) {
361                 break;
362             }
363             if (this.endOfStream) {
364                 break;
365             }
366         }
367         return decrypted;
368     }
369 
370     /**
371      * Reads encrypted data and returns whether the channel associated with
372      * this session has any decrypted inbound data available for reading.
373      *
374      * @throws IOException in case of an I/O error.
375      */
376     public synchronized boolean isAppInputReady() throws IOException {
377         int bytesRead = receiveEncryptedData();
378         if (bytesRead == -1) {
379             this.endOfStream = true;
380         }
381         doHandshake();
382         HandshakeStatus status = this.sslEngine.getHandshakeStatus();
383         if (status == HandshakeStatus.NOT_HANDSHAKING || status == HandshakeStatus.FINISHED) {
384             decryptData();
385         }
386         // Some decrypted data is available or at the end of stream
387         return (this.appEventMask & SelectionKey.OP_READ) > 0
388             && (this.inPlain.position() > 0
389                     || (this.appBufferStatus != null && this.appBufferStatus.hasBufferedInput())
390                     || (this.endOfStream && this.status == ACTIVE));
391     }
392 
393     /**
394      * Returns whether the channel associated with this session is ready to
395      * accept outbound unecrypted data for writing.
396      *
397      * @throws IOException - not thrown currently
398      */
399     public synchronized boolean isAppOutputReady() throws IOException {
400         return (this.appEventMask & SelectionKey.OP_WRITE) > 0
401             && this.status == ACTIVE
402             && this.sslEngine.getHandshakeStatus() == HandshakeStatus.NOT_HANDSHAKING;
403     }
404 
405     /**
406      * Executes inbound SSL transport operations.
407      *
408      * @throws IOException - not thrown currently
409      */
410     public synchronized void inboundTransport() throws IOException {
411         updateEventMask();
412     }
413 
414     /**
415      * Sends encrypted data and executes outbound SSL transport operations.
416      *
417      * @throws IOException in case of an I/O error.
418      */
419     public synchronized void outboundTransport() throws IOException {
420         sendEncryptedData();
421         doHandshake();
422         updateEventMask();
423     }
424 
425     /**
426      * Returns whether the session will produce any more inbound data.
427      */
428     public synchronized boolean isInboundDone() {
429         return this.sslEngine.isInboundDone();
430     }
431 
432     /**
433      * Returns whether the session will accept any more outbound data.
434      */
435     public synchronized boolean isOutboundDone() {
436         return this.sslEngine.isOutboundDone();
437     }
438 
439     private synchronized int writePlain(final ByteBuffer src) throws SSLException {
440         if (src == null) {
441             throw new IllegalArgumentException("Byte buffer may not be null");
442         }
443         if (this.status != ACTIVE) {
444             return -1;
445         }
446         if (this.outPlain.position() > 0) {
447             this.outPlain.flip();
448             doWrap(this.outPlain, this.outEncrypted);
449             this.outPlain.compact();
450         }
451         if (this.outPlain.position() == 0) {
452             SSLEngineResult result = doWrap(src, this.outEncrypted);
453             if (result.getStatus() == Status.CLOSED) {
454                 this.status = CLOSED;
455             }
456             return result.bytesConsumed();
457         } else {
458             return 0;
459         }
460     }
461 
462     private synchronized int readPlain(final ByteBuffer dst) {
463         if (dst == null) {
464             throw new IllegalArgumentException("Byte buffer may not be null");
465         }
466         if (this.inPlain.position() > 0) {
467             this.inPlain.flip();
468             int n = Math.min(this.inPlain.remaining(), dst.remaining());
469             for (int i = 0; i < n; i++) {
470                 dst.put(this.inPlain.get());
471             }
472             this.inPlain.compact();
473             return n;
474         } else {
475             if (this.endOfStream) {
476                 return -1;
477             } else {
478                 return 0;
479             }
480         }
481     }
482 
483     public synchronized void close() {
484         if (this.status >= CLOSING) {
485             return;
486         }
487         this.status = CLOSING;
488         this.sslEngine.closeOutbound();
489         updateEventMask();
490     }
491 
492     public synchronized void shutdown() {
493         if (this.status == CLOSED) {
494             return;
495         }
496         this.status = CLOSED;
497         this.session.shutdown();
498     }
499 
500     public int getStatus() {
501         return this.status;
502     }
503 
504     public boolean isClosed() {
505         return this.status >= CLOSING || this.session.isClosed();
506     }
507 
508     public ByteChannel channel() {
509         return this.channel;
510     }
511 
512     public SocketAddress getLocalAddress() {
513         return this.session.getLocalAddress();
514     }
515 
516     public SocketAddress getRemoteAddress() {
517         return this.session.getRemoteAddress();
518     }
519 
520     public synchronized int getEventMask() {
521         return this.appEventMask;
522     }
523 
524     public synchronized void setEventMask(int ops) {
525         this.appEventMask = ops;
526         updateEventMask();
527     }
528 
529     public synchronized void setEvent(int op) {
530         this.appEventMask = this.appEventMask | op;
531         updateEventMask();
532     }
533 
534     public synchronized void clearEvent(int op) {
535         this.appEventMask = this.appEventMask & ~op;
536         updateEventMask();
537     }
538 
539     public int getSocketTimeout() {
540         return this.session.getSocketTimeout();
541     }
542 
543     public void setSocketTimeout(int timeout) {
544         this.session.setSocketTimeout(timeout);
545     }
546 
547     public synchronized boolean hasBufferedInput() {
548         return (this.appBufferStatus != null && this.appBufferStatus.hasBufferedInput())
549             || this.inEncrypted.position() > 0
550             || this.inPlain.position() > 0;
551     }
552 
553     public synchronized boolean hasBufferedOutput() {
554         return (this.appBufferStatus != null && this.appBufferStatus.hasBufferedOutput())
555             || this.outEncrypted.position() > 0
556             || this.outPlain.position() > 0;
557     }
558 
559     public synchronized void setBufferStatus(final SessionBufferStatus status) {
560         this.appBufferStatus = status;
561     }
562 
563     public Object getAttribute(final String name) {
564         return this.session.getAttribute(name);
565     }
566 
567     public Object removeAttribute(final String name) {
568         return this.session.removeAttribute(name);
569     }
570 
571     public void setAttribute(final String name, final Object obj) {
572         this.session.setAttribute(name, obj);
573     }
574 
575     private static void formatOps(final StringBuilder buffer, int ops) {
576         if ((ops & SelectionKey.OP_READ) > 0) {
577             buffer.append('r');
578         }
579         if ((ops & SelectionKey.OP_WRITE) > 0) {
580             buffer.append('w');
581         }
582     }
583 
584     @Override
585     public synchronized String toString() {
586         StringBuilder buffer = new StringBuilder();
587         buffer.append(this.session);
588         buffer.append("[");
589         switch (this.status) {
590         case ACTIVE:
591             buffer.append("ACTIVE");
592             break;
593         case CLOSING:
594             buffer.append("CLOSING");
595             break;
596         case CLOSED:
597             buffer.append("CLOSED");
598             break;
599         }
600         buffer.append("][");
601         formatOps(buffer, this.appEventMask);
602         buffer.append("][");
603         buffer.append(this.sslEngine.getHandshakeStatus());
604         if (this.sslEngine.isInboundDone()) {
605             buffer.append("][inbound done][");
606         }
607         if (this.sslEngine.isOutboundDone()) {
608             buffer.append("][outbound done][");
609         }
610         if (this.endOfStream) {
611             buffer.append("][EOF][");
612         }
613         buffer.append("][");
614         buffer.append(this.inEncrypted.position());
615         buffer.append("][");
616         buffer.append(this.inPlain.position());
617         buffer.append("][");
618         buffer.append(this.outEncrypted.position());
619         buffer.append("][");
620         buffer.append(this.outPlain.position());
621         buffer.append("]");
622         return buffer.toString();
623     }
624     
625     public Socket getSocket(){
626     	if (this.session instanceof SocketAccessor){
627     		return ((SocketAccessor) this.session).getSocket();
628     	} else {
629     		return null;
630     	}
631     }
632 
633     private class InternalByteChannel implements ByteChannel {
634 
635         public int write(final ByteBuffer src) throws IOException {
636             return SSLIOSession.this.writePlain(src);
637         }
638 
639         public int read(final ByteBuffer dst) throws IOException {
640             return SSLIOSession.this.readPlain(dst);
641         }
642 
643         public void close() throws IOException {
644             SSLIOSession.this.close();
645         }
646 
647         public boolean isOpen() {
648             return !SSLIOSession.this.isClosed();
649         }
650 
651     }
652 
653 }