/*
* 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.net.SocketTimeoutException;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Logger;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.RecordingHostnameVerifier;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.TestLogHandler;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;
import okhttp3.internal.tls.SslClient;
import okhttp3.mockwebserver.Dispatcher;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import okhttp3.mockwebserver.SocketPolicy;
import okio.Buffer;
import okio.ByteString;
import org.junit.After;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import static okhttp3.TestUtil.defaultClient;
import static okhttp3.TestUtil.repeat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public final class WebSocketHttpTest {
@Rule public final MockWebServer webServer = new MockWebServer();
private final SslClient sslClient = SslClient.localhost();
private final WebSocketRecorder clientListener = new WebSocketRecorder("client");
private final WebSocketRecorder serverListener = new WebSocketRecorder("server");
private final Random random = new Random(0);
private OkHttpClient client = defaultClient().newBuilder()
.writeTimeout(500, TimeUnit.MILLISECONDS)
.readTimeout(500, TimeUnit.MILLISECONDS)
.addInterceptor(new Interceptor() {
@Override public Response intercept(Chain chain) throws IOException {
Response response = chain.proceed(chain.request());
assertNotNull(response.body()); // Ensure application interceptors never see a null body.
return response;
}
})
.build();
@After public void tearDown() {
clientListener.assertExhausted();
}
@Test public void textMessage() throws IOException {
webServer.enqueue(new MockResponse().withWebSocketUpgrade(serverListener));
WebSocket webSocket = newWebSocket();
clientListener.assertOpen();
serverListener.assertOpen();
webSocket.send("Hello, WebSockets!");
serverListener.assertTextMessage("Hello, WebSockets!");
}
@Test public void binaryMessage() throws IOException {
webServer.enqueue(new MockResponse().withWebSocketUpgrade(serverListener));
WebSocket webSocket = newWebSocket();
clientListener.assertOpen();
serverListener.assertOpen();
webSocket.send(ByteString.encodeUtf8("Hello!"));
serverListener.assertBinaryMessage(ByteString.of(new byte[] {'H', 'e', 'l', 'l', 'o', '!'}));
}
@Test public void nullStringThrows() throws IOException {
webServer.enqueue(new MockResponse().withWebSocketUpgrade(serverListener));
WebSocket webSocket = newWebSocket();
clientListener.assertOpen();
try {
webSocket.send((String) null);
fail();
} catch (NullPointerException e) {
assertEquals("text == null", e.getMessage());
}
}
@Test public void nullByteStringThrows() throws IOException {
webServer.enqueue(new MockResponse().withWebSocketUpgrade(serverListener));
WebSocket webSocket = newWebSocket();
clientListener.assertOpen();
try {
webSocket.send((ByteString) null);
fail();
} catch (NullPointerException e) {
assertEquals("bytes == null", e.getMessage());
}
}
@Test public void serverMessage() throws IOException {
webServer.enqueue(new MockResponse().withWebSocketUpgrade(serverListener));
newWebSocket();
clientListener.assertOpen();
WebSocket server = serverListener.assertOpen();
server.send("Hello, WebSockets!");
clientListener.assertTextMessage("Hello, WebSockets!");
}
@Test public void throwingOnOpenFailsImmediately() {
webServer.enqueue(new MockResponse().withWebSocketUpgrade(serverListener));
final RuntimeException e = new RuntimeException();
clientListener.setNextEventDelegate(new WebSocketListener() {
@Override public void onOpen(WebSocket webSocket, Response response) {
throw e;
}
});
newWebSocket();
serverListener.assertOpen();
serverListener.assertExhausted();
clientListener.assertFailure(e);
}
@Ignore("AsyncCall currently lets runtime exceptions propagate.")
@Test public void throwingOnFailLogs() throws InterruptedException {
TestLogHandler logs = new TestLogHandler();
Logger logger = Logger.getLogger(OkHttpClient.class.getName());
logger.addHandler(logs);
webServer.enqueue(new MockResponse().setResponseCode(200).setBody("Body"));
final RuntimeException e = new RuntimeException();
clientListener.setNextEventDelegate(new WebSocketListener() {
@Override public void onFailure(WebSocket webSocket, Throwable t, Response response) {
throw e;
}
});
newWebSocket();
assertEquals("", logs.take());
logger.removeHandler(logs);
}
@Test public void throwingOnMessageClosesImmediatelyAndFails() throws IOException {
webServer.enqueue(new MockResponse().withWebSocketUpgrade(serverListener));
newWebSocket();
clientListener.assertOpen();
WebSocket server = serverListener.assertOpen();
final RuntimeException e = new RuntimeException();
clientListener.setNextEventDelegate(new WebSocketListener() {
@Override public void onMessage(WebSocket webSocket, String text) {
throw e;
}
});
server.send("Hello, WebSockets!");
clientListener.assertFailure(e);
serverListener.assertFailure(EOFException.class);
serverListener.assertExhausted();
}
@Test public void throwingOnClosingClosesImmediatelyAndFails() throws IOException {
webServer.enqueue(new MockResponse().withWebSocketUpgrade(serverListener));
newWebSocket();
clientListener.assertOpen();
WebSocket server = serverListener.assertOpen();
final RuntimeException e = new RuntimeException();
clientListener.setNextEventDelegate(new WebSocketListener() {
@Override public void onClosing(WebSocket webSocket, int code, String reason) {
throw e;
}
});
server.close(1000, "bye");
clientListener.assertFailure(e);
serverListener.assertExhausted();
}
@Test public void non101RetainsBody() throws IOException {
webServer.enqueue(new MockResponse().setResponseCode(200).setBody("Body"));
newWebSocket();
clientListener.assertFailure(200, "Body", ProtocolException.class,
"Expected HTTP 101 response but was '200 OK'");
}
@Test public void notFound() throws IOException {
webServer.enqueue(new MockResponse().setStatus("HTTP/1.1 404 Not Found"));
newWebSocket();
clientListener.assertFailure(404, null, ProtocolException.class,
"Expected HTTP 101 response but was '404 Not Found'");
}
@Test public void clientTimeoutClosesBody() throws IOException {
webServer.enqueue(new MockResponse().setResponseCode(408));
webServer.enqueue(new MockResponse().withWebSocketUpgrade(serverListener));
WebSocket webSocket = newWebSocket();
clientListener.assertOpen();
WebSocket server = serverListener.assertOpen();
webSocket.send("abc");
serverListener.assertTextMessage("abc");
server.send("def");
clientListener.assertTextMessage("def");
}
@Test public void missingConnectionHeader() throws IOException {
webServer.enqueue(new MockResponse()
.setResponseCode(101)
.setHeader("Upgrade", "websocket")
.setHeader("Sec-WebSocket-Accept", "ujmZX4KXZqjwy6vi1aQFH5p4Ygk="));
newWebSocket();
clientListener.assertFailure(101, null, ProtocolException.class,
"Expected 'Connection' header value 'Upgrade' but was 'null'");
}
@Test public void wrongConnectionHeader() throws IOException {
webServer.enqueue(new MockResponse()
.setResponseCode(101)
.setHeader("Upgrade", "websocket")
.setHeader("Connection", "Downgrade")
.setHeader("Sec-WebSocket-Accept", "ujmZX4KXZqjwy6vi1aQFH5p4Ygk="));
newWebSocket();
clientListener.assertFailure(101, null, ProtocolException.class,
"Expected 'Connection' header value 'Upgrade' but was 'Downgrade'");
}
@Test public void missingUpgradeHeader() throws IOException {
webServer.enqueue(new MockResponse()
.setResponseCode(101)
.setHeader("Connection", "Upgrade")
.setHeader("Sec-WebSocket-Accept", "ujmZX4KXZqjwy6vi1aQFH5p4Ygk="));
newWebSocket();
clientListener.assertFailure(101, null, ProtocolException.class,
"Expected 'Upgrade' header value 'websocket' but was 'null'");
}
@Test public void wrongUpgradeHeader() throws IOException {
webServer.enqueue(new MockResponse()
.setResponseCode(101)
.setHeader("Connection", "Upgrade")
.setHeader("Upgrade", "Pepsi")
.setHeader("Sec-WebSocket-Accept", "ujmZX4KXZqjwy6vi1aQFH5p4Ygk="));
newWebSocket();
clientListener.assertFailure(101, null, ProtocolException.class,
"Expected 'Upgrade' header value 'websocket' but was 'Pepsi'");
}
@Test public void missingMagicHeader() throws IOException {
webServer.enqueue(new MockResponse()
.setResponseCode(101)
.setHeader("Connection", "Upgrade")
.setHeader("Upgrade", "websocket"));
newWebSocket();
clientListener.assertFailure(101, null, ProtocolException.class,
"Expected 'Sec-WebSocket-Accept' header value 'ujmZX4KXZqjwy6vi1aQFH5p4Ygk=' but was 'null'");
}
@Test public void wrongMagicHeader() throws IOException {
webServer.enqueue(new MockResponse()
.setResponseCode(101)
.setHeader("Connection", "Upgrade")
.setHeader("Upgrade", "websocket")
.setHeader("Sec-WebSocket-Accept", "magic"));
newWebSocket();
clientListener.assertFailure(101, null, ProtocolException.class,
"Expected 'Sec-WebSocket-Accept' header value 'ujmZX4KXZqjwy6vi1aQFH5p4Ygk=' but was 'magic'");
}
@Test public void webSocketAndApplicationInterceptors() throws IOException {
final AtomicInteger interceptedCount = new AtomicInteger();
client = client.newBuilder()
.addInterceptor(new Interceptor() {
@Override public Response intercept(Chain chain) throws IOException {
assertNull(chain.request().body());
Response response = chain.proceed(chain.request());
assertEquals("Upgrade", response.header("Connection"));
assertTrue("", response.body().source().exhausted());
interceptedCount.incrementAndGet();
return response;
}
}).build();
webServer.enqueue(new MockResponse().withWebSocketUpgrade(serverListener));
WebSocket webSocket = newWebSocket();
clientListener.assertOpen();
assertEquals(1, interceptedCount.get());
webSocket.close(1000, null);
WebSocket server = serverListener.assertOpen();
server.close(1000, null);
}
@Test public void webSocketAndNetworkInterceptors() throws IOException {
client = client.newBuilder()
.addNetworkInterceptor(new Interceptor() {
@Override public Response intercept(Chain chain) throws IOException {
throw new AssertionError(); // Network interceptors don't execute.
}
}).build();
webServer.enqueue(new MockResponse().withWebSocketUpgrade(serverListener));
WebSocket webSocket = newWebSocket();
clientListener.assertOpen();
webSocket.close(1000, null);
WebSocket server = serverListener.assertOpen();
server.close(1000, null);
}
@Test public void overflowOutgoingQueue() throws IOException {
webServer.enqueue(new MockResponse().withWebSocketUpgrade(serverListener));
WebSocket webSocket = newWebSocket();
clientListener.assertOpen();
// Send messages until the client's outgoing buffer overflows!
ByteString message = ByteString.of(new byte[1024 * 1024]);
int messageCount = 0;
while (true) {
boolean success = webSocket.send(message);
if (!success) break;
messageCount++;
long queueSize = webSocket.queueSize();
assertTrue(queueSize >= 0 && queueSize <= messageCount * message.size());
assertTrue(messageCount < 32); // Expect to fail before enqueueing 32 MiB.
}
// Confirm all sent messages were received, followed by a client-initiated close.
WebSocket server = serverListener.assertOpen();
for (int i = 0; i < messageCount; i++) {
serverListener.assertBinaryMessage(message);
}
serverListener.assertClosing(1001, "");
// When the server acknowledges the close the connection shuts down gracefully.
server.close(1000, null);
clientListener.assertClosing(1000, "");
clientListener.assertClosed(1000, "");
serverListener.assertClosed(1001, "");
}
@Test public void closeReasonMaximumLength() throws IOException {
webServer.enqueue(new MockResponse().withWebSocketUpgrade(serverListener));
String clientReason = repeat('C', 123);
String serverReason = repeat('S', 123);
WebSocket webSocket = newWebSocket();
WebSocket server = serverListener.assertOpen();
clientListener.assertOpen();
webSocket.close(1000, clientReason);
serverListener.assertClosing(1000, clientReason);
server.close(1000, serverReason);
clientListener.assertClosing(1000, serverReason);
clientListener.assertClosed(1000, serverReason);
serverListener.assertClosed(1000, clientReason);
}
@Test public void closeReasonTooLong() throws IOException {
webServer.enqueue(new MockResponse().withWebSocketUpgrade(serverListener));
WebSocket webSocket = newWebSocket();
WebSocket server = serverListener.assertOpen();
clientListener.assertOpen();
String reason = repeat('X', 124);
try {
webSocket.close(1000, reason);
fail();
} catch (IllegalArgumentException expected) {
assertEquals("reason.size() > 123: " + reason, expected.getMessage());
}
webSocket.close(1000, null);
serverListener.assertClosing(1000, "");
server.close(1000, null);
clientListener.assertClosing(1000, "");
clientListener.assertClosed(1000, "");
serverListener.assertClosed(1000, "");
}
@Test public void wsScheme() throws IOException {
websocketScheme("ws");
}
@Test public void wsUppercaseScheme() throws IOException {
websocketScheme("WS");
}
@Test public void wssScheme() throws IOException {
webServer.useHttps(sslClient.socketFactory, false);
client = client.newBuilder()
.sslSocketFactory(sslClient.socketFactory, sslClient.trustManager)
.hostnameVerifier(new RecordingHostnameVerifier())
.build();
websocketScheme("wss");
}
@Test public void httpsScheme() throws IOException {
webServer.useHttps(sslClient.socketFactory, false);
client = client.newBuilder()
.sslSocketFactory(sslClient.socketFactory, sslClient.trustManager)
.hostnameVerifier(new RecordingHostnameVerifier())
.build();
websocketScheme("https");
}
@Test public void readTimeoutAppliesToHttpRequest() throws IOException {
webServer.enqueue(new MockResponse()
.setSocketPolicy(SocketPolicy.NO_RESPONSE));
WebSocket webSocket = newWebSocket();
clientListener.assertFailure(SocketTimeoutException.class, "timeout", "Read timed out");
assertFalse(webSocket.close(1000, null));
}
/**
* There's no read timeout when reading the first byte of a new frame. But as soon as we start
* reading a frame we enable the read timeout. In this test we have the server returning the first
* byte of a frame but no more frames.
*/
@Test public void readTimeoutAppliesWithinFrames() throws IOException {
webServer.setDispatcher(new Dispatcher() {
@Override public MockResponse dispatch(RecordedRequest request) throws InterruptedException {
return upgradeResponse(request)
.setBody(new Buffer().write(ByteString.decodeHex("81"))) // Truncated frame.
.removeHeader("Content-Length")
.setSocketPolicy(SocketPolicy.KEEP_OPEN);
}
});
WebSocket webSocket = newWebSocket();
clientListener.assertOpen();
clientListener.assertFailure(SocketTimeoutException.class, "timeout", "Read timed out");
assertFalse(webSocket.close(1000, null));
}
@Test public void readTimeoutDoesNotApplyAcrossFrames() throws Exception {
webServer.enqueue(new MockResponse().withWebSocketUpgrade(serverListener));
newWebSocket();
clientListener.assertOpen();
WebSocket server = serverListener.assertOpen();
// Sleep longer than the HTTP client's read timeout.
Thread.sleep(client.readTimeoutMillis() + 500);
server.send("abc");
clientListener.assertTextMessage("abc");
}
@Test public void clientPingsServerOnInterval() throws Exception {
client = client.newBuilder()
.pingInterval(500, TimeUnit.MILLISECONDS)
.build();
webServer.enqueue(new MockResponse().withWebSocketUpgrade(serverListener));
RealWebSocket webSocket = newWebSocket();
clientListener.assertOpen();
RealWebSocket server = (RealWebSocket) serverListener.assertOpen();
long startNanos = System.nanoTime();
while (webSocket.pongCount() < 3) {
Thread.sleep(50);
}
long elapsedUntilPong3 = System.nanoTime() - startNanos;
assertEquals(1500, TimeUnit.NANOSECONDS.toMillis(elapsedUntilPong3), 250d);
// The client pinged the server 3 times, and it has ponged back 3 times.
assertEquals(3, server.pingCount());
assertEquals(3, webSocket.pongCount());
// The server has never pinged the client.
assertEquals(0, server.pongCount());
assertEquals(0, webSocket.pingCount());
}
@Test public void clientDoesNotPingServerByDefault() throws Exception {
webServer.enqueue(new MockResponse().withWebSocketUpgrade(serverListener));
RealWebSocket webSocket = newWebSocket();
clientListener.assertOpen();
RealWebSocket server = (RealWebSocket) serverListener.assertOpen();
Thread.sleep(1000);
// No pings and no pongs.
assertEquals(0, server.pingCount());
assertEquals(0, webSocket.pongCount());
assertEquals(0, server.pongCount());
assertEquals(0, webSocket.pingCount());
}
/** https://github.com/square/okhttp/issues/2788 */
@Test public void clientCancelsIfCloseIsNotAcknowledged() throws Exception {
webServer.enqueue(new MockResponse().withWebSocketUpgrade(serverListener));
RealWebSocket webSocket = newWebSocket();
clientListener.assertOpen();
WebSocket server = serverListener.assertOpen();
// Initiate a close on the client, which will schedule a hard cancel in 500 ms.
long closeAtNanos = System.nanoTime();
webSocket.close(1000, "goodbye", 500);
serverListener.assertClosing(1000, "goodbye");
// Confirm that the hard cancel occurred after 500 ms.
clientListener.assertFailure();
long elapsedUntilFailure = System.nanoTime() - closeAtNanos;
assertEquals(500, TimeUnit.NANOSECONDS.toMillis(elapsedUntilFailure), 250d);
// Close the server and confirm it saw what we expected.
server.close(1000, null);
serverListener.assertClosed(1000, "goodbye");
}
private MockResponse upgradeResponse(RecordedRequest request) {
String key = request.getHeader("Sec-WebSocket-Key");
return new MockResponse()
.setStatus("HTTP/1.1 101 Switching Protocols")
.setHeader("Connection", "Upgrade")
.setHeader("Upgrade", "websocket")
.setHeader("Sec-WebSocket-Accept", WebSocketProtocol.acceptHeader(key));
}
private void websocketScheme(String scheme) throws IOException {
webServer.enqueue(new MockResponse().withWebSocketUpgrade(serverListener));
Request request = new Request.Builder()
.url(scheme + "://" + webServer.getHostName() + ":" + webServer.getPort() + "/")
.build();
RealWebSocket webSocket = newWebSocket(request);
clientListener.assertOpen();
serverListener.assertOpen();
webSocket.send("abc");
serverListener.assertTextMessage("abc");
}
private RealWebSocket newWebSocket() {
return newWebSocket(new Request.Builder().get().url(webServer.url("/")).build());
}
private RealWebSocket newWebSocket(Request request) {
RealWebSocket webSocket = new RealWebSocket(request, clientListener, random);
webSocket.connect(client);
return webSocket;
}
}