Package org.jwebsocket.kit

Source Code of org.jwebsocket.kit.WebSocketHandshake

//  ---------------------------------------------------------------------------
//  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.math.BigInteger;
import java.net.URI;
import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Map;

import javolution.util.FastMap;

/**
* Utility class for all the handshaking related request/response.
* @author aschulze
* @version $Id:$
*/
public final class WebSocketHandshake {

    public static int MAX_HEADER_SIZE = 16834;

    private String mKey1 = null;
    private String mKey2 = null;
    private byte[] mKey3 = null;
    private byte[] mExpectedServerResponse = null;

    private URI mURL = null;
    private String mOrigin = null;
    private String mProtocol = null;

    public WebSocketHandshake(URI aURL) {
        this(aURL, null);
    }

    public WebSocketHandshake(URI aURL, String aProtocol) {
        this.mURL = aURL;
        this.mProtocol = null;
        generateKeys();
    }

    /**
     * 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 aHost, String aPath) {
        // String lPath = aURI.getPath();
        // String lHost = aURI.getHost();
        String lOrigin = "http://" + aHost;
        String lHandshake = "GET " + aPath + " HTTP/1.1\r\n" + "Upgrade: WebSocket\r\n" + "Connection: Upgrade\r\n" + "Host: " + aHost + "\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) {
        StringBuilder lSB = new StringBuilder();
        // 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 aReq
     * @return
     */
    public static Map parseC2SRequest(byte[] aReq) {
        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 = null;
        Long lSecNum2 = null;
        byte[] lSecKeyResp = new byte[8];

        Map lRes = new FastMap();

        int lReqLen = aReq.length;
        String lRequest = "";
        try {
            lRequest = new String(aReq, "US-ASCII");
        } catch (Exception ex) {
            // TODO: add exception handling
        }

        if (lRequest.indexOf("policy-file-request") >= 0) { // "<policy-file-request/>"
            lRes.put("policy-file-request", lRequest);
            return lRes;
        }

        lIsSecure = (lRequest.indexOf("Sec-WebSocket") > 0);

        if (lIsSecure) {
            lReqLen -= 8;
            for (int i = 0; i < 8; i++) {
                lSecKey3[i] = aReq[lReqLen + i];
            }
        }

        // now parse header for correct handshake....
        // get host....
        int lPos = lRequest.indexOf("Host:");
        lPos += 6;
        lHost = lRequest.substring(lPos);
        lPos = lHost.indexOf("\r\n");
        lHost = lHost.substring(0, lPos);
        // get origin....
        lPos = lRequest.indexOf("Origin:");
        lPos += 8;
        lOrigin = lRequest.substring(lPos);
        lPos = lOrigin.indexOf("\r\n");
        lOrigin = lOrigin.substring(0, lPos);
        // get path....
        lPos = lRequest.indexOf("GET");
        lPos += 4;
        lPath = lRequest.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 = lRequest.indexOf("Sec-WebSocket-Key1:");
        if (lPos > 0) {
            lPos += 20;
            lSecKey1 = lRequest.substring(lPos);
            lPos = lSecKey1.indexOf("\r\n");
            lSecKey1 = lSecKey1.substring(0, lPos);
            lSecNum1 = calcSecKeyNum(lSecKey1);
            // log.debug("Sec-WebSocket-Key1:" + secKey1 + " => " + secNum1);
        }

        lPos = lRequest.indexOf("Sec-WebSocket-Key2:");
        if (lPos > 0) {
            lPos += 20;
            lSecKey2 = lRequest.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 != null && lSecNum2 != null) {

            // log.debug("Sec-WebSocket-Key3:" + new String(secKey3, "UTF-8"));
            BigInteger sec1 = new BigInteger(lSecNum1.toString());
            BigInteger sec2 = new BigInteger(lSecNum2.toString());

            // concatenate 3 parts secNum1 + secNum2 + secKey (16 Bytes)
            byte[] l128Bit = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
            byte[] lTmp;
            int lOfs;

            lTmp = sec1.toByteArray();
      int lIdx = lTmp.length;
      int lCnt = 0;
      while(lIdx > 0 && lCnt < 4) {
        lIdx--;
        lCnt++;
                l128Bit[4 - lCnt] = lTmp[lIdx];
            }

            lTmp = sec2.toByteArray();
      lIdx = lTmp.length;
      lCnt = 0;
      while(lIdx > 0 && lCnt < 4) {
        lIdx--;
        lCnt++;
                l128Bit[8 - lCnt] = lTmp[lIdx];
            }

            lTmp = lSecKey3;
      System.arraycopy(lSecKey3, 0, l128Bit, 8, 8);
/*
            // 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");
                lSecKeyResp = md.digest(l128Bit);
            } 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", 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(Map 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");
        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 Map parseS2CResponse(byte[] aResp) {
        Map lRes = new FastMap();
        String lResp = null;
        try {
            lResp = new String(aResp, "US-ASCII");
        } catch (Exception ex) {
            // TODO: add exception handling
        }
        return lRes;
    }

    public byte[] getHandshake() {
        String path = mURL.getPath();
        String host = mURL.getHost();
        mOrigin = "http://" + host;
        if ("".equals(path)) {
            path = "/";
        }
        String handshake = "GET " + path + " HTTP/1.1\r\n" + "Host: " + host + "\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Key2: " + mKey2 + "\r\n";

        if (mProtocol != null) {
            handshake += "Sec-WebSocket-Protocol: " + mProtocol + "\r\n";
        }

        handshake += "Upgrade: WebSocket\r\n" + "Sec-WebSocket-Key1: " + mKey1 + "\r\n" + "Origin: " + mOrigin + "\r\n" + "\r\n";

        byte[] handshakeBytes = new byte[handshake.getBytes().length + 8];
        System.arraycopy(handshake.getBytes(), 0, handshakeBytes, 0, handshake.getBytes().length);
        System.arraycopy(mKey3, 0, handshakeBytes, handshake.getBytes().length, 8);

        return handshakeBytes;
    }

    public void verifyServerResponse(byte[] bytes) throws WebSocketException {
        if (!Arrays.equals(bytes, mExpectedServerResponse)) {
            throw new WebSocketException("not a WebSocket Server");
        }
    }

    public void verifyServerStatusLine(String statusLine) throws WebSocketException {
        int statusCode = Integer.valueOf(statusLine.substring(9, 12));

        if (statusCode == 407) {
            throw new WebSocketException("connection failed: proxy authentication not supported");
        } else if (statusCode == 404) {
            throw new WebSocketException("connection failed: 404 not found");
        } else if (statusCode != 101) {
            throw new WebSocketException("connection failed: unknown status code " + statusCode);
        }
    }

    public void verifyServerHandshakeHeaders(Map<String, String> headers) throws WebSocketException {
        if (!headers.get("Upgrade").equals("WebSocket")) {
            throw new WebSocketException("connection failed: missing header field in server handshake: Upgrade");
        } else if (!headers.get("Connection").equals("Upgrade")) {
            throw new WebSocketException("connection failed: missing header field in server handshake: Connection");
        } else if (!headers.get("Sec-WebSocket-Origin").equals(mOrigin)) {
            throw new WebSocketException("connection failed: missing header field in server handshake: Sec-WebSocket-Origin");
        }
    }

    private void generateKeys() {

        int spaces1 = rand(1, 12);
        int spaces2 = rand(1, 12);

        int max1 = Integer.MAX_VALUE / spaces1;
        int max2 = Integer.MAX_VALUE / spaces2;

        int number1 = rand(0, max1);
        int number2 = rand(0, max2);

        int product1 = number1 * spaces1;
        int product2 = number2 * spaces2;

        mKey1 = Integer.toString(product1);
        mKey2 = Integer.toString(product2);

        mKey1 = insertRandomCharacters(mKey1);
        mKey2 = insertRandomCharacters(mKey2);

        mKey1 = insertSpaces(mKey1, spaces1);
        mKey2 = insertSpaces(mKey2, spaces2);

        mKey3 = createRandomBytes();

        ByteBuffer buffer = ByteBuffer.allocate(4);
        buffer.putInt(number1);
        byte[] number1Array = buffer.array();
        buffer = ByteBuffer.allocate(4);
        buffer.putInt(number2);
        byte[] number2Array = buffer.array();

        byte[] challenge = new byte[16];
        System.arraycopy(number1Array, 0, challenge, 0, 4);
        System.arraycopy(number2Array, 0, challenge, 4, 4);
        System.arraycopy(mKey3, 0, challenge, 8, 8);

        mExpectedServerResponse = md5(challenge);
    }

    private String insertRandomCharacters(String key) {
        int count = rand(1, 12);

        char[] randomChars = new char[count];
        int randCount = 0;
        while (randCount < count) {
            int rand = (int) (Math.random() * 0x7e + 0x21);
            if (((0x21 < rand) && (rand < 0x2f)) || ((0x3a < rand) && (rand < 0x7e))) {
                randomChars[randCount] = (char) rand;
                randCount += 1;
            }
        }

        for (int i = 0; i < count; i++) {
            int split = rand(0, key.length());
            String part1 = key.substring(0, split);
            String part2 = key.substring(split);
            key = part1 + randomChars[i] + part2;
        }

        return key;
    }

    private String insertSpaces(String key, int spaces) {
        for (int i = 0; i < spaces; i++) {
            int split = rand(0, key.length());
            String part1 = key.substring(0, split);
            String part2 = key.substring(split);
            key = part1 + " " + part2;
        }
        return key;
    }

    private byte[] createRandomBytes() {
        byte[] bytes = new byte[8];

        for (int i = 0; i < 8; i++) {
            bytes[i] = (byte) rand(0, 255);
        }
        return bytes;
    }

    private byte[] md5(byte[] bytes) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            return md.digest(bytes);
        } catch (NoSuchAlgorithmException e) {
            return null;
        }
    }

    private int rand(int min, int max) {
        int rand = (int) (Math.random() * max + min);
        return rand;
    }
}
TOP

Related Classes of org.jwebsocket.kit.WebSocketHandshake

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.