/* * Copyright (C) 2014 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.internal.ws; import java.io.EOFException; import java.io.IOException; import java.net.ProtocolException; import java.util.Random; import java.util.regex.Pattern; import okhttp3.internal.Util; import okio.Buffer; import okio.ByteString; import org.junit.After; import org.junit.Test; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; public final class WebSocketReaderTest { private final Buffer data = new Buffer(); private final WebSocketRecorder callback = new WebSocketRecorder("client"); private final Random random = new Random(0); // Mutually exclusive. Use the one corresponding to the peer whose behavior you wish to test. final WebSocketReader serverReader = new WebSocketReader(false, data, callback.asFrameCallback()); final WebSocketReader clientReader = new WebSocketReader(true, data, callback.asFrameCallback()); @After public void tearDown() { callback.assertExhausted(); } @Test public void controlFramesMustBeFinal() throws IOException { data.write(ByteString.decodeHex("0a00")); // Empty ping. try { clientReader.processNextFrame(); fail(); } catch (ProtocolException e) { assertEquals("Control frames must be final.", e.getMessage()); } } @Test public void reservedFlagsAreUnsupported() throws IOException { data.write(ByteString.decodeHex("9a00")); // Empty ping, flag 1 set. try { clientReader.processNextFrame(); fail(); } catch (ProtocolException e) { assertEquals("Reserved flags are unsupported.", e.getMessage()); } data.clear(); data.write(ByteString.decodeHex("aa00")); // Empty ping, flag 2 set. try { clientReader.processNextFrame(); fail(); } catch (ProtocolException e) { assertEquals("Reserved flags are unsupported.", e.getMessage()); } data.clear(); data.write(ByteString.decodeHex("ca00")); // Empty ping, flag 3 set. try { clientReader.processNextFrame(); fail(); } catch (ProtocolException e) { assertEquals("Reserved flags are unsupported.", e.getMessage()); } } @Test public void clientSentFramesMustBeMasked() throws IOException { data.write(ByteString.decodeHex("8100")); try { serverReader.processNextFrame(); fail(); } catch (ProtocolException e) { assertEquals("Client-sent frames must be masked.", e.getMessage()); } } @Test public void serverSentFramesMustNotBeMasked() throws IOException { data.write(ByteString.decodeHex("8180")); try { clientReader.processNextFrame(); fail(); } catch (ProtocolException e) { assertEquals("Server-sent frames must not be masked.", e.getMessage()); } } @Test public void controlFramePayloadMax() throws IOException { data.write(ByteString.decodeHex("8a7e007e")); try { clientReader.processNextFrame(); fail(); } catch (ProtocolException e) { assertEquals("Control frame must be less than 125B.", e.getMessage()); } } @Test public void clientSimpleHello() throws IOException { data.write(ByteString.decodeHex("810548656c6c6f")); // Hello clientReader.processNextFrame(); callback.assertTextMessage("Hello"); } @Test public void serverSimpleHello() throws IOException { data.write(ByteString.decodeHex("818537fa213d7f9f4d5158")); // Hello serverReader.processNextFrame(); callback.assertTextMessage("Hello"); } @Test public void clientFramePayloadShort() throws IOException { data.write(ByteString.decodeHex("817E000548656c6c6f")); // Hello clientReader.processNextFrame(); callback.assertTextMessage("Hello"); } @Test public void clientFramePayloadLong() throws IOException { data.write(ByteString.decodeHex("817f000000000000000548656c6c6f")); // Hello clientReader.processNextFrame(); callback.assertTextMessage("Hello"); } @Test public void clientFramePayloadTooLongThrows() throws IOException { data.write(ByteString.decodeHex("817f8000000000000000")); try { clientReader.processNextFrame(); fail(); } catch (ProtocolException e) { assertEquals("Frame length 0x8000000000000000 > 0x7FFFFFFFFFFFFFFF", e.getMessage()); } } @Test public void serverHelloTwoChunks() throws IOException { data.write(ByteString.decodeHex("818537fa213d7f9f4d")); // Hel data.write(ByteString.decodeHex("5158")); // lo serverReader.processNextFrame(); callback.assertTextMessage("Hello"); } @Test public void clientTwoFrameHello() throws IOException { data.write(ByteString.decodeHex("010348656c")); // Hel data.write(ByteString.decodeHex("80026c6f")); // lo clientReader.processNextFrame(); callback.assertTextMessage("Hello"); } @Test public void clientTwoFrameHelloWithPongs() throws IOException { data.write(ByteString.decodeHex("010348656c")); // Hel data.write(ByteString.decodeHex("8a00")); // Pong data.write(ByteString.decodeHex("8a00")); // Pong data.write(ByteString.decodeHex("8a00")); // Pong data.write(ByteString.decodeHex("8a00")); // Pong data.write(ByteString.decodeHex("80026c6f")); // lo clientReader.processNextFrame(); callback.assertPong(ByteString.EMPTY); callback.assertPong(ByteString.EMPTY); callback.assertPong(ByteString.EMPTY); callback.assertPong(ByteString.EMPTY); callback.assertTextMessage("Hello"); } @Test public void clientIncompleteMessageBodyThrows() throws IOException { data.write(ByteString.decodeHex("810548656c")); // Length = 5, "Hel" try { clientReader.processNextFrame(); fail(); } catch (EOFException ignored) { } } @Test public void clientIncompleteControlFrameBodyThrows() throws IOException { data.write(ByteString.decodeHex("8a0548656c")); // Length = 5, "Hel" try { clientReader.processNextFrame(); fail(); } catch (EOFException ignored) { } } @Test public void serverIncompleteMessageBodyThrows() throws IOException { data.write(ByteString.decodeHex("818537fa213d7f9f4d")); // Length = 5, "Hel" try { serverReader.processNextFrame(); fail(); } catch (EOFException ignored) { } } @Test public void serverIncompleteControlFrameBodyThrows() throws IOException { data.write(ByteString.decodeHex("8a8537fa213d7f9f4d")); // Length = 5, "Hel" try { serverReader.processNextFrame(); fail(); } catch (EOFException ignored) { } } @Test public void clientSimpleBinary() throws IOException { byte[] bytes = binaryData(256); data.write(ByteString.decodeHex("827E0100")).write(bytes); clientReader.processNextFrame(); callback.assertBinaryMessage(ByteString.of(bytes)); } @Test public void clientTwoFrameBinary() throws IOException { byte[] bytes = binaryData(200); data.write(ByteString.decodeHex("0264")).write(bytes, 0, 100); data.write(ByteString.decodeHex("8064")).write(bytes, 100, 100); clientReader.processNextFrame(); callback.assertBinaryMessage(ByteString.of(bytes)); } @Test public void twoFrameNotContinuation() throws IOException { byte[] bytes = binaryData(200); data.write(ByteString.decodeHex("0264")).write(bytes, 0, 100); data.write(ByteString.decodeHex("8264")).write(bytes, 100, 100); try { clientReader.processNextFrame(); fail(); } catch (ProtocolException e) { assertEquals("Expected continuation opcode. Got: 2", e.getMessage()); } } @Test public void emptyPingCallsCallback() throws IOException { data.write(ByteString.decodeHex("8900")); // Empty ping clientReader.processNextFrame(); callback.assertPing(ByteString.EMPTY); } @Test public void pingCallsCallback() throws IOException { data.write(ByteString.decodeHex("890548656c6c6f")); // Ping with "Hello" clientReader.processNextFrame(); callback.assertPing(ByteString.encodeUtf8("Hello")); } @Test public void emptyCloseCallsCallback() throws IOException { data.write(ByteString.decodeHex("8800")); // Empty close clientReader.processNextFrame(); callback.assertClosing(1005, ""); } @Test public void closeLengthOfOneThrows() throws IOException { data.write(ByteString.decodeHex("880100")); // Close with invalid 1-byte payload try { clientReader.processNextFrame(); fail(); } catch (ProtocolException e) { assertEquals("Malformed close payload length of 1.", e.getMessage()); } } @Test public void closeCallsCallback() throws IOException { data.write(ByteString.decodeHex("880703e848656c6c6f")); // Close with code and reason clientReader.processNextFrame(); callback.assertClosing(1000, "Hello"); } @Test public void closeOutOfRangeThrows() throws IOException { data.write(ByteString.decodeHex("88020001")); // Close with code 1 try { clientReader.processNextFrame(); fail(); } catch (ProtocolException e) { assertEquals("Code must be in range [1000,5000): 1", e.getMessage()); } data.write(ByteString.decodeHex("88021388")); // Close with code 5000 try { clientReader.processNextFrame(); fail(); } catch (ProtocolException e) { assertEquals("Code must be in range [1000,5000): 5000", e.getMessage()); } } @Test public void closeReservedSetThrows() throws IOException { data.write(ByteString.decodeHex("880203ec")); // Close with code 1004 data.write(ByteString.decodeHex("880203ed")); // Close with code 1005 data.write(ByteString.decodeHex("880203ee")); // Close with code 1006 for (int i = 1012; i <= 2999; i++) { data.write(ByteString.decodeHex("8802" + Util.format("%04X", i))); // Close with code 'i' } int count = 0; for (; !data.exhausted(); count++) { try { clientReader.processNextFrame(); fail(); } catch (ProtocolException e) { String message = e.getMessage(); assertTrue(message, Pattern.matches("Code \\d+ is reserved and may not be used.", message)); } } assertEquals(1991, count); } private byte[] binaryData(int length) { byte[] junk = new byte[length]; random.nextBytes(junk); return junk; } }