Home | History | Annotate | Download | only in elonen
      1 package fi.iki.elonen;
      2 
      3 import java.security.MessageDigest;
      4 import java.security.NoSuchAlgorithmException;
      5 import java.util.Map;
      6 
      7 import fi.iki.elonen.NanoHTTPD.IHTTPSession;
      8 import fi.iki.elonen.NanoHTTPD.Response;
      9 
     10 public class WebSocketResponseHandler {
     11     public static final String HEADER_UPGRADE = "upgrade";
     12     public static final String HEADER_UPGRADE_VALUE = "websocket";
     13     public static final String HEADER_CONNECTION = "connection";
     14     public static final String HEADER_CONNECTION_VALUE = "Upgrade";
     15     public static final String HEADER_WEBSOCKET_VERSION = "sec-websocket-version";
     16     public static final String HEADER_WEBSOCKET_VERSION_VALUE = "13";
     17     public static final String HEADER_WEBSOCKET_KEY = "sec-websocket-key";
     18     public static final String HEADER_WEBSOCKET_ACCEPT = "sec-websocket-accept";
     19     public static final String HEADER_WEBSOCKET_PROTOCOL = "sec-websocket-protocol";
     20 
     21     public final static String WEBSOCKET_KEY_MAGIC = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
     22 
     23     private final IWebSocketFactory webSocketFactory;
     24 
     25     public WebSocketResponseHandler(IWebSocketFactory webSocketFactory) {
     26         this.webSocketFactory = webSocketFactory;
     27     }
     28 
     29     public Response serve(final IHTTPSession session) {
     30         Map<String, String> headers = session.getHeaders();
     31         if (isWebsocketRequested(session)) {
     32             if (!HEADER_WEBSOCKET_VERSION_VALUE.equalsIgnoreCase(headers.get(HEADER_WEBSOCKET_VERSION))) {
     33                 return new Response(Response.Status.BAD_REQUEST, NanoHTTPD.MIME_PLAINTEXT,
     34                         "Invalid Websocket-Version " + headers.get(HEADER_WEBSOCKET_VERSION));
     35             }
     36 
     37             if (!headers.containsKey(HEADER_WEBSOCKET_KEY)) {
     38                 return new Response(Response.Status.BAD_REQUEST, NanoHTTPD.MIME_PLAINTEXT,
     39                         "Missing Websocket-Key");
     40             }
     41 
     42             WebSocket webSocket = webSocketFactory.openWebSocket(session);
     43             Response handshakeResponse = webSocket.getHandshakeResponse();
     44             try {
     45                 handshakeResponse.addHeader(HEADER_WEBSOCKET_ACCEPT, makeAcceptKey(headers.get(HEADER_WEBSOCKET_KEY)));
     46             } catch (NoSuchAlgorithmException e) {
     47                 return new Response(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT,
     48                         "The SHA-1 Algorithm required for websockets is not available on the server.");
     49             }
     50 
     51             if (headers.containsKey(HEADER_WEBSOCKET_PROTOCOL)) {
     52                 handshakeResponse.addHeader(HEADER_WEBSOCKET_PROTOCOL, headers.get(HEADER_WEBSOCKET_PROTOCOL).split(",")[0]);
     53             }
     54 
     55             return handshakeResponse;
     56         } else {
     57             return null;
     58         }
     59     }
     60 
     61     protected boolean isWebsocketRequested(IHTTPSession session) {
     62         Map<String, String> headers = session.getHeaders();
     63         String upgrade = headers.get(HEADER_UPGRADE);
     64         boolean isCorrectConnection = isWebSocketConnectionHeader(headers);
     65         boolean isUpgrade = HEADER_UPGRADE_VALUE.equalsIgnoreCase(upgrade);
     66         return (isUpgrade && isCorrectConnection);
     67     }
     68 
     69     private boolean isWebSocketConnectionHeader(Map<String, String> headers) {
     70         String connection = headers.get(HEADER_CONNECTION);
     71         return (connection != null && connection.toLowerCase().contains(HEADER_CONNECTION_VALUE.toLowerCase()));
     72     }
     73 
     74     public static String makeAcceptKey(String key) throws NoSuchAlgorithmException {
     75         MessageDigest md = MessageDigest.getInstance("SHA-1");
     76         String text = key + WEBSOCKET_KEY_MAGIC;
     77         md.update(text.getBytes(), 0, text.length());
     78         byte[] sha1hash = md.digest();
     79         return encodeBase64(sha1hash);
     80     }
     81 
     82     private final static char[] ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray();
     83 
     84     /**
     85      * Translates the specified byte array into Base64 string.
     86      * <p>
     87      * Android has android.util.Base64, sun has sun.misc.Base64Encoder, Java 8 hast java.util.Base64,
     88      * I have this from stackoverflow: http://stackoverflow.com/a/4265472
     89      * </p>
     90      *
     91      * @param buf the byte array (not null)
     92      * @return the translated Base64 string (not null)
     93      */
     94     private static String encodeBase64(byte[] buf) {
     95         int size = buf.length;
     96         char[] ar = new char[((size + 2) / 3) * 4];
     97         int a = 0;
     98         int i = 0;
     99         while (i < size) {
    100             byte b0 = buf[i++];
    101             byte b1 = (i < size) ? buf[i++] : 0;
    102             byte b2 = (i < size) ? buf[i++] : 0;
    103 
    104             int mask = 0x3F;
    105             ar[a++] = ALPHABET[(b0 >> 2) & mask];
    106             ar[a++] = ALPHABET[((b0 << 4) | ((b1 & 0xFF) >> 4)) & mask];
    107             ar[a++] = ALPHABET[((b1 << 2) | ((b2 & 0xFF) >> 6)) & mask];
    108             ar[a++] = ALPHABET[b2 & mask];
    109         }
    110         switch (size % 3) {
    111             case 1:
    112                 ar[--a] = '=';
    113             case 2:
    114                 ar[--a] = '=';
    115         }
    116         return new String(ar);
    117     }
    118 }
    119