/
SSLSocketChannel2.java
463 lines (413 loc) · 16.7 KB
/
SSLSocketChannel2.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
/*
* Copyright (c) 2010-2020 Nathan Rajlich
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
package org.java_websocket;
import org.java_websocket.interfaces.ISSLChannel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLEngineResult;
import javax.net.ssl.SSLEngineResult.HandshakeStatus;
import javax.net.ssl.SSLEngineResult.Status;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSession;
import java.io.EOFException;
import java.io.IOException;
import java.net.Socket;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ByteChannel;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
/**
* Implements the relevant portions of the SocketChannel interface with the SSLEngine wrapper.
*/
public class SSLSocketChannel2 implements ByteChannel, WrappedByteChannel, ISSLChannel {
/**
* This object is used to feed the {@link SSLEngine}'s wrap and unwrap methods during the handshake phase.
**/
protected static ByteBuffer emptybuffer = ByteBuffer.allocate( 0 );
/**
* Logger instance
*
* @since 1.4.0
*/
private final Logger log = LoggerFactory.getLogger(SSLSocketChannel2.class);
protected ExecutorService exec;
protected List<Future<?>> tasks;
/** raw payload incomming */
protected ByteBuffer inData;
/** encrypted data outgoing */
protected ByteBuffer outCrypt;
/** encrypted data incoming */
protected ByteBuffer inCrypt;
/** the underlying channel */
protected SocketChannel socketChannel;
/** used to set interestOP SelectionKey.OP_WRITE for the underlying channel */
protected SelectionKey selectionKey;
protected SSLEngine sslEngine;
protected SSLEngineResult readEngineResult;
protected SSLEngineResult writeEngineResult;
/**
* Should be used to count the buffer allocations.
* But because of #190 where HandshakeStatus.FINISHED is not properly returned by nio wrap/unwrap this variable is used to check whether {@link #createBuffers(SSLSession)} needs to be called.
**/
protected int bufferallocations = 0;
public SSLSocketChannel2( SocketChannel channel , SSLEngine sslEngine , ExecutorService exec , SelectionKey key ) throws IOException {
if( channel == null || sslEngine == null || exec == null )
throw new IllegalArgumentException( "parameter must not be null" );
this.socketChannel = channel;
this.sslEngine = sslEngine;
this.exec = exec;
readEngineResult = writeEngineResult = new SSLEngineResult( Status.BUFFER_UNDERFLOW, sslEngine.getHandshakeStatus(), 0, 0 ); // init to prevent NPEs
tasks = new ArrayList<Future<?>>( 3 );
if( key != null ) {
key.interestOps( key.interestOps() | SelectionKey.OP_WRITE );
this.selectionKey = key;
}
createBuffers( sslEngine.getSession() );
// kick off handshake
socketChannel.write( wrap( emptybuffer ) );// initializes res
processHandshake();
}
private void consumeFutureUninterruptible( Future<?> f ) {
try {
while ( true ) {
try {
f.get();
break;
} catch ( InterruptedException e ) {
Thread.currentThread().interrupt();
}
}
} catch ( ExecutionException e ) {
throw new RuntimeException( e );
}
}
/**
* This method will do whatever necessary to process the sslengine handshake.
* Thats why it's called both from the {@link #read(ByteBuffer)} and {@link #write(ByteBuffer)}
**/
private synchronized void processHandshake() throws IOException {
if( sslEngine.getHandshakeStatus() == HandshakeStatus.NOT_HANDSHAKING )
return; // since this may be called either from a reading or a writing thread and because this method is synchronized it is necessary to double check if we are still handshaking.
if( !tasks.isEmpty() ) {
Iterator<Future<?>> it = tasks.iterator();
while ( it.hasNext() ) {
Future<?> f = it.next();
if( f.isDone() ) {
it.remove();
} else {
if( isBlocking() )
consumeFutureUninterruptible( f );
return;
}
}
}
if( sslEngine.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_UNWRAP ) {
if( !isBlocking() || readEngineResult.getStatus() == Status.BUFFER_UNDERFLOW ) {
inCrypt.compact();
int read = socketChannel.read( inCrypt );
if( read == -1 ) {
throw new IOException( "connection closed unexpectedly by peer" );
}
inCrypt.flip();
}
inData.compact();
unwrap();
if( readEngineResult.getHandshakeStatus() == HandshakeStatus.FINISHED ) {
createBuffers( sslEngine.getSession() );
return;
}
}
consumeDelegatedTasks();
if( tasks.isEmpty() || sslEngine.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_WRAP ) {
socketChannel.write( wrap( emptybuffer ) );
if( writeEngineResult.getHandshakeStatus() == HandshakeStatus.FINISHED ) {
createBuffers( sslEngine.getSession() );
return;
}
}
assert ( sslEngine.getHandshakeStatus() != HandshakeStatus.NOT_HANDSHAKING );// this function could only leave NOT_HANDSHAKING after createBuffers was called unless #190 occurs which means that nio wrap/unwrap never return HandshakeStatus.FINISHED
bufferallocations = 1; // look at variable declaration why this line exists and #190. Without this line buffers would not be be recreated when #190 AND a rehandshake occur.
}
private synchronized ByteBuffer wrap( ByteBuffer b ) throws SSLException {
outCrypt.compact();
writeEngineResult = sslEngine.wrap( b, outCrypt );
outCrypt.flip();
return outCrypt;
}
/**
* performs the unwrap operation by unwrapping from {@link #inCrypt} to {@link #inData}
**/
private synchronized ByteBuffer unwrap() throws SSLException {
int rem;
//There are some ssl test suites, which get around the selector.select() call, which cause an infinite unwrap and 100% cpu usage (see #459 and #458)
if(readEngineResult.getStatus() == SSLEngineResult.Status.CLOSED && sslEngine.getHandshakeStatus() == HandshakeStatus.NOT_HANDSHAKING){
try {
close();
} catch (IOException e) {
//Not really interesting
}
}
do {
rem = inData.remaining();
readEngineResult = sslEngine.unwrap( inCrypt, inData );
} while ( readEngineResult.getStatus() == SSLEngineResult.Status.OK && ( rem != inData.remaining() || sslEngine.getHandshakeStatus() == HandshakeStatus.NEED_UNWRAP ) );
inData.flip();
return inData;
}
protected void consumeDelegatedTasks() {
Runnable task;
while ( ( task = sslEngine.getDelegatedTask() ) != null ) {
tasks.add( exec.submit( task ) );
// task.run();
}
}
protected void createBuffers( SSLSession session ) {
saveCryptedData(); // save any remaining data in inCrypt
int netBufferMax = session.getPacketBufferSize();
int appBufferMax = Math.max(session.getApplicationBufferSize(), netBufferMax);
if( inData == null ) {
inData = ByteBuffer.allocate( appBufferMax );
outCrypt = ByteBuffer.allocate( netBufferMax );
inCrypt = ByteBuffer.allocate( netBufferMax );
} else {
if( inData.capacity() != appBufferMax )
inData = ByteBuffer.allocate( appBufferMax );
if( outCrypt.capacity() != netBufferMax )
outCrypt = ByteBuffer.allocate( netBufferMax );
if( inCrypt.capacity() != netBufferMax )
inCrypt = ByteBuffer.allocate( netBufferMax );
}
if (inData.remaining() != 0 && log.isTraceEnabled()) {
log.trace(new String( inData.array(), inData.position(), inData.remaining()));
}
inData.rewind();
inData.flip();
if (inCrypt.remaining() != 0 && log.isTraceEnabled()) {
log.trace(new String( inCrypt.array(), inCrypt.position(), inCrypt.remaining()));
}
inCrypt.rewind();
inCrypt.flip();
outCrypt.rewind();
outCrypt.flip();
bufferallocations++;
}
public int write( ByteBuffer src ) throws IOException {
if( !isHandShakeComplete() ) {
processHandshake();
return 0;
}
// assert ( bufferallocations > 1 ); //see #190
//if( bufferallocations <= 1 ) {
// createBuffers( sslEngine.getSession() );
//}
int num = socketChannel.write( wrap( src ) );
if (writeEngineResult.getStatus() == SSLEngineResult.Status.CLOSED) {
throw new EOFException("Connection is closed");
}
return num;
}
/**
* Blocks when in blocking mode until at least one byte has been decoded.<br>
* When not in blocking mode 0 may be returned.
*
* @return the number of bytes read.
**/
public int read(ByteBuffer dst) throws IOException {
tryRestoreCryptedData();
while (true) {
if (!dst.hasRemaining())
return 0;
if (!isHandShakeComplete()) {
if (isBlocking()) {
while (!isHandShakeComplete()) {
processHandshake();
}
} else {
processHandshake();
if (!isHandShakeComplete()) {
return 0;
}
}
}
// assert ( bufferallocations > 1 ); //see #190
//if( bufferallocations <= 1 ) {
// createBuffers( sslEngine.getSession() );
//}
/* 1. When "dst" is smaller than "inData" readRemaining will fill "dst" with data decoded in a previous read call.
* 2. When "inCrypt" contains more data than "inData" has remaining space, unwrap has to be called on more time(readRemaining)
*/
int purged = readRemaining(dst);
if (purged != 0)
return purged;
/* We only continue when we really need more data from the network.
* Thats the case if inData is empty or inCrypt holds to less data than necessary for decryption
*/
assert (inData.position() == 0);
inData.clear();
if (!inCrypt.hasRemaining())
inCrypt.clear();
else
inCrypt.compact();
if (isBlocking() || readEngineResult.getStatus() == Status.BUFFER_UNDERFLOW)
if (socketChannel.read(inCrypt) == -1) {
return -1;
}
inCrypt.flip();
unwrap();
int transfered = transfereTo(inData, dst);
if (transfered == 0 && isBlocking()) {
continue;
}
return transfered;
}
}
/**
* {@link #read(ByteBuffer)} may not be to leave all buffers(inData, inCrypt)
**/
private int readRemaining( ByteBuffer dst ) throws SSLException {
if( inData.hasRemaining() ) {
return transfereTo( inData, dst );
}
if( !inData.hasRemaining() )
inData.clear();
tryRestoreCryptedData();
// test if some bytes left from last read (e.g. BUFFER_UNDERFLOW)
if( inCrypt.hasRemaining() ) {
unwrap();
int amount = transfereTo( inData, dst );
if (readEngineResult.getStatus() == SSLEngineResult.Status.CLOSED) {
return -1;
}
if( amount > 0 )
return amount;
}
return 0;
}
public boolean isConnected() {
return socketChannel.isConnected();
}
public void close() throws IOException {
sslEngine.closeOutbound();
sslEngine.getSession().invalidate();
if( socketChannel.isOpen() )
socketChannel.write( wrap( emptybuffer ) );// FIXME what if not all bytes can be written
socketChannel.close();
}
private boolean isHandShakeComplete() {
HandshakeStatus status = sslEngine.getHandshakeStatus();
return status == SSLEngineResult.HandshakeStatus.FINISHED || status == SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING;
}
public SelectableChannel configureBlocking( boolean b ) throws IOException {
return socketChannel.configureBlocking( b );
}
public boolean connect( SocketAddress remote ) throws IOException {
return socketChannel.connect( remote );
}
public boolean finishConnect() throws IOException {
return socketChannel.finishConnect();
}
public Socket socket() {
return socketChannel.socket();
}
public boolean isInboundDone() {
return sslEngine.isInboundDone();
}
@Override
public boolean isOpen() {
return socketChannel.isOpen();
}
@Override
public boolean isNeedWrite() {
return outCrypt.hasRemaining() || !isHandShakeComplete(); // FIXME this condition can cause high cpu load during handshaking when network is slow
}
@Override
public void writeMore() throws IOException {
write( outCrypt );
}
@Override
public boolean isNeedRead() {
return saveCryptData != null || inData.hasRemaining() || ( inCrypt.hasRemaining() && readEngineResult.getStatus() != Status.BUFFER_UNDERFLOW && readEngineResult.getStatus() != Status.CLOSED );
}
@Override
public int readMore( ByteBuffer dst ) throws SSLException {
return readRemaining( dst );
}
private int transfereTo( ByteBuffer from, ByteBuffer to ) {
int fremain = from.remaining();
int toremain = to.remaining();
if( fremain > toremain ) {
// FIXME there should be a more efficient transfer method
int limit = Math.min( fremain, toremain );
for( int i = 0 ; i < limit ; i++ ) {
to.put( from.get() );
}
return limit;
} else {
to.put( from );
return fremain;
}
}
@Override
public boolean isBlocking() {
return socketChannel.isBlocking();
}
@Override
public SSLEngine getSSLEngine() {
return sslEngine;
}
// to avoid complexities with inCrypt, extra unwrapped data after SSL handshake will be saved off in a byte array
// and the inserted back on first read
private byte[] saveCryptData = null;
private void saveCryptedData()
{
// did we find any extra data?
if (inCrypt != null && inCrypt.remaining() > 0)
{
int saveCryptSize = inCrypt.remaining();
saveCryptData = new byte[saveCryptSize];
inCrypt.get(saveCryptData);
}
}
private void tryRestoreCryptedData()
{
// was there any extra data, then put into inCrypt and clean up
if ( saveCryptData != null )
{
inCrypt.clear();
inCrypt.put( saveCryptData );
inCrypt.flip();
saveCryptData = null;
}
}
}