Home | History | Annotate | Download | only in ws
      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