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.hc.core5.reactor.ssl;
29  
30  import java.io.IOException;
31  import java.net.SocketAddress;
32  import java.nio.ByteBuffer;
33  import java.nio.channels.ByteChannel;
34  import java.nio.channels.CancelledKeyException;
35  import java.nio.channels.ClosedChannelException;
36  import java.nio.channels.SelectionKey;
37  import java.util.concurrent.atomic.AtomicInteger;
38  import java.util.concurrent.atomic.AtomicReference;
39  import java.util.concurrent.locks.Lock;
40  
41  import javax.net.ssl.SSLContext;
42  import javax.net.ssl.SSLEngine;
43  import javax.net.ssl.SSLEngineResult;
44  import javax.net.ssl.SSLEngineResult.HandshakeStatus;
45  import javax.net.ssl.SSLException;
46  import javax.net.ssl.SSLSession;
47  
48  import org.apache.hc.core5.annotation.Contract;
49  import org.apache.hc.core5.annotation.Internal;
50  import org.apache.hc.core5.annotation.ThreadingBehavior;
51  import org.apache.hc.core5.function.Callback;
52  import org.apache.hc.core5.io.CloseMode;
53  import org.apache.hc.core5.net.NamedEndpoint;
54  import org.apache.hc.core5.reactor.Command;
55  import org.apache.hc.core5.reactor.EventMask;
56  import org.apache.hc.core5.reactor.IOEventHandler;
57  import org.apache.hc.core5.reactor.IOSession;
58  import org.apache.hc.core5.util.Args;
59  import org.apache.hc.core5.util.Asserts;
60  import org.apache.hc.core5.util.ReflectionUtils;
61  import org.apache.hc.core5.util.Timeout;
62  
63  /**
64   * {@code SSLIOSession} is a decorator class intended to transparently extend
65   * an {@link IOSession} with transport layer security capabilities based on
66   * the SSL/TLS protocol.
67   *
68   * @since 4.2
69   */
70  @Contract(threading = ThreadingBehavior.SAFE_CONDITIONAL)
71  @Internal
72  public class SSLIOSession implements IOSession {
73  
74      enum TLSHandShakeState { READY, INITIALIZED, HANDSHAKING, COMPLETE }
75  
76      private static final ByteBuffer EMPTY_BUFFER = ByteBuffer.allocate(0);
77  
78      private final NamedEndpoint targetEndpoint;
79      private final IOSession session;
80      private final SSLEngine sslEngine;
81      private final SSLManagedBuffer inEncrypted;
82      private final SSLManagedBuffer outEncrypted;
83      private final SSLManagedBuffer inPlain;
84      private final SSLSessionInitializer initializer;
85      private final SSLSessionVerifier verifier;
86      private final Callback<SSLIOSession> sessionStartCallback;
87      private final Callback<SSLIOSession> sessionEndCallback;
88      private final Timeout connectTimeout;
89      private final SSLMode sslMode;
90      private final AtomicInteger outboundClosedCount;
91      private final AtomicReference<TLSHandShakeState> handshakeStateRef;
92      private final IOEventHandler internalEventHandler;
93  
94      private int appEventMask;
95  
96      private volatile boolean endOfStream;
97      private volatile Status status = Status.ACTIVE;
98      private volatile Timeout socketTimeout;
99      private volatile TlsDetails tlsDetails;
100 
101     /**
102      * Creates new instance of {@code SSLIOSession} class.
103      *
104      * @param session I/O session to be decorated with the TLS/SSL capabilities.
105      * @param sslMode SSL mode (client or server)
106      * @param targetEndpoint target endpoint (applicable in client mode only). May be {@code null}.
107      * @param sslContext SSL context to use for this I/O session.
108      * @param sslBufferMode buffer management mode
109      * @param initializer optional SSL session initializer. May be {@code null}.
110      * @param verifier optional SSL session verifier. May be {@code null}.
111      * @param connectTimeout timeout to apply for the TLS/SSL handshake. May be {@code null}.
112      *
113      * @since 5.0
114      */
115     public SSLIOSession(
116             final NamedEndpoint targetEndpoint,
117             final IOSession session,
118             final SSLMode sslMode,
119             final SSLContext sslContext,
120             final SSLBufferMode sslBufferMode,
121             final SSLSessionInitializer initializer,
122             final SSLSessionVerifier verifier,
123             final Callback<SSLIOSession> sessionStartCallback,
124             final Callback<SSLIOSession> sessionEndCallback,
125             final Timeout connectTimeout) {
126         super();
127         Args.notNull(session, "IO session");
128         Args.notNull(sslContext, "SSL context");
129         this.targetEndpoint = targetEndpoint;
130         this.session = session;
131         this.sslMode = sslMode;
132         this.initializer = initializer;
133         this.verifier = verifier;
134         this.sessionStartCallback = sessionStartCallback;
135         this.sessionEndCallback = sessionEndCallback;
136 
137         this.appEventMask = session.getEventMask();
138         if (this.sslMode == SSLMode.CLIENT && targetEndpoint != null) {
139             this.sslEngine = sslContext.createSSLEngine(targetEndpoint.getHostName(), targetEndpoint.getPort());
140         } else {
141             this.sslEngine = sslContext.createSSLEngine();
142         }
143 
144         final SSLSession sslSession = this.sslEngine.getSession();
145         // Allocate buffers for network (encrypted) data
146         final int netBufferSize = sslSession.getPacketBufferSize();
147         this.inEncrypted = SSLManagedBuffer.create(sslBufferMode, netBufferSize);
148         this.outEncrypted = SSLManagedBuffer.create(sslBufferMode, netBufferSize);
149 
150         // Allocate buffers for application (unencrypted) data
151         final int appBufferSize = sslSession.getApplicationBufferSize();
152         this.inPlain = SSLManagedBuffer.create(sslBufferMode, appBufferSize);
153         this.outboundClosedCount = new AtomicInteger(0);
154         this.handshakeStateRef = new AtomicReference<>(TLSHandShakeState.READY);
155         this.connectTimeout = connectTimeout;
156         this.internalEventHandler = new IOEventHandler() {
157 
158             @Override
159             public void connected(final IOSession protocolSession) throws IOException {
160                 if (handshakeStateRef.compareAndSet(TLSHandShakeState.READY, TLSHandShakeState.INITIALIZED)) {
161                     initialize(protocolSession);
162                 }
163             }
164 
165             @Override
166             public void inputReady(final IOSession protocolSession, final ByteBuffer src) throws IOException {
167                 if (handshakeStateRef.compareAndSet(TLSHandShakeState.READY, TLSHandShakeState.INITIALIZED)) {
168                     initialize(protocolSession);
169                 }
170                 receiveEncryptedData();
171                 doHandshake(protocolSession);
172                 decryptData(protocolSession);
173                 updateEventMask();
174             }
175 
176             @Override
177             public void outputReady(final IOSession protocolSession) throws IOException {
178                 if (handshakeStateRef.compareAndSet(TLSHandShakeState.READY, TLSHandShakeState.INITIALIZED)) {
179                     initialize(protocolSession);
180                 }
181                 encryptData(protocolSession);
182                 sendEncryptedData();
183                 doHandshake(protocolSession);
184                 updateEventMask();
185             }
186 
187             @Override
188             public void timeout(final IOSession protocolSession, final Timeout timeout) throws IOException {
189                 if (sslEngine.isInboundDone() && !sslEngine.isInboundDone()) {
190                     // The session failed to terminate cleanly
191                     close(CloseMode.IMMEDIATE);
192                 }
193                 ensureHandler().timeout(protocolSession, timeout);
194             }
195 
196             @Override
197             public void exception(final IOSession protocolSession, final Exception cause) {
198                 final IOEventHandler handler = session.getHandler();
199                 if (handshakeStateRef.get() != TLSHandShakeState.COMPLETE) {
200                     session.close(CloseMode.GRACEFUL);
201                     close(CloseMode.IMMEDIATE);
202                 }
203                 if (handler != null) {
204                     handler.exception(protocolSession, cause);
205                 }
206             }
207 
208             @Override
209             public void disconnected(final IOSession protocolSession) {
210                 final IOEventHandler handler = session.getHandler();
211                 if (handler != null) {
212                     handler.disconnected(protocolSession);
213                 }
214             }
215 
216         };
217 
218     }
219 
220     private IOEventHandler ensureHandler() {
221         final IOEventHandler handler = session.getHandler();
222         Asserts.notNull(handler, "IO event handler");
223         return handler;
224     }
225 
226     @Override
227     public IOEventHandler getHandler() {
228         return internalEventHandler;
229     }
230 
231     private void initialize(final IOSession protocolSession) throws IOException {
232         // Save the initial socketTimeout of the underlying IOSession, to be restored after the handshake is finished
233         this.socketTimeout = this.session.getSocketTimeout();
234         if (connectTimeout != null) {
235             this.session.setSocketTimeout(connectTimeout);
236         }
237 
238         this.session.getLock().lock();
239         try {
240             if (this.status.compareTo(Status.CLOSING) >= 0) {
241                 return;
242             }
243             switch (this.sslMode) {
244                 case CLIENT:
245                     this.sslEngine.setUseClientMode(true);
246                     break;
247                 case SERVER:
248                     this.sslEngine.setUseClientMode(false);
249                     break;
250             }
251             if (this.initializer != null) {
252                 this.initializer.initialize(this.targetEndpoint, this.sslEngine);
253             }
254             this.handshakeStateRef.set(TLSHandShakeState.HANDSHAKING);
255             this.sslEngine.beginHandshake();
256 
257             this.inEncrypted.release();
258             this.outEncrypted.release();
259             doHandshake(protocolSession);
260             updateEventMask();
261         } finally {
262             this.session.getLock().unlock();
263         }
264     }
265 
266     // A works-around for exception handling craziness in Sun/Oracle's SSLEngine
267     // implementation.
268     //
269     // sun.security.pkcs11.wrapper.PKCS11Exception is re-thrown as
270     // plain RuntimeException in sun.security.ssl.Handshaker#checkThrown
271     private SSLException convert(final RuntimeException ex) {
272         Throwable cause = ex.getCause();
273         if (cause == null) {
274             cause = ex;
275         }
276         return new SSLException(cause);
277     }
278 
279     private SSLEngineResult doWrap(final ByteBuffer src, final ByteBuffer dst) throws SSLException {
280         try {
281             return this.sslEngine.wrap(src, dst);
282         } catch (final RuntimeException ex) {
283             throw convert(ex);
284         }
285     }
286 
287     private SSLEngineResult doUnwrap(final ByteBuffer src, final ByteBuffer dst) throws SSLException {
288         try {
289             return this.sslEngine.unwrap(src, dst);
290         } catch (final RuntimeException ex) {
291             throw convert(ex);
292         }
293     }
294 
295     private void doRunTask() {
296         final Runnable r = this.sslEngine.getDelegatedTask();
297         if (r != null) {
298             r.run();
299         }
300     }
301 
302     private void doHandshake(final IOSession protocolSession) throws IOException {
303         boolean handshaking = true;
304 
305         SSLEngineResult result = null;
306         while (handshaking) {
307              HandshakeStatus handshakeStatus = this.sslEngine.getHandshakeStatus();
308 
309             // Work-around for what appears to be a bug in Conscrypt SSLEngine that does not
310             // transition into the handshaking state upon #closeOutbound() call but still
311             // has some handshake data stuck in its internal buffer.
312             if (handshakeStatus == HandshakeStatus.NOT_HANDSHAKING && outboundClosedCount.get() > 0) {
313                 handshakeStatus = HandshakeStatus.NEED_WRAP;
314             }
315 
316             switch (handshakeStatus) {
317             case NEED_WRAP:
318                 // Generate outgoing handshake data
319 
320                 this.session.getLock().lock();
321                 try {
322                     // Acquire buffers
323                     final ByteBuffer outEncryptedBuf = this.outEncrypted.acquire();
324 
325                     // Just wrap an empty buffer because there is no data to write.
326                     result = doWrap(EMPTY_BUFFER, outEncryptedBuf);
327 
328                     if (result.getStatus() != SSLEngineResult.Status.OK || result.getHandshakeStatus() == HandshakeStatus.NEED_WRAP) {
329                         handshaking = false;
330                     }
331                     break;
332                 } finally {
333                     this.session.getLock().unlock();
334                 }
335             case NEED_UNWRAP:
336                 // Process incoming handshake data
337 
338                 // Acquire buffers
339                 final ByteBuffer inEncryptedBuf = this.inEncrypted.acquire();
340                 final ByteBuffer inPlainBuf = this.inPlain.acquire();
341 
342                 // Perform operations
343                 inEncryptedBuf.flip();
344                 try {
345                     result = doUnwrap(inEncryptedBuf, inPlainBuf);
346                 } finally {
347                     inEncryptedBuf.compact();
348                 }
349 
350                 try {
351                     if (!inEncryptedBuf.hasRemaining() && result.getHandshakeStatus() == HandshakeStatus.NEED_UNWRAP) {
352                         throw new SSLException("Input buffer is full");
353                     }
354                 } finally {
355                     // Release inEncrypted if empty
356                     if (inEncryptedBuf.position() == 0) {
357                         this.inEncrypted.release();
358                     }
359                 }
360 
361                 if (this.status.compareTo(Status.CLOSING) >= 0) {
362                     this.inPlain.release();
363                 }
364                 if (result.getStatus() != SSLEngineResult.Status.OK) {
365                     handshaking = false;
366                 }
367                 break;
368             case NEED_TASK:
369                 doRunTask();
370                 break;
371             case NOT_HANDSHAKING:
372                 handshaking = false;
373                 break;
374             }
375         }
376 
377         // The SSLEngine has just finished handshaking. This value is only generated by a call
378         // to SSLEngine.wrap()/unwrap() when that call finishes a handshake.
379         // It is never generated by SSLEngine.getHandshakeStatus().
380         if (result != null && result.getHandshakeStatus() == HandshakeStatus.FINISHED) {
381             this.handshakeStateRef.set(TLSHandShakeState.COMPLETE);
382             this.session.setSocketTimeout(this.socketTimeout);
383             if (this.verifier != null) {
384                 this.tlsDetails = this.verifier.verify(this.targetEndpoint, this.sslEngine);
385             }
386             if (this.tlsDetails == null) {
387                 final SSLSession sslSession = this.sslEngine.getSession();
388                 final String applicationProtocol = ReflectionUtils.callGetter(this.sslEngine, "ApplicationProtocol", String.class);
389                 this.tlsDetails = new TlsDetails(sslSession, applicationProtocol);
390             }
391 
392             ensureHandler().connected(protocolSession);
393 
394             if (this.sessionStartCallback != null) {
395                 this.sessionStartCallback.execute(this);
396             }
397         }
398     }
399 
400     private void updateEventMask() {
401         this.session.getLock().lock();
402         try {
403             // Graceful session termination
404             if (this.status == Status.ACTIVE
405                     && (this.endOfStream || this.sslEngine.isInboundDone())) {
406                 this.status = Status.CLOSING;
407             }
408             if (this.status == Status.CLOSING && !this.outEncrypted.hasData()) {
409                 this.sslEngine.closeOutbound();
410                 this.outboundClosedCount.incrementAndGet();
411             }
412             if (this.status == Status.CLOSING && this.sslEngine.isOutboundDone()
413                     && (this.endOfStream || this.sslEngine.isInboundDone())) {
414                 this.status = Status.CLOSED;
415             }
416             // Abnormal session termination
417             if (this.status.compareTo(Status.CLOSING) <= 0 && this.endOfStream
418                     && this.sslEngine.getHandshakeStatus() == HandshakeStatus.NEED_UNWRAP) {
419                 this.status = Status.CLOSED;
420             }
421             if (this.status == Status.CLOSED) {
422                 this.session.close();
423                 if (sessionEndCallback != null) {
424                     sessionEndCallback.execute(this);
425                 }
426                 return;
427             }
428             // Is there a task pending?
429             if (this.sslEngine.getHandshakeStatus() == HandshakeStatus.NEED_TASK) {
430                 doRunTask();
431             }
432             // Need to toggle the event mask for this channel?
433             final int oldMask = this.session.getEventMask();
434             int newMask = oldMask;
435             switch (this.sslEngine.getHandshakeStatus()) {
436                 case NEED_WRAP:
437                     newMask = EventMask.READ_WRITE;
438                     break;
439                 case NEED_UNWRAP:
440                     newMask = EventMask.READ;
441                     break;
442                 case NOT_HANDSHAKING:
443                     newMask = this.appEventMask;
444                     break;
445             }
446 
447             if (this.endOfStream && !this.inPlain.hasData()) {
448                 newMask = newMask & ~EventMask.READ;
449             } else if (this.status == Status.CLOSING) {
450                 newMask = newMask | EventMask.READ;
451             }
452 
453             // Do we have encrypted data ready to be sent?
454             if (this.outEncrypted.hasData()) {
455                 newMask = newMask | EventMask.WRITE;
456             } else if (this.sslEngine.isOutboundDone()) {
457                 newMask = newMask & ~EventMask.WRITE;
458             }
459 
460             // Update the mask if necessary
461             if (oldMask != newMask) {
462                 this.session.setEventMask(newMask);
463             }
464         } finally {
465             this.session.getLock().unlock();
466         }
467     }
468 
469     private int sendEncryptedData() throws IOException {
470         this.session.getLock().lock();
471         try {
472             if (!this.outEncrypted.hasData()) {
473                 // If the buffer isn't acquired or is empty, call write() with an empty buffer.
474                 // This will ensure that tests performed by write() still take place without
475                 // having to acquire and release an empty buffer (e.g. connection closed,
476                 // interrupted thread, etc..)
477                 return this.session.write(EMPTY_BUFFER);
478             }
479 
480             // Acquire buffer
481             final ByteBuffer outEncryptedBuf = this.outEncrypted.acquire();
482 
483             // Clear output buffer if the session has been closed
484             // in case there is still `close_notify` data stuck in it
485             if (this.status == Status.CLOSED) {
486                 outEncryptedBuf.clear();
487             }
488 
489             // Perform operation
490             int bytesWritten = 0;
491             if (outEncryptedBuf.position() > 0) {
492                 outEncryptedBuf.flip();
493                 try {
494                     bytesWritten = this.session.write(outEncryptedBuf);
495                 } finally {
496                     outEncryptedBuf.compact();
497                 }
498             }
499 
500             // Release if empty
501             if (outEncryptedBuf.position() == 0) {
502                 this.outEncrypted.release();
503             }
504             return bytesWritten;
505         } finally {
506             this.session.getLock().unlock();
507         }
508     }
509 
510     private int receiveEncryptedData() throws IOException {
511         if (this.endOfStream) {
512             return -1;
513         }
514 
515         // Acquire buffer
516         final ByteBuffer inEncryptedBuf = this.inEncrypted.acquire();
517 
518         // Perform operation
519         final int bytesRead = this.session.read(inEncryptedBuf);
520 
521         // Release if empty
522         if (inEncryptedBuf.position() == 0) {
523             this.inEncrypted.release();
524         }
525         if (bytesRead == -1) {
526             this.endOfStream = true;
527         }
528         return bytesRead;
529     }
530 
531     private void decryptData(final IOSession protocolSession) throws IOException {
532         final HandshakeStatus handshakeStatus = sslEngine.getHandshakeStatus();
533         if ((handshakeStatus == HandshakeStatus.NOT_HANDSHAKING || handshakeStatus == HandshakeStatus.FINISHED)
534                 && inEncrypted.hasData()) {
535             final ByteBuffer inEncryptedBuf = inEncrypted.acquire();
536             inEncryptedBuf.flip();
537             try {
538                 while (inEncryptedBuf.hasRemaining()) {
539                     final ByteBuffer inPlainBuf = inPlain.acquire();
540                     try {
541                         final SSLEngineResult result = doUnwrap(inEncryptedBuf, inPlainBuf);
542                         if (!inEncryptedBuf.hasRemaining() && result.getHandshakeStatus() == HandshakeStatus.NEED_UNWRAP) {
543                             throw new SSLException("Unable to complete SSL handshake");
544                         }
545                         if (sslEngine.isInboundDone()) {
546                             endOfStream = true;
547                         }
548                         if(inPlainBuf.position() > 0) {
549                             inPlainBuf.flip();
550                             try {
551                                 ensureHandler().inputReady(protocolSession, inPlainBuf.hasRemaining() ? inPlainBuf : null);
552                             } finally {
553                                 inPlainBuf.clear();
554                             }
555                         }
556                         if (result.getStatus() != SSLEngineResult.Status.OK) {
557                             if (result.getStatus() == SSLEngineResult.Status.BUFFER_UNDERFLOW && endOfStream) {
558                                 throw new SSLException("Unable to decrypt incoming data due to unexpected end of stream");
559                             }
560                             break;
561                         }
562                     } finally {
563                         inPlain.release();
564                     }
565                 }
566             } finally {
567                 inEncryptedBuf.compact();
568                 // Release inEncrypted if empty
569                 if (inEncryptedBuf.position() == 0) {
570                     inEncrypted.release();
571                 }
572             }
573         }
574         if (endOfStream && !inEncrypted.hasData()) {
575             ensureHandler().inputReady(protocolSession, null);
576         }
577     }
578 
579     private void encryptData(final IOSession protocolSession) throws IOException {
580         final boolean appReady;
581         this.session.getLock().lock();
582         try {
583             appReady = (this.appEventMask & SelectionKey.OP_WRITE) > 0
584                     && this.status == Status.ACTIVE
585                     && this.sslEngine.getHandshakeStatus() == HandshakeStatus.NOT_HANDSHAKING;
586         } finally {
587             this.session.getLock().unlock();
588         }
589         if (appReady) {
590             ensureHandler().outputReady(protocolSession);
591         }
592     }
593 
594     @Override
595     public int write(final ByteBuffer src) throws IOException {
596         Args.notNull(src, "Byte buffer");
597         this.session.getLock().lock();
598         try {
599             if (this.status != Status.ACTIVE) {
600                 throw new ClosedChannelException();
601             }
602             if (this.handshakeStateRef.get() == TLSHandShakeState.READY) {
603                 return 0;
604             }
605             final ByteBuffer outEncryptedBuf = this.outEncrypted.acquire();
606             final SSLEngineResult result = doWrap(src, outEncryptedBuf);
607             return result.bytesConsumed();
608         } finally {
609             this.session.getLock().unlock();
610         }
611     }
612 
613     @Override
614     public int read(final ByteBuffer dst) {
615         return endOfStream ? -1 : 0;
616     }
617 
618     @Override
619     public String getId() {
620         return session.getId();
621     }
622 
623     @Override
624     public Lock getLock() {
625         return this.session.getLock();
626     }
627 
628     @Override
629     public void upgrade(final IOEventHandler handler) {
630         this.session.upgrade(handler);
631     }
632 
633     public TlsDetails getTlsDetails() {
634         return tlsDetails;
635     }
636 
637     @Override
638     public boolean isOpen() {
639         return this.status == Status.ACTIVE && this.session.isOpen();
640     }
641 
642     @Override
643     public void close() {
644         close(CloseMode.GRACEFUL);
645     }
646 
647     @Override
648     public void close(final CloseMode closeMode) {
649         this.session.getLock().lock();
650         try {
651             if (closeMode == CloseMode.GRACEFUL) {
652                 if (this.status.compareTo(Status.CLOSING) >= 0) {
653                     return;
654                 }
655                 this.status = Status.CLOSING;
656                 if (this.session.getSocketTimeout().isDisabled()) {
657                     this.session.setSocketTimeout(Timeout.ofMilliseconds(1000));
658                 }
659                 try {
660                     // Catch all unchecked exceptions in case something goes wrong
661                     // in the JSSE provider. For instance
662                     // com.android.org.conscrypt.NativeCrypto#SSL_get_shutdown can
663                     // throw NPE at this point
664                     updateEventMask();
665                 } catch (final CancelledKeyException ex) {
666                     this.session.close(CloseMode.GRACEFUL);
667                 } catch (final Exception ex) {
668                     this.session.close(CloseMode.IMMEDIATE);
669                 }
670             } else {
671                 if (this.status == Status.CLOSED) {
672                     return;
673                 }
674                 this.inEncrypted.release();
675                 this.outEncrypted.release();
676                 this.inPlain.release();
677 
678                 this.status = Status.CLOSED;
679                 this.session.close(closeMode);
680             }
681         } finally {
682             this.session.getLock().unlock();
683         }
684     }
685 
686     @Override
687     public Status getStatus() {
688         return this.status;
689     }
690 
691     @Override
692     public void enqueue(final Command command, final Command.Priority priority) {
693         this.session.getLock().lock();
694         try {
695             this.session.enqueue(command, priority);
696             setEvent(SelectionKey.OP_WRITE);
697         } finally {
698             this.session.getLock().unlock();
699         }
700     }
701 
702     @Override
703     public boolean hasCommands() {
704         return this.session.hasCommands();
705     }
706 
707     @Override
708     public Command poll() {
709         return this.session.poll();
710     }
711 
712     @Override
713     public ByteChannel channel() {
714         return this.session.channel();
715     }
716 
717     @Override
718     public SocketAddress getLocalAddress() {
719         return this.session.getLocalAddress();
720     }
721 
722     @Override
723     public SocketAddress getRemoteAddress() {
724         return this.session.getRemoteAddress();
725     }
726 
727     @Override
728     public int getEventMask() {
729         this.session.getLock().lock();
730         try {
731             return this.appEventMask;
732         } finally {
733             this.session.getLock().unlock();
734         }
735     }
736 
737     @Override
738     public void setEventMask(final int ops) {
739         this.session.getLock().lock();
740         try {
741             this.appEventMask = ops;
742             updateEventMask();
743         } finally {
744             this.session.getLock().unlock();
745         }
746     }
747 
748     @Override
749     public void setEvent(final int op) {
750         this.session.getLock().lock();
751         try {
752             this.appEventMask = this.appEventMask | op;
753             updateEventMask();
754         } finally {
755             this.session.getLock().unlock();
756         }
757     }
758 
759     @Override
760     public void clearEvent(final int op) {
761         this.session.getLock().lock();
762         try {
763             this.appEventMask = this.appEventMask & ~op;
764             updateEventMask();
765         } finally {
766             this.session.getLock().unlock();
767         }
768     }
769 
770     @Override
771     public Timeout getSocketTimeout() {
772         return this.session.getSocketTimeout();
773     }
774 
775     @Override
776     public void setSocketTimeout(final Timeout timeout) {
777         this.socketTimeout = timeout;
778         if (this.sslEngine.getHandshakeStatus() == HandshakeStatus.FINISHED) {
779             this.session.setSocketTimeout(timeout);
780         }
781     }
782 
783     @Override
784     public void updateReadTime() {
785         this.session.updateReadTime();
786     }
787 
788     @Override
789     public void updateWriteTime() {
790         this.session.updateWriteTime();
791     }
792 
793     @Override
794     public long getLastReadTime() {
795         return this.session.getLastReadTime();
796     }
797 
798     @Override
799     public long getLastWriteTime() {
800         return this.session.getLastWriteTime();
801     }
802 
803     @Override
804     public long getLastEventTime() {
805         return this.session.getLastEventTime();
806     }
807 
808     private static void formatOps(final StringBuilder buffer, final int ops) {
809         if ((ops & SelectionKey.OP_READ) > 0) {
810             buffer.append('r');
811         }
812         if ((ops & SelectionKey.OP_WRITE) > 0) {
813             buffer.append('w');
814         }
815     }
816 
817     @Override
818     public String toString() {
819         this.session.getLock().lock();
820         try {
821             final StringBuilder buffer = new StringBuilder();
822             buffer.append(this.session);
823             buffer.append("[");
824             buffer.append(this.status);
825             buffer.append("][");
826             formatOps(buffer, this.appEventMask);
827             buffer.append("][");
828             buffer.append(this.sslEngine.getHandshakeStatus());
829             if (this.sslEngine.isInboundDone()) {
830                 buffer.append("][inbound done][");
831             }
832             if (this.sslEngine.isOutboundDone()) {
833                 buffer.append("][outbound done][");
834             }
835             if (this.endOfStream) {
836                 buffer.append("][EOF][");
837             }
838             buffer.append("][");
839             buffer.append(!this.inEncrypted.hasData() ? 0 : inEncrypted.acquire().position());
840             buffer.append("][");
841             buffer.append(!this.inPlain.hasData() ? 0 : inPlain.acquire().position());
842             buffer.append("][");
843             buffer.append(!this.outEncrypted.hasData() ? 0 : outEncrypted.acquire().position());
844             buffer.append("]");
845             return buffer.toString();
846         } finally {
847             this.session.getLock().unlock();
848         }
849     }
850 
851 }