• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
WebSocketResponseHandler(IWebSocketFactory webSocketFactory)25     public WebSocketResponseHandler(IWebSocketFactory webSocketFactory) {
26         this.webSocketFactory = webSocketFactory;
27     }
28 
serve(final IHTTPSession session)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 
isWebsocketRequested(IHTTPSession session)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 
isWebSocketConnectionHeader(Map<String, String> headers)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 
makeAcceptKey(String key)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      */
encodeBase64(byte[] buf)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