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 SSLMode defaultMode;
82      private final SSLEngine sslEngine;
83      private final ByteBuffer inEncrypted;
84      private final ByteBuffer outEncrypted;
85      private final ByteBuffer inPlain;
86      private final ByteBuffer outPlain;
87      private final InternalByteChannel channel;
88      private final SSLSetupHandler handler;
89  
90      private int appEventMask;
91      private SessionBufferStatus appBufferStatus;
92  
93      private boolean endOfStream;
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 defaultMode default 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 defaultMode,
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.defaultMode = defaultMode;
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         final SocketAddress address = session.getRemoteAddress();
123         if (address instanceof InetSocketAddress) {
124             final String hostname = ((InetSocketAddress) address).getHostName();
125             final int port = ((InetSocketAddress) address).getPort();
126             this.sslEngine = sslContext.createSSLEngine(hostname, port);
127         } else {
128             this.sslEngine = sslContext.createSSLEngine();
129         }
130 
131         // Allocate buffers for network (encrypted) data
132         final int netBuffersize = this.sslEngine.getSession().getPacketBufferSize();
133         this.inEncrypted = ByteBuffer.allocate(netBuffersize);
134         this.outEncrypted = ByteBuffer.allocate(netBuffersize);
135 
136         // Allocate buffers for application (unencrypted) data
137         final int appBuffersize = this.sslEngine.getSession().getApplicationBufferSize();
138         this.inPlain = ByteBuffer.allocate(appBuffersize);
139         this.outPlain = ByteBuffer.allocate(appBuffersize);
140     }
141 
142     protected SSLSetupHandler getSSLSetupHandler() {
143         return this.handler;
144     }
145 
146     /**
147      * Returns <code>true</code> is the session has been fully initialized,
148      * <code>false</code> otherwise.
149      */
150     public boolean isInitialized() {
151         return this.initialized;
152     }
153 
154     /**
155      * Initializes the session in the given {@link SSLMode}. This method
156      * invokes the {@link SSLSetupHandler#initalize(SSLEngine)} callback
157      * if an instance of {@link SSLSetupHandler} was specified at
158      * the construction time.
159      *
160      * @param mode mode of operation (client or server).
161      * @throws SSLException in case of a SSL protocol exception.
162      * @throws IllegalStateException if the session has already been initialized.
163      */
164     public synchronized void initialize(final SSLMode mode) throws SSLException {
165         Asserts.check(!this.initialized, "SSL I/O session already initialized");
166         if (this.status >= IOSession.CLOSING) {
167             return;
168         }
169         switch (mode) {
170         case CLIENT:
171             this.sslEngine.setUseClientMode(true);
172             break;
173         case SERVER:
174             this.sslEngine.setUseClientMode(false);
175             break;
176         }
177         if (this.handler != null) {
178             this.handler.initalize(this.sslEngine);
179         }
180         this.initialized = true;
181         this.sslEngine.beginHandshake();
182         doHandshake();
183     }
184 
185     /**
186      * Initializes the session in the default operation mode. This method
187      * invokes the {@link SSLSetupHandler#initalize(SSLEngine)} callback
188      * if an instance of {@link SSLSetupHandler} was specified at
189      * the construction time.
190      *
191      * @throws SSLException in case of a SSL protocol exception.
192      * @throws IllegalStateException if the session has already been initialized.
193      */
194     public void initialize() throws SSLException {
195         initialize(this.defaultMode);
196     }
197 
198     public synchronized SSLSession getSSLSession() {
199         return this.sslEngine.getSession();
200     }
201 
202     // A works-around for exception handling craziness in Sun/Oracle's SSLEngine
203     // implementation.
204     //
205     // sun.security.pkcs11.wrapper.PKCS11Exception is re-thrown as
206     // plain RuntimeException in sun.security.ssl.Handshaker#checkThrown
207     private SSLException convert(final RuntimeException ex) throws SSLException {
208         Throwable cause = ex.getCause();
209         if (cause == null) {
210             cause = ex;
211         }
212         return new SSLException(cause);
213     }
214 
215     private SSLEngineResult doWrap(final ByteBuffer src, final ByteBuffer dst) throws SSLException {
216         try {
217             return this.sslEngine.wrap(src, dst);
218         } catch (final RuntimeException ex) {
219             throw convert(ex);
220         }
221     }
222 
223     private SSLEngineResult doUnwrap(final ByteBuffer src, final ByteBuffer dst) throws SSLException {
224         try {
225             return this.sslEngine.unwrap(src, dst);
226         } catch (final RuntimeException ex) {
227             throw convert(ex);
228         }
229     }
230 
231     private void doRunTask() throws SSLException {
232         try {
233             final Runnable r = this.sslEngine.getDelegatedTask();
234             if (r != null) {
235                 r.run();
236             }
237         } catch (final RuntimeException ex) {
238             throw convert(ex);
239         }
240     }
241 
242     private void doHandshake() throws SSLException {
243         boolean handshaking = true;
244 
245         SSLEngineResult result = null;
246         while (handshaking) {
247             switch (this.sslEngine.getHandshakeStatus()) {
248             case NEED_WRAP:
249                 // Generate outgoing handshake data
250                 this.outPlain.flip();
251                 result = doWrap(this.outPlain, this.outEncrypted);
252                 this.outPlain.compact();
253                 if (result.getStatus() != Status.OK) {
254                     handshaking = false;
255                 }
256                 break;
257             case NEED_UNWRAP:
258                 // Process incoming handshake data
259                 this.inEncrypted.flip();
260                 result = doUnwrap(this.inEncrypted, this.inPlain);
261                 this.inEncrypted.compact();
262                 if (result.getStatus() != Status.OK) {
263                     handshaking = false;
264                 }
265                 break;
266             case NEED_TASK:
267                 doRunTask();
268                 break;
269             case NOT_HANDSHAKING:
270                 handshaking = false;
271                 break;
272             case FINISHED:
273                 break;
274             }
275         }
276 
277         // The SSLEngine has just finished handshaking. This value is only generated by a call
278         // to SSLEngine.wrap()/unwrap() when that call finishes a handshake.
279         // It is never generated by SSLEngine.getHandshakeStatus().
280         if (result != null && result.getHandshakeStatus() == HandshakeStatus.FINISHED) {
281             if (this.handler != null) {
282                 this.handler.verify(this.session, this.sslEngine.getSession());
283             }
284         }
285     }
286 
287     private void updateEventMask() {
288         // Graceful session termination
289         if (this.status == CLOSING && this.sslEngine.isOutboundDone()
290                 && (this.endOfStream || this.sslEngine.isInboundDone())) {
291             this.status = CLOSED;
292         }
293         // Abnormal session termination
294         if (this.status == ACTIVE && this.endOfStream
295                 && this.sslEngine.getHandshakeStatus() == HandshakeStatus.NEED_UNWRAP) {
296             this.status = CLOSED;
297         }
298         if (this.status == CLOSED) {
299             this.session.close();
300             return;
301         }
302         // Need to toggle the event mask for this channel?
303         final int oldMask = this.session.getEventMask();
304         int newMask = oldMask;
305         switch (this.sslEngine.getHandshakeStatus()) {
306         case NEED_WRAP:
307             newMask = EventMask.READ_WRITE;
308             break;
309         case NEED_UNWRAP:
310             newMask = EventMask.READ;
311             break;
312         case NOT_HANDSHAKING:
313             newMask = this.appEventMask;
314             break;
315         case NEED_TASK:
316             break;
317         case FINISHED:
318             break;
319         }
320 
321         // Do we have encrypted data ready to be sent?
322         if (this.outEncrypted.position() > 0) {
323             newMask = newMask | EventMask.WRITE;
324         }
325 
326         // Update the mask if necessary
327         if (oldMask != newMask) {
328             this.session.setEventMask(newMask);
329         }
330     }
331 
332     private int sendEncryptedData() throws IOException {
333         this.outEncrypted.flip();
334         final int bytesWritten = this.session.channel().write(this.outEncrypted);
335         this.outEncrypted.compact();
336         return bytesWritten;
337     }
338 
339     private int receiveEncryptedData() throws IOException {
340         if (this.endOfStream) {
341             return -1;
342         }
343         return this.session.channel().read(this.inEncrypted);
344     }
345 
346     private boolean decryptData() throws SSLException {
347         boolean decrypted = false;
348         while (this.inEncrypted.position() > 0) {
349             this.inEncrypted.flip();
350             final SSLEngineResult result = doUnwrap(this.inEncrypted, this.inPlain);
351             this.inEncrypted.compact();
352             if (result.getStatus() == Status.OK) {
353                 decrypted = true;
354             } else {
355                 break;
356             }
357             if (result.getHandshakeStatus() != HandshakeStatus.NOT_HANDSHAKING) {
358                 break;
359             }
360             if (this.endOfStream) {
361                 break;
362             }
363         }
364         return decrypted;
365     }
366 
367     /**
368      * Reads encrypted data and returns whether the channel associated with
369      * this session has any decrypted inbound data available for reading.
370      *
371      * @throws IOException in case of an I/O error.
372      */
373     public synchronized boolean isAppInputReady() throws IOException {
374         final int bytesRead = receiveEncryptedData();
375         if (bytesRead == -1) {
376             this.endOfStream = true;
377         }
378         doHandshake();
379         final HandshakeStatus status = this.sslEngine.getHandshakeStatus();
380         if (status == HandshakeStatus.NOT_HANDSHAKING || status == HandshakeStatus.FINISHED) {
381             decryptData();
382         }
383         // Some decrypted data is available or at the end of stream
384         return (this.appEventMask & SelectionKey.OP_READ) > 0
385             && (this.inPlain.position() > 0
386                     || (this.appBufferStatus != null && this.appBufferStatus.hasBufferedInput())
387                     || (this.endOfStream && this.status == ACTIVE));
388     }
389 
390     /**
391      * Returns whether the channel associated with this session is ready to
392      * accept outbound unecrypted data for writing.
393      *
394      * @throws IOException - not thrown currently
395      */
396     public synchronized boolean isAppOutputReady() throws IOException {
397         return (this.appEventMask & SelectionKey.OP_WRITE) > 0
398             && this.status == ACTIVE
399             && this.sslEngine.getHandshakeStatus() == HandshakeStatus.NOT_HANDSHAKING;
400     }
401 
402     /**
403      * Executes inbound SSL transport operations.
404      *
405      * @throws IOException - not thrown currently
406      */
407     public synchronized void inboundTransport() throws IOException {
408         updateEventMask();
409     }
410 
411     /**
412      * Sends encrypted data and executes outbound SSL transport operations.
413      *
414      * @throws IOException in case of an I/O error.
415      */
416     public synchronized void outboundTransport() throws IOException {
417         sendEncryptedData();
418         doHandshake();
419         updateEventMask();
420     }
421 
422     /**
423      * Returns whether the session will produce any more inbound data.
424      */
425     public synchronized boolean isInboundDone() {
426         return this.sslEngine.isInboundDone();
427     }
428 
429     /**
430      * Returns whether the session will accept any more outbound data.
431      */
432     public synchronized boolean isOutboundDone() {
433         return this.sslEngine.isOutboundDone();
434     }
435 
436     private synchronized int writePlain(final ByteBuffer src) throws SSLException {
437         Args.notNull(src, "Byte buffer");
438         if (this.status != ACTIVE) {
439             return -1;
440         }
441         if (this.outPlain.position() > 0) {
442             this.outPlain.flip();
443             doWrap(this.outPlain, this.outEncrypted);
444             this.outPlain.compact();
445         }
446         if (this.outPlain.position() == 0) {
447             final SSLEngineResult result = doWrap(src, this.outEncrypted);
448             if (result.getStatus() == Status.CLOSED) {
449                 this.status = CLOSED;
450             }
451             return result.bytesConsumed();
452         } else {
453             return 0;
454         }
455     }
456 
457     private synchronized int readPlain(final ByteBuffer dst) {
458         Args.notNull(dst, "Byte buffer");
459         if (this.inPlain.position() > 0) {
460             this.inPlain.flip();
461             final int n = Math.min(this.inPlain.remaining(), dst.remaining());
462             for (int i = 0; i < n; i++) {
463                 dst.put(this.inPlain.get());
464             }
465             this.inPlain.compact();
466             return n;
467         } else {
468             if (this.endOfStream) {
469                 return -1;
470             } else {
471                 return 0;
472             }
473         }
474     }
475 
476     public synchronized void close() {
477         if (this.status >= CLOSING) {
478             return;
479         }
480         this.status = CLOSING;
481         this.sslEngine.closeOutbound();
482         updateEventMask();
483     }
484 
485     public synchronized void shutdown() {
486         if (this.status == CLOSED) {
487             return;
488         }
489         this.status = CLOSED;
490         this.session.shutdown();
491     }
492 
493     public int getStatus() {
494         return this.status;
495     }
496 
497     public boolean isClosed() {
498         return this.status >= CLOSING || this.session.isClosed();
499     }
500 
501     public ByteChannel channel() {
502         return this.channel;
503     }
504 
505     public SocketAddress getLocalAddress() {
506         return this.session.getLocalAddress();
507     }
508 
509     public SocketAddress getRemoteAddress() {
510         return this.session.getRemoteAddress();
511     }
512 
513     public synchronized int getEventMask() {
514         return this.appEventMask;
515     }
516 
517     public synchronized void setEventMask(final int ops) {
518         this.appEventMask = ops;
519         updateEventMask();
520     }
521 
522     public synchronized void setEvent(final int op) {
523         this.appEventMask = this.appEventMask | op;
524         updateEventMask();
525     }
526 
527     public synchronized void clearEvent(final int op) {
528         this.appEventMask = this.appEventMask & ~op;
529         updateEventMask();
530     }
531 
532     public int getSocketTimeout() {
533         return this.session.getSocketTimeout();
534     }
535 
536     public void setSocketTimeout(final int timeout) {
537         this.session.setSocketTimeout(timeout);
538     }
539 
540     public synchronized boolean hasBufferedInput() {
541         return (this.appBufferStatus != null && this.appBufferStatus.hasBufferedInput())
542             || this.inEncrypted.position() > 0
543             || this.inPlain.position() > 0;
544     }
545 
546     public synchronized boolean hasBufferedOutput() {
547         return (this.appBufferStatus != null && this.appBufferStatus.hasBufferedOutput())
548             || this.outEncrypted.position() > 0
549             || this.outPlain.position() > 0;
550     }
551 
552     public synchronized void setBufferStatus(final SessionBufferStatus status) {
553         this.appBufferStatus = status;
554     }
555 
556     public Object getAttribute(final String name) {
557         return this.session.getAttribute(name);
558     }
559 
560     public Object removeAttribute(final String name) {
561         return this.session.removeAttribute(name);
562     }
563 
564     public void setAttribute(final String name, final Object obj) {
565         this.session.setAttribute(name, obj);
566     }
567 
568     private static void formatOps(final StringBuilder buffer, int ops) {
569         if ((ops & SelectionKey.OP_READ) > 0) {
570             buffer.append('r');
571         }
572         if ((ops & SelectionKey.OP_WRITE) > 0) {
573             buffer.append('w');
574         }
575     }
576 
577     @Override
578     public String toString() {
579         final StringBuilder buffer = new StringBuilder();
580         buffer.append(this.session);
581         buffer.append("[");
582         switch (this.status) {
583         case ACTIVE:
584             buffer.append("ACTIVE");
585             break;
586         case CLOSING:
587             buffer.append("CLOSING");
588             break;
589         case CLOSED:
590             buffer.append("CLOSED");
591             break;
592         }
593         buffer.append("][");
594         formatOps(buffer, this.appEventMask);
595         buffer.append("][");
596         buffer.append(this.sslEngine.getHandshakeStatus());
597         if (this.sslEngine.isInboundDone()) {
598             buffer.append("][inbound done][");
599         }
600         if (this.sslEngine.isOutboundDone()) {
601             buffer.append("][outbound done][");
602         }
603         if (this.endOfStream) {
604             buffer.append("][EOF][");
605         }
606         buffer.append("][");
607         buffer.append(this.inEncrypted.position());
608         buffer.append("][");
609         buffer.append(this.inPlain.position());
610         buffer.append("][");
611         buffer.append(this.outEncrypted.position());
612         buffer.append("][");
613         buffer.append(this.outPlain.position());
614         buffer.append("]");
615         return buffer.toString();
616     }
617 
618     public Socket getSocket(){
619         if (this.session instanceof SocketAccessor){
620             return ((SocketAccessor) this.session).getSocket();
621         } else {
622             return null;
623         }
624     }
625 
626     private class InternalByteChannel implements ByteChannel {
627 
628         public int write(final ByteBuffer src) throws IOException {
629             return SSLIOSession.this.writePlain(src);
630         }
631 
632         public int read(final ByteBuffer dst) throws IOException {
633             return SSLIOSession.this.readPlain(dst);
634         }
635 
636         public void close() throws IOException {
637             SSLIOSession.this.close();
638         }
639 
640         public boolean isOpen() {
641             return !SSLIOSession.this.isClosed();
642         }
643 
644     }
645 
646 }