• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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