| /* |
| * 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 com.squareup.okhttp.internal.ws; |
| |
| import com.squareup.okhttp.ws.WebSocketRecorder; |
| import java.io.EOFException; |
| import java.io.IOException; |
| import java.net.ProtocolException; |
| import java.util.Random; |
| import java.util.concurrent.atomic.AtomicReference; |
| import okio.Buffer; |
| import okio.BufferedSource; |
| import okio.ByteString; |
| import org.junit.After; |
| import org.junit.Test; |
| |
| import static com.squareup.okhttp.ws.WebSocket.PayloadType; |
| import static com.squareup.okhttp.ws.WebSocketRecorder.MessageDelegate; |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertNotNull; |
| import static org.junit.Assert.fail; |
| |
| public final class WebSocketReaderTest { |
| private final Buffer data = new Buffer(); |
| private final WebSocketRecorder callback = new WebSocketRecorder(); |
| private final Random random = new Random(0); |
| |
| // Mutually exclusive. Use the one corresponding to the peer whose behavior you wish to test. |
| private final WebSocketReader serverReader = new WebSocketReader(false, data, callback); |
| private final WebSocketReader clientReader = new WebSocketReader(true, data, callback); |
| |
| @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. Server sent must not.", e.getMessage()); |
| } |
| } |
| |
| @Test public void serverSentFramesMustNotBeMasked() throws IOException { |
| data.write(ByteString.decodeHex("8180")); |
| try { |
| clientReader.processNextFrame(); |
| fail(); |
| } catch (ProtocolException e) { |
| assertEquals("Client-sent frames must be masked. Server sent must not.", 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 |
| |
| final Buffer sink = new Buffer(); |
| callback.setNextMessageDelegate(new MessageDelegate() { |
| @Override public void onMessage(BufferedSource payload, PayloadType type) throws IOException { |
| payload.readFully(sink, 3); // Read "Hel" |
| data.write(ByteString.decodeHex("5158")); // lo |
| payload.readFully(sink, 2); // Read "lo" |
| payload.close(); |
| } |
| }); |
| serverReader.processNextFrame(); |
| |
| assertEquals("Hello", sink.readUtf8()); |
| } |
| |
| @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(null); |
| callback.assertPong(null); |
| callback.assertPong(null); |
| callback.assertPong(null); |
| 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(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(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 noCloseErrors() throws IOException { |
| data.write(ByteString.decodeHex("810548656c6c6f")); // Hello |
| callback.setNextMessageDelegate(new MessageDelegate() { |
| @Override public void onMessage(BufferedSource payload, PayloadType type) throws IOException { |
| payload.readAll(new Buffer()); |
| } |
| }); |
| try { |
| clientReader.processNextFrame(); |
| fail(); |
| } catch (IllegalStateException e) { |
| assertEquals("Listener failed to call close on message payload.", e.getMessage()); |
| } |
| } |
| |
| @Test public void closeExhaustsMessage() throws IOException { |
| data.write(ByteString.decodeHex("810548656c6c6f")); // Hello |
| data.write(ByteString.decodeHex("810448657921")); // Hey! |
| |
| final Buffer sink = new Buffer(); |
| callback.setNextMessageDelegate(new MessageDelegate() { |
| @Override public void onMessage(BufferedSource payload, PayloadType type) throws IOException { |
| payload.read(sink, 3); |
| payload.close(); |
| } |
| }); |
| |
| clientReader.processNextFrame(); |
| assertEquals("Hel", sink.readUtf8()); |
| |
| clientReader.processNextFrame(); |
| callback.assertTextMessage("Hey!"); |
| } |
| |
| @Test public void closeExhaustsMessageOverControlFrames() throws IOException { |
| data.write(ByteString.decodeHex("010348656c")); // Hel |
| data.write(ByteString.decodeHex("8a00")); // Pong |
| data.write(ByteString.decodeHex("8a00")); // Pong |
| data.write(ByteString.decodeHex("80026c6f")); // lo |
| data.write(ByteString.decodeHex("810448657921")); // Hey! |
| |
| final Buffer sink = new Buffer(); |
| callback.setNextMessageDelegate(new MessageDelegate() { |
| @Override public void onMessage(BufferedSource payload, PayloadType type) throws IOException { |
| payload.read(sink, 2); |
| payload.close(); |
| } |
| }); |
| |
| clientReader.processNextFrame(); |
| assertEquals("He", sink.readUtf8()); |
| callback.assertPong(null); |
| callback.assertPong(null); |
| |
| clientReader.processNextFrame(); |
| callback.assertTextMessage("Hey!"); |
| } |
| |
| @Test public void closedMessageSourceThrows() throws IOException { |
| data.write(ByteString.decodeHex("810548656c6c6f")); // Hello |
| |
| final AtomicReference<Exception> exception = new AtomicReference<>(); |
| callback.setNextMessageDelegate(new MessageDelegate() { |
| @Override public void onMessage(BufferedSource payload, PayloadType type) throws IOException { |
| payload.close(); |
| try { |
| payload.readAll(new Buffer()); |
| fail(); |
| } catch (IllegalStateException e) { |
| exception.set(e); |
| } |
| } |
| }); |
| clientReader.processNextFrame(); |
| |
| assertNotNull(exception.get()); |
| } |
| |
| @Test public void emptyPingCallsCallback() throws IOException { |
| data.write(ByteString.decodeHex("8900")); // Empty ping |
| clientReader.processNextFrame(); |
| callback.assertPing(null); |
| } |
| |
| @Test public void pingCallsCallback() throws IOException { |
| data.write(ByteString.decodeHex("890548656c6c6f")); // Ping with "Hello" |
| clientReader.processNextFrame(); |
| callback.assertPing(new Buffer().writeUtf8("Hello")); |
| } |
| |
| @Test public void emptyCloseCallsCallback() throws IOException { |
| data.write(ByteString.decodeHex("8800")); // Empty close |
| clientReader.processNextFrame(); |
| callback.assertClose(0, ""); |
| } |
| |
| @Test public void closeCallsCallback() throws IOException { |
| data.write(ByteString.decodeHex("880703e848656c6c6f")); // Close with code and reason |
| clientReader.processNextFrame(); |
| callback.assertClose(1000, "Hello"); |
| } |
| |
| private byte[] binaryData(int length) { |
| byte[] junk = new byte[length]; |
| random.nextBytes(junk); |
| return junk; |
| } |
| } |