/*
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.stetho.websocket;
import android.util.Base64;
import com.facebook.stetho.common.Utf8Charset;
import com.facebook.stetho.server.http.HttpHandler;
import com.facebook.stetho.server.http.HttpStatus;
import com.facebook.stetho.server.SocketLike;
import com.facebook.stetho.server.http.LightHttpBody;
import com.facebook.stetho.server.http.LightHttpMessage;
import com.facebook.stetho.server.http.LightHttpRequest;
import com.facebook.stetho.server.http.LightHttpResponse;
import com.facebook.stetho.server.http.LightHttpServer;
import javax.annotation.Nullable;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* Crazy kludge to support upgrading to the WebSocket protocol while still using the
* {@link HttpHandler} harness.
* <p>
* The way this works is that we pump the request directly into our WebSocket implementation and
* force write the response out to the connection without returning. Then, we extract the
* remaining buffered input stream bytes from the socket and stitch them together with the
* raw sockets input stream and pass everything onto the WebSocket engine which blocks
* until WebSocket orderly shutdown.
*/
public class WebSocketHandler implements HttpHandler {
private static final String HEADER_UPGRADE = "Upgrade";
private static final String HEADER_CONNECTION = "Connection";
private static final String HEADER_SEC_WEBSOCKET_KEY = "Sec-WebSocket-Key";
private static final String HEADER_SEC_WEBSOCKET_ACCEPT = "Sec-WebSocket-Accept";
private static final String HEADER_SEC_WEBSOCKET_PROTOCOL = "Sec-WebSocket-Protocol";
private static final String HEADER_SEC_WEBSOCKET_VERSION = "Sec-WebSocket-Version";
private static final String HEADER_UPGRADE_WEBSOCKET = "websocket";
private static final String HEADER_CONNECTION_UPGRADE = "Upgrade";
private static final String HEADER_SEC_WEBSOCKET_VERSION_13 = "13";
// Are you kidding me? The WebSocket spec requires that we append this weird hardcoded String
// to the key we receive from the client, SHA-1 that, and base64 encode it back to the client.
// I'm guessing this is to prevent replay attacks of some kind but given that there's no actual
// security context here, I can only imagine that this is just security through obscurity in
// some fashion.
private static final String SERVER_KEY_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
private final SimpleEndpoint mEndpoint;
public WebSocketHandler(SimpleEndpoint endpoint) {
mEndpoint = endpoint;
}
@Override
public boolean handleRequest(
SocketLike socket,
LightHttpRequest request,
LightHttpResponse response) throws IOException {
if (!isSupportableUpgradeRequest(request)) {
response.code = HttpStatus.HTTP_NOT_IMPLEMENTED;
response.reasonPhrase = "Not Implemented";
response.body = LightHttpBody.create(
"Not a supported WebSocket upgrade request\n",
"text/plain");
return true;
}
// This will not return on successful WebSocket upgrade, but rather block until the session is
// shut down or a socket error occurs.
doUpgrade(socket, request, response);
return false;
}
private static boolean isSupportableUpgradeRequest(LightHttpRequest request) {
return HEADER_UPGRADE_WEBSOCKET.equalsIgnoreCase(getFirstHeaderValue(request, HEADER_UPGRADE)) &&
HEADER_CONNECTION_UPGRADE.equals(getFirstHeaderValue(request, HEADER_CONNECTION)) &&
HEADER_SEC_WEBSOCKET_VERSION_13.equals(
getFirstHeaderValue(request, HEADER_SEC_WEBSOCKET_VERSION));
}
private void doUpgrade(
SocketLike socketLike,
LightHttpRequest request,
LightHttpResponse response)
throws IOException {
response.code = HttpStatus.HTTP_SWITCHING_PROTOCOLS;
response.reasonPhrase = "Switching Protocols";
response.addHeader(HEADER_UPGRADE, HEADER_UPGRADE_WEBSOCKET);
response.addHeader(HEADER_CONNECTION, HEADER_CONNECTION_UPGRADE);
response.body = null;
String clientKey = getFirstHeaderValue(request, HEADER_SEC_WEBSOCKET_KEY);
if (clientKey != null) {
response.addHeader(HEADER_SEC_WEBSOCKET_ACCEPT, generateServerKey(clientKey));
}
InputStream in = socketLike.getInput();
OutputStream out = socketLike.getOutput();
LightHttpServer.writeResponseMessage(
response,
new LightHttpServer.HttpMessageWriter(new BufferedOutputStream(out)));
WebSocketSession session = new WebSocketSession(in, out, mEndpoint);
session.handle();
}
private static String generateServerKey(String clientKey) {
try {
String serverKey = clientKey + SERVER_KEY_GUID;
MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
sha1.update(Utf8Charset.encodeUTF8(serverKey));
return Base64.encodeToString(sha1.digest(), Base64.NO_WRAP);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
@Nullable
private static String getFirstHeaderValue(LightHttpMessage message, String headerName) {
return message.getFirstHeaderValue(headerName);
}
}