1 /* 2 * Copyright (C) 2014 Square, Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.squareup.okhttp.internal.ws; 17 18 import com.squareup.okhttp.MediaType; 19 import com.squareup.okhttp.RequestBody; 20 import com.squareup.okhttp.ws.WebSocketRecorder; 21 import java.io.Closeable; 22 import java.io.IOException; 23 import java.net.ProtocolException; 24 import java.util.Random; 25 import java.util.concurrent.Executor; 26 import okio.Buffer; 27 import okio.BufferedSink; 28 import okio.BufferedSource; 29 import okio.ByteString; 30 import okio.Okio; 31 import okio.Sink; 32 import okio.Source; 33 import okio.Timeout; 34 import org.junit.After; 35 import org.junit.Before; 36 import org.junit.Test; 37 38 import static com.squareup.okhttp.ws.WebSocket.BINARY; 39 import static com.squareup.okhttp.ws.WebSocket.TEXT; 40 import static org.junit.Assert.assertEquals; 41 import static org.junit.Assert.assertFalse; 42 import static org.junit.Assert.assertNotEquals; 43 import static org.junit.Assert.assertTrue; 44 import static org.junit.Assert.fail; 45 46 public final class RealWebSocketTest { 47 // NOTE: Fields are named 'client' and 'server' for cognitive simplicity. This differentiation has 48 // zero effect on the behavior of the WebSocket API which is why tests are only written once 49 // from the perspective of a single peer. 50 51 private final Executor clientExecutor = new SynchronousExecutor(); 52 private RealWebSocket client; 53 private boolean clientConnectionCloseThrows; 54 private boolean clientConnectionClosed; 55 private final MemorySocket client2Server = new MemorySocket(); 56 private final WebSocketRecorder clientListener = new WebSocketRecorder(); 57 58 private final Executor serverExecutor = new SynchronousExecutor(); 59 private RealWebSocket server; 60 private boolean serverConnectionClosed; 61 private final MemorySocket server2client = new MemorySocket(); 62 private final WebSocketRecorder serverListener = new WebSocketRecorder(); 63 setUp()64 @Before public void setUp() { 65 Random random = new Random(0); 66 String url = "http://example.com/websocket"; 67 68 client = new RealWebSocket(true, server2client.source(), client2Server.sink(), random, 69 clientExecutor, clientListener, url) { 70 @Override protected void close() throws IOException { 71 if (clientConnectionClosed) { 72 throw new AssertionError("Already closed"); 73 } 74 clientConnectionClosed = true; 75 76 if (clientConnectionCloseThrows) { 77 throw new IOException("Oops!"); 78 } 79 } 80 }; 81 server = new RealWebSocket(false, client2Server.source(), server2client.sink(), random, 82 serverExecutor, serverListener, url) { 83 @Override protected void close() throws IOException { 84 if (serverConnectionClosed) { 85 throw new AssertionError("Already closed"); 86 } 87 serverConnectionClosed = true; 88 } 89 }; 90 } 91 tearDown()92 @After public void tearDown() { 93 clientListener.assertExhausted(); 94 serverListener.assertExhausted(); 95 } 96 nullMessageThrows()97 @Test public void nullMessageThrows() throws IOException { 98 try { 99 client.sendMessage(null); 100 fail(); 101 } catch (NullPointerException e) { 102 assertEquals("message == null", e.getMessage()); 103 } 104 } 105 textMessage()106 @Test public void textMessage() throws IOException { 107 client.sendMessage(RequestBody.create(TEXT, "Hello!")); 108 server.readMessage(); 109 serverListener.assertTextMessage("Hello!"); 110 } 111 binaryMessage()112 @Test public void binaryMessage() throws IOException { 113 client.sendMessage(RequestBody.create(BINARY, "Hello!")); 114 server.readMessage(); 115 serverListener.assertBinaryMessage(new byte[] { 'H', 'e', 'l', 'l', 'o', '!' }); 116 } 117 missingContentTypeThrows()118 @Test public void missingContentTypeThrows() throws IOException { 119 try { 120 client.sendMessage(RequestBody.create(null, "Hey!")); 121 fail(); 122 } catch (IllegalArgumentException e) { 123 assertEquals("Message content type was null. Must use WebSocket.TEXT or WebSocket.BINARY.", 124 e.getMessage()); 125 } 126 } 127 unknownContentTypeThrows()128 @Test public void unknownContentTypeThrows() throws IOException { 129 try { 130 client.sendMessage(RequestBody.create(MediaType.parse("text/plain"), "Hey!")); 131 fail(); 132 } catch (IllegalArgumentException e) { 133 assertEquals( 134 "Unknown message content type: text/plain. Must use WebSocket.TEXT or WebSocket.BINARY.", 135 e.getMessage()); 136 } 137 } 138 streamingMessage()139 @Test public void streamingMessage() throws IOException { 140 RequestBody message = new RequestBody() { 141 @Override public MediaType contentType() { 142 return TEXT; 143 } 144 145 @Override public void writeTo(BufferedSink sink) throws IOException { 146 sink.writeUtf8("Hel").flush(); 147 sink.writeUtf8("lo!").flush(); 148 sink.close(); 149 } 150 }; 151 client.sendMessage(message); 152 server.readMessage(); 153 serverListener.assertTextMessage("Hello!"); 154 } 155 streamingMessageCanInterleavePing()156 @Test public void streamingMessageCanInterleavePing() throws IOException, InterruptedException { 157 RequestBody message = new RequestBody() { 158 @Override public MediaType contentType() { 159 return TEXT; 160 } 161 162 @Override public void writeTo(BufferedSink sink) throws IOException { 163 sink.writeUtf8("Hel").flush(); 164 client.sendPing(new Buffer().writeUtf8("Pong?")); 165 sink.writeUtf8("lo!").flush(); 166 sink.close(); 167 } 168 }; 169 170 client.sendMessage(message); 171 server.readMessage(); 172 serverListener.assertTextMessage("Hello!"); 173 client.readMessage(); 174 clientListener.assertPong(new Buffer().writeUtf8("Pong?")); 175 } 176 pingWritesPong()177 @Test public void pingWritesPong() throws IOException, InterruptedException { 178 client.sendPing(new Buffer().writeUtf8("Hello!")); 179 server.readMessage(); // Read the ping, write the pong. 180 client.readMessage(); // Read the pong. 181 clientListener.assertPong(new Buffer().writeUtf8("Hello!")); 182 } 183 unsolicitedPong()184 @Test public void unsolicitedPong() throws IOException { 185 client.sendPong(new Buffer().writeUtf8("Hello!")); 186 server.readMessage(); 187 serverListener.assertPong(new Buffer().writeUtf8("Hello!")); 188 } 189 close()190 @Test public void close() throws IOException { 191 client.close(1000, "Hello!"); 192 assertFalse(server.readMessage()); // This will trigger a close response. 193 serverListener.assertClose(1000, "Hello!"); 194 assertFalse(client.readMessage()); 195 clientListener.assertClose(1000, "Hello!"); 196 } 197 clientCloseThenMethodsThrow()198 @Test public void clientCloseThenMethodsThrow() throws IOException { 199 client.close(1000, "Hello!"); 200 201 try { 202 client.sendPing(new Buffer().writeUtf8("Pong?")); 203 fail(); 204 } catch (IllegalStateException e) { 205 assertEquals("closed", e.getMessage()); 206 } 207 try { 208 client.close(1000, "Hello!"); 209 fail(); 210 } catch (IllegalStateException e) { 211 assertEquals("closed", e.getMessage()); 212 } 213 try { 214 client.sendMessage(RequestBody.create(TEXT, "Hello!")); 215 fail(); 216 } catch (IllegalStateException e) { 217 assertEquals("closed", e.getMessage()); 218 } 219 } 220 socketClosedDuringPingKillsWebSocket()221 @Test public void socketClosedDuringPingKillsWebSocket() throws IOException { 222 client2Server.close(); 223 224 try { 225 client.sendPing(new Buffer().writeUtf8("Ping!")); 226 fail(); 227 } catch (IOException ignored) { 228 } 229 230 // A failed write prevents further use of the WebSocket instance. 231 try { 232 client.sendMessage(RequestBody.create(TEXT, "Hello!")); 233 fail(); 234 } catch (IllegalStateException e) { 235 assertEquals("must call close()", e.getMessage()); 236 } 237 try { 238 client.sendPing(new Buffer().writeUtf8("Ping!")); 239 fail(); 240 } catch (IllegalStateException e) { 241 assertEquals("must call close()", e.getMessage()); 242 } 243 } 244 socketClosedDuringMessageKillsWebSocket()245 @Test public void socketClosedDuringMessageKillsWebSocket() throws IOException { 246 client2Server.close(); 247 248 try { 249 client.sendMessage(RequestBody.create(TEXT, "Hello!")); 250 fail(); 251 } catch (IOException ignored) { 252 } 253 254 // A failed write prevents further use of the WebSocket instance. 255 try { 256 client.sendMessage(RequestBody.create(TEXT, "Hello!")); 257 fail(); 258 } catch (IllegalStateException e) { 259 assertEquals("must call close()", e.getMessage()); 260 } 261 try { 262 client.sendPing(new Buffer().writeUtf8("Ping!")); 263 fail(); 264 } catch (IllegalStateException e) { 265 assertEquals("must call close()", e.getMessage()); 266 } 267 } 268 serverCloseThenWritingPingThrows()269 @Test public void serverCloseThenWritingPingThrows() throws IOException { 270 server.close(1000, "Hello!"); 271 client.readMessage(); 272 clientListener.assertClose(1000, "Hello!"); 273 274 try { 275 client.sendPing(new Buffer().writeUtf8("Pong?")); 276 fail(); 277 } catch (IOException e) { 278 assertEquals("closed", e.getMessage()); 279 } 280 } 281 serverCloseThenWritingMessageThrows()282 @Test public void serverCloseThenWritingMessageThrows() throws IOException { 283 server.close(1000, "Hello!"); 284 client.readMessage(); 285 clientListener.assertClose(1000, "Hello!"); 286 287 try { 288 client.sendMessage(RequestBody.create(TEXT, "Hi!")); 289 fail(); 290 } catch (IOException e) { 291 assertEquals("closed", e.getMessage()); 292 } 293 } 294 serverCloseThenWritingCloseThrows()295 @Test public void serverCloseThenWritingCloseThrows() throws IOException { 296 server.close(1000, "Hello!"); 297 client.readMessage(); 298 clientListener.assertClose(1000, "Hello!"); 299 300 try { 301 client.close(1000, "Bye!"); 302 fail(); 303 } catch (IOException e) { 304 assertEquals("closed", e.getMessage()); 305 } 306 } 307 serverCloseWhileWritingThrows()308 @Test public void serverCloseWhileWritingThrows() throws IOException { 309 RequestBody message = new RequestBody() { 310 @Override public MediaType contentType() { 311 return TEXT; 312 } 313 314 @Override public void writeTo(BufferedSink sink) throws IOException { 315 // Start writing data. 316 sink.writeUtf8("Hel").flush(); 317 318 server.close(1000, "Hello!"); 319 client.readMessage(); 320 clientListener.assertClose(1000, "Hello!"); 321 322 try { 323 sink.flush(); // No flushing. 324 fail(); 325 } catch (IOException e) { 326 assertEquals("closed", e.getMessage()); 327 } 328 try { 329 sink.close(); // No closing because this requires writing a frame. 330 fail(); 331 } catch (IOException e) { 332 assertEquals("closed", e.getMessage()); 333 } 334 } 335 }; 336 client.sendMessage(message); 337 } 338 clientCloseClosesConnection()339 @Test public void clientCloseClosesConnection() throws IOException { 340 client.close(1000, "Hello!"); 341 assertFalse(clientConnectionClosed); 342 server.readMessage(); // Read client close, send server close. 343 serverListener.assertClose(1000, "Hello!"); 344 345 client.readMessage(); // Read server close, close connection. 346 assertTrue(clientConnectionClosed); 347 clientListener.assertClose(1000, "Hello!"); 348 } 349 serverCloseClosesConnection()350 @Test public void serverCloseClosesConnection() throws IOException { 351 server.close(1000, "Hello!"); 352 353 client.readMessage(); // Read server close, send client close, close connection. 354 assertTrue(clientConnectionClosed); 355 clientListener.assertClose(1000, "Hello!"); 356 357 server.readMessage(); 358 serverListener.assertClose(1000, "Hello!"); 359 } 360 clientAndServerCloseClosesConnection()361 @Test public void clientAndServerCloseClosesConnection() throws IOException { 362 // Send close from both sides at the same time. 363 server.close(1000, "Hello!"); 364 client.close(1000, "Hi!"); 365 assertFalse(clientConnectionClosed); 366 367 client.readMessage(); // Read close, close connection close. 368 assertTrue(clientConnectionClosed); 369 clientListener.assertClose(1000, "Hello!"); 370 371 server.readMessage(); 372 serverListener.assertClose(1000, "Hi!"); 373 374 serverListener.assertExhausted(); // Client should not have sent second close. 375 clientListener.assertExhausted(); // Server should not have sent second close. 376 } 377 serverCloseBreaksReadMessageLoop()378 @Test public void serverCloseBreaksReadMessageLoop() throws IOException { 379 server.sendMessage(RequestBody.create(TEXT, "Hello!")); 380 server.close(1000, "Bye!"); 381 assertTrue(client.readMessage()); 382 clientListener.assertTextMessage("Hello!"); 383 assertFalse(client.readMessage()); 384 clientListener.assertClose(1000, "Bye!"); 385 } 386 protocolErrorBeforeCloseSendsClose()387 @Test public void protocolErrorBeforeCloseSendsClose() throws IOException { 388 server2client.raw().write(ByteString.decodeHex("0a00")); // Invalid non-final ping frame. 389 390 client.readMessage(); // Detects error, send close, close connection. 391 assertTrue(clientConnectionClosed); 392 clientListener.assertFailure(ProtocolException.class, "Control frames must be final."); 393 394 server.readMessage(); 395 serverListener.assertClose(1002, ""); 396 } 397 protocolErrorAfterCloseDoesNotSendClose()398 @Test public void protocolErrorAfterCloseDoesNotSendClose() throws IOException { 399 client.close(1000, "Hello!"); 400 assertFalse(clientConnectionClosed); // Not closed until close reply is received. 401 server2client.raw().write(ByteString.decodeHex("0a00")); // Invalid non-final ping frame. 402 403 client.readMessage(); // Detects error, closes connection immediately since close already sent. 404 assertTrue(clientConnectionClosed); 405 clientListener.assertFailure(ProtocolException.class, "Control frames must be final."); 406 407 server.readMessage(); 408 serverListener.assertClose(1000, "Hello!"); 409 410 serverListener.assertExhausted(); // Client should not have sent second close. 411 } 412 closeThrowingClosesConnection()413 @Test public void closeThrowingClosesConnection() { 414 client2Server.close(); 415 416 try { 417 client.close(1000, null); 418 fail(); 419 } catch (IOException ignored) { 420 } 421 assertTrue(clientConnectionClosed); 422 } 423 closeMessageAndConnectionCloseThrowingDoesNotMaskOriginal()424 @Test public void closeMessageAndConnectionCloseThrowingDoesNotMaskOriginal() throws IOException { 425 client2Server.close(); 426 clientConnectionCloseThrows = true; 427 428 try { 429 client.close(1000, "Bye!"); 430 fail(); 431 } catch (IOException e) { 432 assertNotEquals("Oops!", e.getMessage()); 433 } 434 assertTrue(clientConnectionClosed); 435 } 436 peerConnectionCloseThrowingDoesNotPropagate()437 @Test public void peerConnectionCloseThrowingDoesNotPropagate() throws IOException { 438 clientConnectionCloseThrows = true; 439 440 server.close(1000, "Bye!"); 441 client.readMessage(); 442 assertTrue(clientConnectionClosed); 443 clientListener.assertClose(1000, "Bye!"); 444 445 server.readMessage(); 446 serverListener.assertClose(1000, "Bye!"); 447 } 448 449 static final class MemorySocket implements Closeable { 450 private final Buffer buffer = new Buffer(); 451 private boolean closed; 452 close()453 @Override public void close() { 454 closed = true; 455 } 456 raw()457 Buffer raw() { 458 return buffer; 459 } 460 source()461 BufferedSource source() { 462 return Okio.buffer(new Source() { 463 @Override public long read(Buffer sink, long byteCount) throws IOException { 464 if (closed) throw new IOException("closed"); 465 return buffer.read(sink, byteCount); 466 } 467 468 @Override public Timeout timeout() { 469 return Timeout.NONE; 470 } 471 472 @Override public void close() throws IOException { 473 closed = true; 474 } 475 }); 476 } 477 sink()478 BufferedSink sink() { 479 return Okio.buffer(new Sink() { 480 @Override public void write(Buffer source, long byteCount) throws IOException { 481 if (closed) throw new IOException("closed"); 482 buffer.write(source, byteCount); 483 } 484 485 @Override public void flush() throws IOException { 486 } 487 488 @Override public Timeout timeout() { 489 return Timeout.NONE; 490 } 491 492 @Override public void close() throws IOException { 493 closed = true; 494 } 495 }); 496 } 497 } 498 499 static final class SynchronousExecutor implements Executor { 500 @Override public void execute(Runnable command) { 501 command.run(); 502 } 503 } 504 } 505