| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. | |
| 2 // Use of this source code is governed by a BSD-style license that can be | |
| 3 // found in the LICENSE file. | |
| 4 | |
| 5 package org.chromium.sdk.internal.websocket; | |
| 6 | |
| 7 import java.io.ByteArrayOutputStream; | |
| 8 import java.io.IOException; | |
| 9 import java.io.InputStream; | |
| 10 import java.io.OutputStream; | |
| 11 import java.io.OutputStreamWriter; | |
| 12 import java.io.Writer; | |
| 13 import java.net.InetSocketAddress; | |
| 14 import java.security.MessageDigest; | |
| 15 import java.security.NoSuchAlgorithmException; | |
| 16 import java.util.ArrayList; | |
| 17 import java.util.Arrays; | |
| 18 import java.util.Collections; | |
| 19 import java.util.HashSet; | |
| 20 import java.util.List; | |
| 21 import java.util.Map; | |
| 22 import java.util.Random; | |
| 23 import java.util.Set; | |
| 24 | |
| 25 import org.chromium.sdk.internal.transport.SocketWrapper; | |
| 26 | |
| 27 /** | |
| 28 * A more or less straightforward implementation of WebSocket client-side handsh
ake | |
| 29 * as defined in Internet-Draft of May 23, 2010. | |
| 30 * See http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-00 | |
| 31 * <p>Note that the standard was reworked completely. This implementation is obs
olete. However | |
| 32 * it is still compatible with the current Chrome implementation. | |
| 33 */ | |
| 34 class Hybi00Handshake { | |
| 35 static void performHandshake(SocketWrapper socket, InetSocketAddress endpoint, | |
| 36 String resourceName, String origin, Random random) throws IOException { | |
| 37 HandshakeUtil.checkOriginString(origin); | |
| 38 | |
| 39 OutputStream output = socket.getLoggableOutput().getOutputStream(); | |
| 40 Writer outputWriter = new OutputStreamWriter(output, HandshakeUtil.UTF_8_CHA
RSET); | |
| 41 | |
| 42 outputWriter.write("GET " + resourceName + " HTTP/1.1\r\n"); | |
| 43 | |
| 44 List<String> fields = HandshakeUtil.createHttpFields(endpoint); | |
| 45 fields.add("Upgrade: WebSocket"); | |
| 46 fields.add("Origin: " + origin); | |
| 47 int port = endpoint.getPort(); | |
| 48 String portSuffix = port == 80 ? "" : ":" + port; | |
| 49 fields.add("Host: " + endpoint.getHostName() + portSuffix); | |
| 50 WsKey key1 = new WsKey(random); | |
| 51 WsKey key2 = new WsKey(random); | |
| 52 fields.add("Sec-WebSocket-Key1: " + key1.getKeySocketField()); | |
| 53 fields.add("Sec-WebSocket-Key2: " + key2.getKeySocketField()); | |
| 54 | |
| 55 Collections.shuffle(fields, random); | |
| 56 for (String field : fields) { | |
| 57 outputWriter.write(field); | |
| 58 outputWriter.write("\r\n"); | |
| 59 } | |
| 60 outputWriter.write("\r\n"); | |
| 61 byte[] key3 = new byte[8]; | |
| 62 random.nextBytes(key3); | |
| 63 | |
| 64 outputWriter.flush(); | |
| 65 | |
| 66 output.write(key3); | |
| 67 output.flush(); | |
| 68 | |
| 69 | |
| 70 byte[] expectedMd5Bytes; | |
| 71 { | |
| 72 // Challenge. | |
| 73 ByteArrayOutputStream challengeBytes = new ByteArrayOutputStream(16); | |
| 74 writeIntBigEndian(key1.getNumber(), challengeBytes); | |
| 75 writeIntBigEndian(key2.getNumber(), challengeBytes); | |
| 76 challengeBytes.write(key3); | |
| 77 MessageDigest digest; | |
| 78 try { | |
| 79 digest = MessageDigest.getInstance("MD5"); | |
| 80 } catch (NoSuchAlgorithmException e) { | |
| 81 throw new RuntimeException(e); | |
| 82 } | |
| 83 expectedMd5Bytes = digest.digest(challengeBytes.toByteArray()); | |
| 84 } | |
| 85 | |
| 86 final InputStream input = socket.getLoggableInput().getInputStream(); | |
| 87 | |
| 88 HandshakeUtil.LineReader lineReader = HandshakeUtil.createLineReader(input); | |
| 89 | |
| 90 HandshakeUtil.HttpResponse httpResponse = HandshakeUtil.readHttpResponse(lin
eReader); | |
| 91 | |
| 92 if (httpResponse.getCode() != 101) { | |
| 93 throw new IOException("Unexpected response code " + httpResponse.getCode()
); | |
| 94 } | |
| 95 | |
| 96 Map<String, String> responseFields = httpResponse.getFields(); | |
| 97 | |
| 98 if (responseFields.size() != EXPECTED_FIELDS.size()) { | |
| 99 throw new IOException("Malformed response"); | |
| 100 } | |
| 101 if (!responseFields.keySet().containsAll(EXPECTED_FIELDS)) { | |
| 102 throw new IOException("Malformed response"); | |
| 103 } | |
| 104 if (!"WebSocket".equals(responseFields.get("upgrade"))) { | |
| 105 throw new IOException("Malformed response"); | |
| 106 } | |
| 107 if (!"upgrade".equalsIgnoreCase(responseFields.get("connection"))) { | |
| 108 throw new IOException("Malformed response"); | |
| 109 } | |
| 110 if (!origin.equals(responseFields.get("sec-websocket-origin"))) { | |
| 111 throw new IOException("Malformed response"); | |
| 112 } | |
| 113 String expectedUrl = createUrl(endpoint, resourceName, false); | |
| 114 if (!expectedUrl.equals(responseFields.get("sec-websocket-location"))) { | |
| 115 throw new IOException("Malformed response: unexpected sec-websocket-locati
on"); | |
| 116 } | |
| 117 | |
| 118 { | |
| 119 // Challenge response. | |
| 120 byte[] actualMd5Bytes = new byte[16]; | |
| 121 { | |
| 122 int readPos = 0; | |
| 123 while (readPos < actualMd5Bytes.length) { | |
| 124 int readRes = input.read(actualMd5Bytes, readPos, actualMd5Bytes.lengt
h - readPos); | |
| 125 if (readRes == -1) { | |
| 126 throw new IOException("End of stream"); | |
| 127 } | |
| 128 readPos += readRes; | |
| 129 } | |
| 130 } | |
| 131 if (!Arrays.equals(expectedMd5Bytes, actualMd5Bytes)) { | |
| 132 throw new IOException("Wrong challenge response: expected=" + | |
| 133 Arrays.toString(expectedMd5Bytes) + " recieved=" + Arrays.toString(a
ctualMd5Bytes)); | |
| 134 } | |
| 135 } | |
| 136 } | |
| 137 | |
| 138 private static String createUrl(InetSocketAddress endpoint, String resourceNam
e, | |
| 139 boolean secure) { | |
| 140 boolean needPort; | |
| 141 if (secure) { | |
| 142 needPort = endpoint.getPort() != 443; | |
| 143 } else { | |
| 144 needPort = endpoint.getPort() != 80; | |
| 145 } | |
| 146 return (secure ? "wss://" : "ws://") + | |
| 147 endpoint.getHostName() + | |
| 148 (needPort ? ":" + endpoint.getPort() : "") + | |
| 149 resourceName; | |
| 150 } | |
| 151 | |
| 152 private static void writeIntBigEndian(long value, OutputStream output) throws
IOException { | |
| 153 output.write((byte) ((value & 0xFF000000L) >> (3 * 8))); | |
| 154 output.write((byte) ((value & 0xFF0000L) >> (2 * 8))); | |
| 155 output.write((byte) ((value & 0xFF00L) >> (1 * 8))); | |
| 156 output.write((byte) ((value & 0xFFL))); | |
| 157 } | |
| 158 | |
| 159 private static final Set<String> EXPECTED_FIELDS = new HashSet<String>(Arrays.
asList( | |
| 160 "upgrade", | |
| 161 "connection", | |
| 162 "sec-websocket-origin", | |
| 163 "sec-websocket-location" | |
| 164 )); | |
| 165 | |
| 166 private static class WsKey { | |
| 167 private static final long SPEC_MAX = 4294967295l; | |
| 168 | |
| 169 private final long resNumber; | |
| 170 private final String keyString; | |
| 171 | |
| 172 WsKey(Random random) { | |
| 173 int spaces = random.nextInt(12) + 1; | |
| 174 long max = SPEC_MAX / spaces; | |
| 175 long number = Math.abs(random.nextLong()) % (max + 1); | |
| 176 resNumber = number; | |
| 177 long product = number * spaces; | |
| 178 assert(product <= SPEC_MAX); | |
| 179 | |
| 180 String productStr = Long.toString(product); | |
| 181 List<Byte> keyBytes = new ArrayList<Byte>(40); | |
| 182 keyBytes.addAll(Collections.nCopies(productStr.length(), (byte) '1')); | |
| 183 int stuffByteNumber = random.nextInt(12) + 1; | |
| 184 for (int i = 0; i < stuffByteNumber; i++) { | |
| 185 keyBytes.add(StuffBytes.getByte(random)); | |
| 186 } | |
| 187 Collections.shuffle(keyBytes, random); | |
| 188 keyBytes.subList(0, keyBytes.size() - 1).addAll(Collections.nCopies(spaces
, (byte) ' ')); | |
| 189 Collections.shuffle(keyBytes.subList(1, keyBytes.size() - 1), random); | |
| 190 byte[] resultBytes = new byte[keyBytes.size()]; | |
| 191 int strPos = 0; | |
| 192 for (int i = 0; i < resultBytes.length; i++) { | |
| 193 byte b = keyBytes.get(i); | |
| 194 if (b == (byte) '1') { | |
| 195 b = (byte) productStr.charAt(strPos); | |
| 196 strPos++; | |
| 197 } | |
| 198 resultBytes[i] = b; | |
| 199 } | |
| 200 assert(strPos == productStr.length()); | |
| 201 keyString = new String(resultBytes, HandshakeUtil.ASCII_CHARSET); | |
| 202 } | |
| 203 | |
| 204 String getKeySocketField() { | |
| 205 return keyString; | |
| 206 } | |
| 207 | |
| 208 long getNumber() { | |
| 209 return resNumber; | |
| 210 } | |
| 211 | |
| 212 private static class StuffBytes { | |
| 213 private static byte RANGE_1_BEGIN = 0x21; | |
| 214 private static byte RANGE_1_END = 0x2F + 1; | |
| 215 private static byte RANGE_2_BEGIN = 0x3A; | |
| 216 private static byte RANGE_2_END = 0x7E + 1; | |
| 217 | |
| 218 private static int RANDOM_RANGE_1 = RANGE_1_END - RANGE_1_BEGIN; | |
| 219 private static int RANDOM_RANGE = RANDOM_RANGE_1 + RANGE_2_END - RANGE_2_B
EGIN; | |
| 220 | |
| 221 private static byte getByte(Random random) { | |
| 222 int i = random.nextInt(RANDOM_RANGE); | |
| 223 if (i < RANDOM_RANGE_1) { | |
| 224 return (byte) (i + RANGE_1_BEGIN); | |
| 225 } else { | |
| 226 return (byte) (i + - RANDOM_RANGE_1 + RANGE_2_BEGIN); | |
| 227 } | |
| 228 } | |
| 229 } | |
| 230 } | |
| 231 } | |
| OLD | NEW |