// --------------------------------------------------------------------------- // jWebSocket - Copyright (c) 2010 jwebsocket.org // --------------------------------------------------------------------------- // This program is free software; you can redistribute it and/or modify it // under the terms of the GNU Lesser General Public License as published by the // Free Software Foundation; either version 3 of the License, or (at your // option) any later version. // This program is distributed in the hope that it will be useful, but WITHOUT // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or // FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for // more details. // You should have received a copy of the GNU Lesser General Public License along // with this program; if not, see <http://www.gnu.org/licenses/lgpl.html>. // --------------------------------------------------------------------------- package org.jwebsocket.kit; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; // import java.security.MessageDigest; import javolution.util.FastMap; /** * * @author aschulze */ public class WebSocketHandshake { public static int MAX_HEADER_SIZE = 16834; /** * Generates the initial handshake request from a client to the jWebSocket * Server. This is send from a Java client to the server when a connection * is about to be established. The browser's implement that internally. * @param aURI * @return */ // public static byte[] generateC2SRequest(URI aURI) { public static byte[] generateC2SRequest(String lHost, String lPath) { // String lPath = aURI.getPath(); // String lHost = aURI.getHost(); String lOrigin = "http://" + lHost; String lHandshake = "GET " + lPath + " HTTP/1.1\r\n" + "Upgrade: WebSocket\r\n" + "Connection: Upgrade\r\n" + "Host: " + lHost + "\r\n" + "Origin: " + lOrigin + "\r\n" + "\r\n"; byte[] lBA = null; try { lBA = lHandshake.getBytes("US-ASCII"); } catch (Exception ex) { } return lBA; } private static long calcSecKeyNum(String aKey) { StringBuffer lSB = new StringBuffer(); // StringBuuffer lSB = new StringBuuffer(); int lSpaces = 0; for (int i = 0; i < aKey.length(); i++) { char lC = aKey.charAt(i); if (lC == ' ') { lSpaces++; } else if (lC >= '0' && lC <= '9') { lSB.append(lC); } } long lRes = -1; if (lSpaces > 0) { try { lRes = Long.parseLong(lSB.toString()) / lSpaces; // log.debug("Key: " + aKey + ", Numbers: " + lSB.toString() + ", Spaces: " + lSpaces + ", Result: " + lRes); } catch (NumberFormatException ex) { // use default result } } return lRes; } /** * Parses the response from the client on an initial client's handshake * request. This is always performed on the server only when a client * - irrespective of if it is a Java Client or Browser Client - * initiates a connection. * @param aResp * @return */ public static FastMap parseC2SRequest(byte[] aResp) { String lHost = null; String lOrigin = null; String lLocation = null; String lPath = null; String lSecKey1 = null; String lSecKey2 = null; byte[] lSecKey3 = new byte[8]; boolean lIsSecure = false; long lSecNum1 = -1; long lSecNum2 = -1; byte[] lSecKeyResp = new byte[8]; FastMap lRes = new FastMap(); int lRespLen = aResp.length; String lResp = ""; try { lResp = new String(aResp, "US-ASCII"); } catch (Exception ex) { // TODO: add exception handling } if (lResp.indexOf("policy-file-request") >= 0) { // "<policy-file-request/>" lRes.put("policy-file-request", lResp); return lRes; } lIsSecure = (lResp.indexOf("Sec-WebSocket") > 0); if (lIsSecure) { lRespLen -= 8; for (int i = 0; i < 8; i++) { lSecKey3[i] = aResp[lRespLen + i]; } } // now parse header for correct handshake.... // get host.... int lPos = lResp.indexOf("Host:"); lPos += 6; lHost = lResp.substring(lPos); lPos = lHost.indexOf("\r\n"); lHost = lHost.substring(0, lPos); // get origin.... lPos = lResp.indexOf("Origin:"); lPos += 8; lOrigin = lResp.substring(lPos); lPos = lOrigin.indexOf("\r\n"); lOrigin = lOrigin.substring(0, lPos); // get path.... lPos = lResp.indexOf("GET"); lPos += 4; lPath = lResp.substring(lPos); lPos = lPath.indexOf("HTTP"); lPath = lPath.substring(0, lPos - 1); lLocation = "ws://" + lHost + lPath; // the following section implements the sec-key process in WebSocket Draft 76 /* To prove that the handshake was received, the server has to take three pieces of information and combine them to form a response. The first two pieces of information come from the |Sec-WebSocket-Key1| and |Sec-WebSocket-Key2| fields in the client handshake. Sec-WebSocket-Key1: 18x 6]8vM;54 *(5: { U1]8 z [ 8 Sec-WebSocket-Key2: 1_ tx7X d < nw 334J702) 7]o}` 0 For each of these fields, the server has to take the digits from the value to obtain a number (in this case 1868545188 and 1733470270 respectively), then divide that number by the number of spaces characters in the value (in this case 12 and 10) to obtain a 32-bit number (155712099 and 173347027). These two resulting numbers are then used in the server handshake, as described below. */ lPos = lResp.indexOf("Sec-WebSocket-Key1:"); if (lPos > 0) { lPos += 20; lSecKey1 = lResp.substring(lPos); lPos = lSecKey1.indexOf("\r\n"); lSecKey1 = lSecKey1.substring(0, lPos); lSecNum1 = calcSecKeyNum(lSecKey1); // log.debug("Sec-WebSocket-Key1:" + secKey1 + " => " + secNum1); } lPos = lResp.indexOf("Sec-WebSocket-Key2:"); if (lPos > 0) { lPos += 20; lSecKey2 = lResp.substring(lPos); lPos = lSecKey2.indexOf("\r\n"); lSecKey2 = lSecKey2.substring(0, lPos); lSecNum2 = calcSecKeyNum(lSecKey2); // log.debug("Sec-WebSocket-Key2:" + secKey2 + " => " + secNum2); } /* The third piece of information is given after the fields, in the last eight bytes of the handshake, expressed here as they would be seen if interpreted as ASCII: Tm[K T2u The concatenation of the number obtained from processing the |Sec- WebSocket-Key1| field, expressed as a big-endian 32 bit number, the number obtained from processing the |Sec-WebSocket-Key2| field, again expressed as a big-endian 32 bit number, and finally the eight bytes at the end of the handshake, form a 128 bit string whose MD5 sum is then used by the server to prove that it read the handshake. */ if (lSecNum1 != -1 && lSecNum2 != -1) { // log.debug("Sec-WebSocket-Key3:" + new String(secKey3, "UTF-8")); //BigInteger sec1 = new BigInteger(); //BigInteger sec2 = new BigInteger(lSecNum2.toString()); // concatenate 3 parts secNum1 + secNum2 + secKey byte[] l128Bit = new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; byte[] lTmp; long secTmp; secTmp = lSecNum1; // TODO: replace by arraycopy for (int i = 0; i < 4; i++) { l128Bit[i] = (byte)(secTmp & 0xff); secTmp >>= 8; } secTmp = lSecNum2; for (int i = 0; i < 4; i++) { l128Bit[i+4] = (byte)(secTmp & 0xff); } lTmp = lSecKey3; // TODO: replace by arraycopy for (int i = 0; i < 8; i++) { l128Bit[i + 8] = lTmp[i]; } // build md5 sum of this new 128 byte string try { // MessageDigest md = MessageDigest.getInstance("MD5"); // md.update(l128Bit, 0, 32); // md.digest(lSecKeyResp, 0, 32); } catch (Exception ex) { // log.error("getMD5: " + ex.getMessage()); } } lRes.put("path", lPath); lRes.put("host", lHost); lRes.put("origin", lOrigin); lRes.put("location", lLocation); lRes.put("secKey1", lSecKey1); lRes.put("secKey2", lSecKey2); lRes.put("isSecure", new Boolean(lIsSecure)); lRes.put("secKeyResponse", lSecKeyResp); return lRes; } /** * Generates the response for the server to answer an initial client * request. This is performed on the server only as an answer to a client's * request - irrespective of if it is a Java or Browser Client. * @param aRequest * @return */ public static byte[] generateS2CResponse(FastMap aRequest) { String lPolicyFileRequest = (String) aRequest.get("policy-file-request"); if (lPolicyFileRequest != null) { byte[] lBA; try { lBA = ("<cross-domain-policy>" + "<allow-access-from domain=\"*\" to-ports=\"*\" />" + "</cross-domain-policy>\n").getBytes("US-ASCII"); } catch (UnsupportedEncodingException ex) { lBA = null; } return lBA; } // now that we have parsed the header send handshake... // since 0.9.0.0609 considering Sec-WebSocket-Key processing boolean lIsSecure = ((Boolean) aRequest.get("isSecure")).booleanValue(); String lOrigin = (String) aRequest.get("origin"); String lLocation = (String) aRequest.get("location"); String lRes = // since IETF draft 76 "WebSocket Protocol" not "Web Socket Protocol" // change implemented since v0.9.5.0701 "HTTP/1.1 101 Web" + (lIsSecure ? "" : " ") + "Socket Protocol Handshake\r\n" + "Upgrade: WebSocket\r\n" + "Connection: Upgrade\r\n" + (lIsSecure ? "Sec-" : "") + "WebSocket-Origin: " + lOrigin + "\r\n" + (lIsSecure ? "Sec-" : "") + "WebSocket-Location: " + lLocation + "\r\n" + "\r\n"; byte[] lBA; try { lBA = lRes.getBytes("US-ASCII"); // if Sec-WebSocket-Keys are used send security response first if (lIsSecure) { byte[] lSecKey = (byte[]) aRequest.get("secKeyResponse"); byte[] lResult = new byte[lBA.length + lSecKey.length]; System.arraycopy(lBA, 0, lResult, 0, lBA.length); System.arraycopy(lSecKey, 0, lResult, lBA.length, lSecKey.length); return lResult; } else { return lBA; } } catch (UnsupportedEncodingException ex) { return null; } } /** * Reads the handshake response from the server into an byte array. * This is used on clients only. The browser client implement * that internally. * @param aIS * @return */ public static byte[] readS2CResponse(InputStream aIS) { byte[] lBuff = new byte[MAX_HEADER_SIZE]; boolean lContinue = true; int lIdx = 0; int lB1 = 0, lB2 = 0, lB3 = 0, lB4 = 0; while (lContinue && lIdx < MAX_HEADER_SIZE) { int b; try { b = aIS.read(); if (b < 0) { return null; } } catch (IOException ex) { return null; } // build mini queue to check for \r\n\r\n sequence in handshake lB1 = lB2; lB2 = lB3; lB3 = lB4; lB4 = b; lContinue = !(lB1 == 13 && lB2 == 10 && lB3 == 13 && lB4 == 10); lBuff[lIdx] = (byte) b; lIdx++; } byte[] lRes = new byte[lIdx]; System.arraycopy(lBuff, 0, lRes, 0, lIdx); return lRes; } /* * Parses the websocket handshake response from the server. * This is performed on Java Client only, the browsers implement * that internally. * @param aResp * @return */ public static FastMap parseS2CResponse(byte[] aResp) { FastMap lRes = new FastMap(); String lResp = null; try { lResp = new String(aResp, "US-ASCII"); } catch (Exception ex) { // TODO: add exception handling } return lRes; } }