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