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.ResponseBody; 19 import com.squareup.okhttp.ws.WebSocketRecorder; 20 import java.io.EOFException; 21 import java.io.IOException; 22 import java.net.ProtocolException; 23 import java.util.Random; 24 import java.util.concurrent.atomic.AtomicReference; 25 import java.util.regex.Pattern; 26 import okio.Buffer; 27 import okio.BufferedSource; 28 import okio.ByteString; 29 import org.junit.After; 30 import org.junit.Test; 31 32 import static com.squareup.okhttp.ws.WebSocketRecorder.MessageDelegate; 33 import static org.junit.Assert.assertEquals; 34 import static org.junit.Assert.assertNotNull; 35 import static org.junit.Assert.assertTrue; 36 import static org.junit.Assert.fail; 37 38 public final class WebSocketReaderTest { 39 private final Buffer data = new Buffer(); 40 private final WebSocketRecorder callback = new WebSocketRecorder(); 41 private final Random random = new Random(0); 42 43 // Mutually exclusive. Use the one corresponding to the peer whose behavior you wish to test. 44 private final WebSocketReader serverReader = new WebSocketReader(false, data, callback); 45 private final WebSocketReader clientReader = new WebSocketReader(true, data, callback); 46 47 @After public void tearDown() { 48 callback.assertExhausted(); 49 } 50 51 @Test public void controlFramesMustBeFinal() throws IOException { 52 data.write(ByteString.decodeHex("0a00")); // Empty ping. 53 try { 54 clientReader.processNextFrame(); 55 fail(); 56 } catch (ProtocolException e) { 57 assertEquals("Control frames must be final.", e.getMessage()); 58 } 59 } 60 61 @Test public void reservedFlagsAreUnsupported() throws IOException { 62 data.write(ByteString.decodeHex("9a00")); // Empty ping, flag 1 set. 63 try { 64 clientReader.processNextFrame(); 65 fail(); 66 } catch (ProtocolException e) { 67 assertEquals("Reserved flags are unsupported.", e.getMessage()); 68 } 69 data.clear(); 70 data.write(ByteString.decodeHex("aa00")); // Empty ping, flag 2 set. 71 try { 72 clientReader.processNextFrame(); 73 fail(); 74 } catch (ProtocolException e) { 75 assertEquals("Reserved flags are unsupported.", e.getMessage()); 76 } 77 data.clear(); 78 data.write(ByteString.decodeHex("ca00")); // Empty ping, flag 3 set. 79 try { 80 clientReader.processNextFrame(); 81 fail(); 82 } catch (ProtocolException e) { 83 assertEquals("Reserved flags are unsupported.", e.getMessage()); 84 } 85 } 86 87 @Test public void clientSentFramesMustBeMasked() throws IOException { 88 data.write(ByteString.decodeHex("8100")); 89 try { 90 serverReader.processNextFrame(); 91 fail(); 92 } catch (ProtocolException e) { 93 assertEquals("Client-sent frames must be masked. Server sent must not.", e.getMessage()); 94 } 95 } 96 97 @Test public void serverSentFramesMustNotBeMasked() throws IOException { 98 data.write(ByteString.decodeHex("8180")); 99 try { 100 clientReader.processNextFrame(); 101 fail(); 102 } catch (ProtocolException e) { 103 assertEquals("Client-sent frames must be masked. Server sent must not.", e.getMessage()); 104 } 105 } 106 107 @Test public void controlFramePayloadMax() throws IOException { 108 data.write(ByteString.decodeHex("8a7e007e")); 109 try { 110 clientReader.processNextFrame(); 111 fail(); 112 } catch (ProtocolException e) { 113 assertEquals("Control frame must be less than 125B.", e.getMessage()); 114 } 115 } 116 117 @Test public void clientSimpleHello() throws IOException { 118 data.write(ByteString.decodeHex("810548656c6c6f")); // Hello 119 clientReader.processNextFrame(); 120 callback.assertTextMessage("Hello"); 121 } 122 123 @Test public void serverSimpleHello() throws IOException { 124 data.write(ByteString.decodeHex("818537fa213d7f9f4d5158")); // Hello 125 serverReader.processNextFrame(); 126 callback.assertTextMessage("Hello"); 127 } 128 129 @Test public void clientFramePayloadShort() throws IOException { 130 data.write(ByteString.decodeHex("817E000548656c6c6f")); // Hello 131 clientReader.processNextFrame(); 132 callback.assertTextMessage("Hello"); 133 } 134 135 @Test public void clientFramePayloadLong() throws IOException { 136 data.write(ByteString.decodeHex("817f000000000000000548656c6c6f")); // Hello 137 clientReader.processNextFrame(); 138 callback.assertTextMessage("Hello"); 139 } 140 141 @Test public void clientFramePayloadTooLongThrows() throws IOException { 142 data.write(ByteString.decodeHex("817f8000000000000000")); 143 try { 144 clientReader.processNextFrame(); 145 fail(); 146 } catch (ProtocolException e) { 147 assertEquals("Frame length 0x8000000000000000 > 0x7FFFFFFFFFFFFFFF", e.getMessage()); 148 } 149 } 150 151 @Test public void serverHelloTwoChunks() throws IOException { 152 data.write(ByteString.decodeHex("818537fa213d7f9f4d")); // Hel 153 154 final Buffer sink = new Buffer(); 155 callback.setNextMessageDelegate(new MessageDelegate() { 156 @Override public void onMessage(ResponseBody message) throws IOException { 157 BufferedSource source = message.source(); 158 source.readFully(sink, 3); // Read "Hel" 159 data.write(ByteString.decodeHex("5158")); // lo 160 source.readFully(sink, 2); // Read "lo" 161 source.close(); 162 } 163 }); 164 serverReader.processNextFrame(); 165 166 assertEquals("Hello", sink.readUtf8()); 167 } 168 169 @Test public void clientTwoFrameHello() throws IOException { 170 data.write(ByteString.decodeHex("010348656c")); // Hel 171 data.write(ByteString.decodeHex("80026c6f")); // lo 172 clientReader.processNextFrame(); 173 callback.assertTextMessage("Hello"); 174 } 175 176 @Test public void clientTwoFrameHelloWithPongs() throws IOException { 177 data.write(ByteString.decodeHex("010348656c")); // Hel 178 data.write(ByteString.decodeHex("8a00")); // Pong 179 data.write(ByteString.decodeHex("8a00")); // Pong 180 data.write(ByteString.decodeHex("8a00")); // Pong 181 data.write(ByteString.decodeHex("8a00")); // Pong 182 data.write(ByteString.decodeHex("80026c6f")); // lo 183 clientReader.processNextFrame(); 184 callback.assertPong(null); 185 callback.assertPong(null); 186 callback.assertPong(null); 187 callback.assertPong(null); 188 callback.assertTextMessage("Hello"); 189 } 190 191 @Test public void clientIncompleteMessageBodyThrows() throws IOException { 192 data.write(ByteString.decodeHex("810548656c")); // Length = 5, "Hel" 193 try { 194 clientReader.processNextFrame(); 195 fail(); 196 } catch (EOFException ignored) { 197 } 198 } 199 200 @Test public void clientIncompleteControlFrameBodyThrows() throws IOException { 201 data.write(ByteString.decodeHex("8a0548656c")); // Length = 5, "Hel" 202 try { 203 clientReader.processNextFrame(); 204 fail(); 205 } catch (EOFException ignored) { 206 } 207 } 208 209 @Test public void serverIncompleteMessageBodyThrows() throws IOException { 210 data.write(ByteString.decodeHex("818537fa213d7f9f4d")); // Length = 5, "Hel" 211 try { 212 serverReader.processNextFrame(); 213 fail(); 214 } catch (EOFException ignored) { 215 } 216 } 217 218 @Test public void serverIncompleteControlFrameBodyThrows() throws IOException { 219 data.write(ByteString.decodeHex("8a8537fa213d7f9f4d")); // Length = 5, "Hel" 220 try { 221 serverReader.processNextFrame(); 222 fail(); 223 } catch (EOFException ignored) { 224 } 225 } 226 227 @Test public void clientSimpleBinary() throws IOException { 228 byte[] bytes = binaryData(256); 229 data.write(ByteString.decodeHex("827E0100")).write(bytes); 230 clientReader.processNextFrame(); 231 callback.assertBinaryMessage(bytes); 232 } 233 234 @Test public void clientTwoFrameBinary() throws IOException { 235 byte[] bytes = binaryData(200); 236 data.write(ByteString.decodeHex("0264")).write(bytes, 0, 100); 237 data.write(ByteString.decodeHex("8064")).write(bytes, 100, 100); 238 clientReader.processNextFrame(); 239 callback.assertBinaryMessage(bytes); 240 } 241 242 @Test public void twoFrameNotContinuation() throws IOException { 243 byte[] bytes = binaryData(200); 244 data.write(ByteString.decodeHex("0264")).write(bytes, 0, 100); 245 data.write(ByteString.decodeHex("8264")).write(bytes, 100, 100); 246 try { 247 clientReader.processNextFrame(); 248 fail(); 249 } catch (ProtocolException e) { 250 assertEquals("Expected continuation opcode. Got: 2", e.getMessage()); 251 } 252 } 253 254 @Test public void noCloseErrors() throws IOException { 255 data.write(ByteString.decodeHex("810548656c6c6f")); // Hello 256 callback.setNextMessageDelegate(new MessageDelegate() { 257 @Override public void onMessage(ResponseBody body) throws IOException { 258 body.source().readAll(new Buffer()); 259 } 260 }); 261 try { 262 clientReader.processNextFrame(); 263 fail(); 264 } catch (IllegalStateException e) { 265 assertEquals("Listener failed to call close on message payload.", e.getMessage()); 266 } 267 } 268 269 @Test public void closeExhaustsMessage() throws IOException { 270 data.write(ByteString.decodeHex("810548656c6c6f")); // Hello 271 data.write(ByteString.decodeHex("810448657921")); // Hey! 272 273 final Buffer sink = new Buffer(); 274 callback.setNextMessageDelegate(new MessageDelegate() { 275 @Override public void onMessage(ResponseBody message) throws IOException { 276 message.source().read(sink, 3); 277 message.close(); 278 } 279 }); 280 281 clientReader.processNextFrame(); 282 assertEquals("Hel", sink.readUtf8()); 283 284 clientReader.processNextFrame(); 285 callback.assertTextMessage("Hey!"); 286 } 287 288 @Test public void closeExhaustsMessageOverControlFrames() throws IOException { 289 data.write(ByteString.decodeHex("010348656c")); // Hel 290 data.write(ByteString.decodeHex("8a00")); // Pong 291 data.write(ByteString.decodeHex("8a00")); // Pong 292 data.write(ByteString.decodeHex("80026c6f")); // lo 293 data.write(ByteString.decodeHex("810448657921")); // Hey! 294 295 final Buffer sink = new Buffer(); 296 callback.setNextMessageDelegate(new MessageDelegate() { 297 @Override public void onMessage(ResponseBody message) throws IOException { 298 message.source().read(sink, 2); 299 message.close(); 300 } 301 }); 302 303 clientReader.processNextFrame(); 304 assertEquals("He", sink.readUtf8()); 305 callback.assertPong(null); 306 callback.assertPong(null); 307 308 clientReader.processNextFrame(); 309 callback.assertTextMessage("Hey!"); 310 } 311 312 @Test public void closedMessageSourceThrows() throws IOException { 313 data.write(ByteString.decodeHex("810548656c6c6f")); // Hello 314 315 final AtomicReference<Exception> exception = new AtomicReference<>(); 316 callback.setNextMessageDelegate(new MessageDelegate() { 317 @Override public void onMessage(ResponseBody message) throws IOException { 318 message.close(); 319 try { 320 message.source().readAll(new Buffer()); 321 fail(); 322 } catch (IllegalStateException e) { 323 exception.set(e); 324 } 325 } 326 }); 327 clientReader.processNextFrame(); 328 329 assertNotNull(exception.get()); 330 } 331 332 @Test public void emptyPingCallsCallback() throws IOException { 333 data.write(ByteString.decodeHex("8900")); // Empty ping 334 clientReader.processNextFrame(); 335 callback.assertPing(null); 336 } 337 338 @Test public void pingCallsCallback() throws IOException { 339 data.write(ByteString.decodeHex("890548656c6c6f")); // Ping with "Hello" 340 clientReader.processNextFrame(); 341 callback.assertPing(new Buffer().writeUtf8("Hello")); 342 } 343 344 @Test public void emptyCloseCallsCallback() throws IOException { 345 data.write(ByteString.decodeHex("8800")); // Empty close 346 clientReader.processNextFrame(); 347 callback.assertClose(1000, ""); 348 } 349 350 @Test public void closeLengthOfOneThrows() throws IOException { 351 data.write(ByteString.decodeHex("880100")); // Close with invalid 1-byte payload 352 try { 353 clientReader.processNextFrame(); 354 fail(); 355 } catch (ProtocolException e) { 356 assertEquals("Malformed close payload length of 1.", e.getMessage()); 357 } 358 } 359 360 @Test public void closeCallsCallback() throws IOException { 361 data.write(ByteString.decodeHex("880703e848656c6c6f")); // Close with code and reason 362 clientReader.processNextFrame(); 363 callback.assertClose(1000, "Hello"); 364 } 365 366 @Test public void closeOutOfRangeThrows() throws IOException { 367 data.write(ByteString.decodeHex("88020001")); // Close with code 1 368 try { 369 clientReader.processNextFrame(); 370 fail(); 371 } catch (ProtocolException e) { 372 assertEquals("Code must be in range [1000,5000): 1", e.getMessage()); 373 } 374 data.write(ByteString.decodeHex("88021388")); // Close with code 5000 375 try { 376 clientReader.processNextFrame(); 377 fail(); 378 } catch (ProtocolException e) { 379 assertEquals("Code must be in range [1000,5000): 5000", e.getMessage()); 380 } 381 } 382 383 @Test public void closeReservedSetThrows() throws IOException { 384 data.write(ByteString.decodeHex("880203ec")); // Close with code 1004 385 data.write(ByteString.decodeHex("880203ed")); // Close with code 1005 386 data.write(ByteString.decodeHex("880203ee")); // Close with code 1006 387 for (int i = 1012; i <= 2999; i++) { 388 data.write(ByteString.decodeHex("8802" + String.format("%04X", i))); // Close with code 'i' 389 } 390 391 int count = 0; 392 for (; !data.exhausted(); count++) { 393 try { 394 clientReader.processNextFrame(); 395 fail(); 396 } catch (ProtocolException e) { 397 String message = e.getMessage(); 398 assertTrue(message, Pattern.matches("Code \\d+ is reserved and may not be used.", message)); 399 } 400 } 401 assertEquals(1991, count); 402 } 403 404 private byte[] binaryData(int length) { 405 byte[] junk = new byte[length]; 406 random.nextBytes(junk); 407 return junk; 408 } 409 } 410