1 | /* |
2 | * @(#) $Id: SSLHandler.java 332218 2005-11-10 03:52:42Z trustin $ |
3 | * |
4 | * Copyright 2004 The Apache Software Foundation |
5 | * |
6 | * Licensed under the Apache License, Version 2.0 (the "License"); |
7 | * you may not use this file except in compliance with the License. |
8 | * You may obtain a copy of the License at |
9 | * |
10 | * http://www.apache.org/licenses/LICENSE-2.0 |
11 | * |
12 | * Unless required by applicable law or agreed to in writing, software |
13 | * distributed under the License is distributed on an "AS IS" BASIS, |
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
15 | * See the License for the specific language governing permissions and |
16 | * limitations under the License. |
17 | * |
18 | */ |
19 | package org.apache.mina.io.filter; |
20 | |
21 | import java.nio.ByteBuffer; |
22 | |
23 | import javax.net.ssl.SSLContext; |
24 | import javax.net.ssl.SSLEngine; |
25 | import javax.net.ssl.SSLEngineResult; |
26 | import javax.net.ssl.SSLException; |
27 | import javax.net.ssl.SSLSession; |
28 | |
29 | import org.apache.mina.io.IoSession; |
30 | import org.apache.mina.io.IoFilter.NextFilter; |
31 | import org.apache.mina.util.Queue; |
32 | import org.slf4j.Logger; |
33 | import org.slf4j.LoggerFactory; |
34 | |
35 | /** |
36 | * A helper class using the SSLEngine API to decrypt/encrypt data. |
37 | * <p> |
38 | * Each connection has a SSLEngine that is used through the lifetime of the connection. |
39 | * We allocate byte buffers for use as the outbound and inbound network buffers. |
40 | * These buffers handle all of the intermediary data for the SSL connection. To make things easy, |
41 | * we'll require outNetBuffer be completely flushed before trying to wrap any more data. |
42 | * |
43 | * @author The Apache Directory Project (dev@directory.apache.org) |
44 | * @version $Rev: 332218 $, $Date: 2005-11-10 12:52:42 +0900 $ |
45 | */ |
46 | class SSLHandler |
47 | { |
48 | private static final Logger log = LoggerFactory.getLogger( SSLFilter.class ); |
49 | |
50 | private final SSLFilter parent; |
51 | |
52 | private final IoSession session; |
53 | |
54 | private final Queue nextFilterQueue = new Queue(); |
55 | |
56 | private final Queue writeBufferQueue = new Queue(); |
57 | |
58 | private final Queue writeMarkerQueue = new Queue(); |
59 | |
60 | private final SSLEngine sslEngine; |
61 | |
62 | /** |
63 | * Encrypted data from the net |
64 | */ |
65 | private ByteBuffer inNetBuffer; |
66 | |
67 | /** |
68 | * Encrypted data to be written to the net |
69 | */ |
70 | private ByteBuffer outNetBuffer; |
71 | |
72 | /** |
73 | * Applicaton cleartext data to be read by application |
74 | */ |
75 | private ByteBuffer appBuffer; |
76 | |
77 | /** |
78 | * Empty buffer used during initial handshake and close operations |
79 | */ |
80 | private ByteBuffer hsBB = ByteBuffer.allocate( 0 ); |
81 | |
82 | /** |
83 | * Handshake status |
84 | */ |
85 | private SSLEngineResult.HandshakeStatus initialHandshakeStatus; |
86 | |
87 | /** |
88 | * Initial handshake complete? |
89 | */ |
90 | private boolean initialHandshakeComplete; |
91 | |
92 | /** |
93 | * We have received the shutdown request by our caller, and have |
94 | * closed our outbound side. |
95 | */ |
96 | private boolean shutdown = false; |
97 | |
98 | private boolean closed = false; |
99 | |
100 | private boolean isWritingEncryptedData = false; |
101 | |
102 | /** |
103 | * Constuctor. |
104 | * |
105 | * @param sslc |
106 | * @throws SSLException |
107 | */ |
108 | SSLHandler( SSLFilter parent, SSLContext sslc, IoSession session ) throws SSLException |
109 | { |
110 | this.parent = parent; |
111 | this.session = session; |
112 | sslEngine = sslc.createSSLEngine(); |
113 | sslEngine.setUseClientMode( parent.isUseClientMode() ); |
114 | |
115 | if ( parent.isWantClientAuth() ) |
116 | { |
117 | sslEngine.setWantClientAuth( true ); |
118 | } |
119 | |
120 | if ( parent.isNeedClientAuth() ) |
121 | { |
122 | sslEngine.setNeedClientAuth( true ); |
123 | } |
124 | |
125 | if( parent.getEnabledCipherSuites() != null ) |
126 | { |
127 | sslEngine.setEnabledCipherSuites( parent.getEnabledCipherSuites() ); |
128 | } |
129 | |
130 | if( parent.getEnabledProtocols() != null ) |
131 | { |
132 | sslEngine.setEnabledProtocols( parent.getEnabledProtocols() ); |
133 | } |
134 | |
135 | sslEngine.beginHandshake(); |
136 | initialHandshakeStatus = sslEngine.getHandshakeStatus();//SSLEngineResult.HandshakeStatus.NEED_UNWRAP; |
137 | initialHandshakeComplete = false; |
138 | |
139 | SSLByteBufferPool.initiate( sslEngine ); |
140 | |
141 | appBuffer = SSLByteBufferPool.getApplicationBuffer(); |
142 | |
143 | inNetBuffer = SSLByteBufferPool.getPacketBuffer(); |
144 | outNetBuffer = SSLByteBufferPool.getPacketBuffer(); |
145 | outNetBuffer.position( 0 ); |
146 | outNetBuffer.limit( 0 ); |
147 | } |
148 | |
149 | /** |
150 | * Indicate that we are writing encrypted data. |
151 | * Only used as a flag by IoSSLFiler |
152 | */ |
153 | public void setWritingEncryptedData( boolean flag ) |
154 | { |
155 | isWritingEncryptedData = flag; |
156 | } |
157 | |
158 | /** |
159 | * Check we are writing encrypted data. |
160 | */ |
161 | public boolean isWritingEncryptedData() |
162 | { |
163 | return isWritingEncryptedData; |
164 | } |
165 | |
166 | /** |
167 | * Check if initial handshake is completed. |
168 | */ |
169 | public boolean isInitialHandshakeComplete() |
170 | { |
171 | return initialHandshakeComplete; |
172 | } |
173 | |
174 | /** |
175 | * Check if SSL sesssion closed |
176 | */ |
177 | public boolean isClosed() |
178 | { |
179 | return closed; |
180 | } |
181 | |
182 | /** |
183 | * Check if there is any need to complete initial handshake. |
184 | */ |
185 | public boolean needToCompleteInitialHandshake() |
186 | { |
187 | return ( initialHandshakeStatus == SSLEngineResult.HandshakeStatus.NEED_WRAP && !closed ); |
188 | } |
189 | |
190 | public synchronized void scheduleWrite( NextFilter nextFilter, org.apache.mina.common.ByteBuffer buf, Object marker ) |
191 | { |
192 | nextFilterQueue.push( nextFilter ); |
193 | writeBufferQueue.push( buf ); |
194 | writeMarkerQueue.push( marker ); |
195 | } |
196 | |
197 | public synchronized void flushScheduledWrites() throws SSLException |
198 | { |
199 | NextFilter nextFilter; |
200 | org.apache.mina.common.ByteBuffer scheduledBuf; |
201 | Object scheduledMarker; |
202 | |
203 | while( ( scheduledBuf = ( org.apache.mina.common.ByteBuffer ) writeBufferQueue.pop() ) != null ) |
204 | { |
205 | if( log.isDebugEnabled() ) |
206 | { |
207 | log.debug( session + " Flushing buffered write request: " + scheduledBuf ); |
208 | } |
209 | nextFilter = ( NextFilter ) nextFilterQueue.pop(); |
210 | scheduledMarker = writeMarkerQueue.pop(); |
211 | parent.filterWrite( nextFilter, session, scheduledBuf, scheduledMarker ); |
212 | } |
213 | } |
214 | |
215 | /** |
216 | * Call when data read from net. Will perform inial hanshake or decrypt provided |
217 | * Buffer. |
218 | * Decrytpted data reurned by getAppBuffer(), if any. |
219 | * |
220 | * @param buf buffer to decrypt |
221 | * @throws SSLException on errors |
222 | */ |
223 | public void dataRead( NextFilter nextFilter, ByteBuffer buf ) throws SSLException |
224 | { |
225 | if ( buf.limit() > inNetBuffer.remaining() ) { |
226 | // We have to expand inNetBuffer |
227 | inNetBuffer = SSLByteBufferPool.expandBuffer( inNetBuffer, |
228 | inNetBuffer.capacity() + ( buf.limit() * 2 ) ); |
229 | // We also expand app. buffer (twice the size of in net. buffer) |
230 | appBuffer = SSLByteBufferPool.expandBuffer( appBuffer, inNetBuffer.capacity() * 2); |
231 | appBuffer.position( 0 ); |
232 | appBuffer.limit( 0 ); |
233 | if( log.isDebugEnabled() ) |
234 | { |
235 | log.debug( session + |
236 | " expanded inNetBuffer:" + inNetBuffer ); |
237 | log.debug( session + |
238 | " expanded appBuffer:" + appBuffer ); |
239 | } |
240 | } |
241 | |
242 | // append buf to inNetBuffer |
243 | inNetBuffer.put( buf ); |
244 | if( !initialHandshakeComplete ) |
245 | { |
246 | doHandshake( nextFilter ); |
247 | } |
248 | else |
249 | { |
250 | doDecrypt(); |
251 | } |
252 | } |
253 | |
254 | /** |
255 | * Continue initial SSL handshake. |
256 | * |
257 | * @throws SSLException on errors |
258 | */ |
259 | public void continueHandshake( NextFilter nextFilter ) throws SSLException |
260 | { |
261 | if( log.isDebugEnabled() ) |
262 | { |
263 | log.debug( session + " continueHandshake()" ); |
264 | } |
265 | doHandshake( nextFilter ); |
266 | } |
267 | |
268 | /** |
269 | * Get decrypted application data. |
270 | * |
271 | * @return buffer with data |
272 | */ |
273 | public ByteBuffer getAppBuffer() |
274 | { |
275 | return appBuffer; |
276 | } |
277 | |
278 | /** |
279 | * Get encrypted data to be sent. |
280 | * |
281 | * @return buffer with data |
282 | */ |
283 | public ByteBuffer getOutNetBuffer() |
284 | { |
285 | return outNetBuffer; |
286 | } |
287 | |
288 | /** |
289 | * Encrypt provided buffer. Encytpted data reurned by getOutNetBuffer(). |
290 | * |
291 | * @param buf data to encrypt |
292 | * @throws SSLException on errors |
293 | */ |
294 | public void encrypt( ByteBuffer buf ) throws SSLException |
295 | { |
296 | doEncrypt( buf ); |
297 | } |
298 | |
299 | /** |
300 | * Start SSL shutdown process |
301 | * |
302 | * @throws SSLException on errors |
303 | */ |
304 | public void shutdown() throws SSLException |
305 | { |
306 | if( !shutdown ) |
307 | { |
308 | doShutdown(); |
309 | } |
310 | } |
311 | |
312 | /** |
313 | * Release allocated ByteBuffers. |
314 | */ |
315 | public void release() |
316 | { |
317 | SSLByteBufferPool.release( appBuffer ); |
318 | SSLByteBufferPool.release( inNetBuffer ); |
319 | SSLByteBufferPool.release( outNetBuffer ); |
320 | } |
321 | |
322 | /** |
323 | * Decrypt in net buffer. Result is stored in app buffer. |
324 | * |
325 | * @throws SSLException |
326 | */ |
327 | private void doDecrypt() throws SSLException |
328 | { |
329 | |
330 | if( !initialHandshakeComplete ) |
331 | { |
332 | throw new IllegalStateException(); |
333 | } |
334 | |
335 | if( appBuffer.hasRemaining() ) |
336 | { |
337 | if ( log.isDebugEnabled() ) { |
338 | log.debug( session + " Error: appBuffer not empty!" ); |
339 | } |
340 | //still app data in buffer!? |
341 | throw new IllegalStateException(); |
342 | } |
343 | |
344 | unwrap(); |
345 | } |
346 | |
347 | /** |
348 | * @param status |
349 | * @throws SSLException |
350 | */ |
351 | private SSLEngineResult.Status checkStatus( SSLEngineResult.Status status ) throws SSLException |
352 | { |
353 | if( status != SSLEngineResult.Status.OK && |
354 | status != SSLEngineResult.Status.CLOSED && |
355 | status != SSLEngineResult.Status.BUFFER_UNDERFLOW ) |
356 | { |
357 | throw new SSLException( "SSLEngine error during decrypt: " + |
358 | status + |
359 | " inNetBuffer: " + inNetBuffer + "appBuffer: " + appBuffer); |
360 | } |
361 | |
362 | return status; |
363 | } |
364 | |
365 | private void doEncrypt( ByteBuffer src ) throws SSLException |
366 | { |
367 | if( !initialHandshakeComplete ) |
368 | { |
369 | throw new IllegalStateException(); |
370 | } |
371 | |
372 | // The data buffer is (must be) empty, we can reuse the entire |
373 | // buffer. |
374 | outNetBuffer.clear(); |
375 | |
376 | SSLEngineResult result; |
377 | |
378 | // Loop until there is no more data in src |
379 | while ( src.hasRemaining() ) { |
380 | |
381 | if ( src.remaining() > ( ( outNetBuffer.capacity() - outNetBuffer.position() ) / 2 ) ) { |
382 | // We have to expand outNetBuffer |
383 | // Note: there is no way to know the exact size required, but enrypted data |
384 | // shouln't need to be larger than twice the source data size? |
385 | outNetBuffer = SSLByteBufferPool.expandBuffer( outNetBuffer, src.capacity() * 2 ); |
386 | if ( log.isDebugEnabled() ) { |
387 | log.debug( session + " expanded outNetBuffer:" + outNetBuffer ); |
388 | } |
389 | } |
390 | |
391 | result = sslEngine.wrap( src, outNetBuffer ); |
392 | if ( log.isDebugEnabled() ) { |
393 | log.debug( session + " Wrap res:" + result ); |
394 | } |
395 | |
396 | if ( result.getStatus() == SSLEngineResult.Status.OK ) { |
397 | if ( result.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_TASK ) { |
398 | doTasks(); |
399 | } |
400 | } else { |
401 | throw new SSLException( "SSLEngine error during encrypt: " |
402 | + result.getStatus() + |
403 | " src: " + src + "outNetBuffer: " + outNetBuffer); |
404 | } |
405 | } |
406 | |
407 | outNetBuffer.flip(); |
408 | } |
409 | |
410 | /** |
411 | * Perform any handshaking processing. |
412 | */ |
413 | synchronized void doHandshake( NextFilter nextFilter ) throws SSLException |
414 | { |
415 | |
416 | if( log.isDebugEnabled() ) |
417 | { |
418 | log.debug( session + " doHandshake()" ); |
419 | } |
420 | |
421 | while( !initialHandshakeComplete ) |
422 | { |
423 | if( initialHandshakeStatus == SSLEngineResult.HandshakeStatus.FINISHED ) |
424 | { |
425 | session.setAttribute( SSLFilter.SSL_SESSION, sslEngine.getSession() ); |
426 | if( log.isDebugEnabled() ) |
427 | { |
428 | SSLSession sslSession = sslEngine.getSession(); |
429 | log.debug( session + " initialHandshakeStatus=FINISHED" ); |
430 | log.debug( session + " sslSession CipherSuite used " + sslSession.getCipherSuite() ); |
431 | } |
432 | initialHandshakeComplete = true; |
433 | return; |
434 | } |
435 | else if( initialHandshakeStatus == SSLEngineResult.HandshakeStatus.NEED_TASK ) |
436 | { |
437 | if( log.isDebugEnabled() ) |
438 | { |
439 | log.debug( session + " initialHandshakeStatus=NEED_TASK" ); |
440 | } |
441 | initialHandshakeStatus = doTasks(); |
442 | } |
443 | else if( initialHandshakeStatus == SSLEngineResult.HandshakeStatus.NEED_UNWRAP ) |
444 | { |
445 | // we need more data read |
446 | if( log.isDebugEnabled() ) |
447 | { |
448 | log.debug( session + |
449 | " initialHandshakeStatus=NEED_UNWRAP" ); |
450 | } |
451 | SSLEngineResult.Status status = unwrapHandshake(); |
452 | if( ( initialHandshakeStatus != SSLEngineResult.HandshakeStatus.FINISHED |
453 | && status == SSLEngineResult.Status.BUFFER_UNDERFLOW ) |
454 | || closed ) |
455 | { |
456 | // We need more data or the session is closed |
457 | return; |
458 | } |
459 | } |
460 | else if( initialHandshakeStatus == SSLEngineResult.HandshakeStatus.NEED_WRAP ) |
461 | { |
462 | if( log.isDebugEnabled() ) |
463 | { |
464 | log.debug( session + " initialHandshakeStatus=NEED_WRAP" ); |
465 | } |
466 | // First make sure that the out buffer is completely empty. Since we |
467 | // cannot call wrap with data left on the buffer |
468 | if( outNetBuffer.hasRemaining() ) |
469 | { |
470 | if( log.isDebugEnabled() ) |
471 | { |
472 | log.debug( session + " Still data in out buffer!" ); |
473 | } |
474 | return; |
475 | } |
476 | outNetBuffer.clear(); |
477 | SSLEngineResult result = sslEngine.wrap( hsBB, outNetBuffer ); |
478 | if( log.isDebugEnabled() ) |
479 | { |
480 | log.debug( session + " Wrap res:" + result ); |
481 | } |
482 | |
483 | outNetBuffer.flip(); |
484 | initialHandshakeStatus = result.getHandshakeStatus(); |
485 | parent.writeNetBuffer( nextFilter, session, this ); |
486 | // return to allow data on out buffer being sent |
487 | // TODO: We might want to send more data immidiatley? |
488 | } |
489 | else |
490 | { |
491 | throw new IllegalStateException( "Invalid Handshaking State" |
492 | + initialHandshakeStatus ); |
493 | } |
494 | } |
495 | } |
496 | |
497 | SSLEngineResult.Status unwrap() throws SSLException |
498 | { |
499 | if( log.isDebugEnabled() ) |
500 | { |
501 | log.debug( session + " unwrap()" ); |
502 | } |
503 | // Prepare the application buffer to receive decrypted data |
504 | appBuffer.clear(); |
505 | |
506 | // Prepare the net data for reading. |
507 | inNetBuffer.flip(); |
508 | |
509 | SSLEngineResult res; |
510 | do |
511 | { |
512 | if( log.isDebugEnabled() ) |
513 | { |
514 | log.debug( session + " inNetBuffer: " + inNetBuffer ); |
515 | log.debug( session + " appBuffer: " + appBuffer ); |
516 | } |
517 | res = sslEngine.unwrap( inNetBuffer, appBuffer ); |
518 | if( log.isDebugEnabled() ) |
519 | { |
520 | log.debug( session + " Unwrap res:" + res ); |
521 | } |
522 | } |
523 | while( res.getStatus() == SSLEngineResult.Status.OK ); |
524 | |
525 | // If we are CLOSED, set flag |
526 | if( res.getStatus() == SSLEngineResult.Status.CLOSED ) |
527 | { |
528 | closed = true; |
529 | } |
530 | |
531 | // prepare to be written again |
532 | inNetBuffer.compact(); |
533 | // prepare app data to be read |
534 | appBuffer.flip(); |
535 | |
536 | /* |
537 | * The status may be: |
538 | * OK - Normal operation |
539 | * OVERFLOW - Should never happen since the application buffer is |
540 | * sized to hold the maximum packet size. |
541 | * UNDERFLOW - Need to read more data from the socket. It's normal. |
542 | * CLOSED - The other peer closed the socket. Also normal. |
543 | */ |
544 | return checkStatus( res.getStatus() ); |
545 | } |
546 | |
547 | private SSLEngineResult.Status unwrapHandshake() throws SSLException |
548 | { |
549 | if( log.isDebugEnabled() ) |
550 | { |
551 | log.debug( session + " unwrapHandshake()" ); |
552 | } |
553 | // Prepare the application buffer to receive decrypted data |
554 | appBuffer.clear(); |
555 | |
556 | // Prepare the net data for reading. |
557 | inNetBuffer.flip(); |
558 | |
559 | SSLEngineResult res; |
560 | do |
561 | { |
562 | if( log.isDebugEnabled() ) |
563 | { |
564 | log.debug( session + " inNetBuffer: " + inNetBuffer ); |
565 | log.debug( session + " appBuffer: " + appBuffer ); |
566 | } |
567 | res = sslEngine.unwrap( inNetBuffer, appBuffer ); |
568 | if( log.isDebugEnabled() ) |
569 | { |
570 | log.debug( session + " Unwrap res:" + res ); |
571 | } |
572 | |
573 | } |
574 | while( res.getStatus() == SSLEngineResult.Status.OK && |
575 | res.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_UNWRAP ); |
576 | |
577 | initialHandshakeStatus = res.getHandshakeStatus(); |
578 | |
579 | // If handshake finished, no data was produced, and the status is still ok, |
580 | // try to unwrap more |
581 | if (initialHandshakeStatus == SSLEngineResult.HandshakeStatus.FINISHED |
582 | && appBuffer.position() == 0 |
583 | && res.getStatus() == SSLEngineResult.Status.OK |
584 | && inNetBuffer.hasRemaining()) { |
585 | do { |
586 | if (log.isDebugEnabled()) { |
587 | log.debug( session + " extra handshake unwrap" ); |
588 | log.debug( session + " inNetBuffer: " + inNetBuffer ); |
589 | log.debug( session + " appBuffer: " + appBuffer ); |
590 | } |
591 | res = sslEngine.unwrap(inNetBuffer, appBuffer); |
592 | if (log.isDebugEnabled()) { |
593 | log.debug( session + " Unwrap res:" + res ); |
594 | } |
595 | } while (res.getStatus() == SSLEngineResult.Status.OK); |
596 | } |
597 | |
598 | // If we are CLOSED, set flag |
599 | if( res.getStatus() == SSLEngineResult.Status.CLOSED ) |
600 | { |
601 | closed = true; |
602 | } |
603 | |
604 | // prepare to be written again |
605 | inNetBuffer.compact(); |
606 | |
607 | // prepare app data to be read |
608 | appBuffer.flip(); |
609 | |
610 | /* |
611 | * The status may be: |
612 | * OK - Normal operation |
613 | * OVERFLOW - Should never happen since the application buffer is |
614 | * sized to hold the maximum packet size. |
615 | * UNDERFLOW - Need to read more data from the socket. It's normal. |
616 | * CLOSED - The other peer closed the socket. Also normal. |
617 | */ |
618 | //initialHandshakeStatus = res.getHandshakeStatus(); |
619 | return checkStatus( res.getStatus() ); |
620 | } |
621 | |
622 | /** |
623 | * Do all the outstanding handshake tasks in the current Thread. |
624 | */ |
625 | private SSLEngineResult.HandshakeStatus doTasks() |
626 | { |
627 | if( log.isDebugEnabled() ) |
628 | { |
629 | log.debug( session + " doTasks()" ); |
630 | } |
631 | |
632 | /* |
633 | * We could run this in a separate thread, but I don't see the need |
634 | * for this when used from IoSSLFilter.Use thread filters in Mina instead? |
635 | */ |
636 | Runnable runnable; |
637 | while( ( runnable = sslEngine.getDelegatedTask() ) != null ) |
638 | { |
639 | if( log.isDebugEnabled() ) |
640 | { |
641 | log.debug( session + " doTask: " + runnable ); |
642 | } |
643 | runnable.run(); |
644 | } |
645 | if( log.isDebugEnabled() ) |
646 | { |
647 | log.debug( session + " doTasks(): " |
648 | + sslEngine.getHandshakeStatus() ); |
649 | } |
650 | return sslEngine.getHandshakeStatus(); |
651 | } |
652 | |
653 | /** |
654 | * Begin the shutdown process. |
655 | */ |
656 | void doShutdown() throws SSLException |
657 | { |
658 | |
659 | if( !shutdown ) |
660 | { |
661 | sslEngine.closeOutbound(); |
662 | shutdown = true; |
663 | } |
664 | |
665 | // By RFC 2616, we can "fire and forget" our close_notify |
666 | // message, so that's what we'll do here. |
667 | |
668 | outNetBuffer.clear(); |
669 | SSLEngineResult result = sslEngine.wrap( hsBB, outNetBuffer ); |
670 | if( result.getStatus() != SSLEngineResult.Status.CLOSED ) |
671 | { |
672 | throw new SSLException( "Improper close state: " + result ); |
673 | } |
674 | outNetBuffer.flip(); |
675 | } |
676 | } |