PageRenderTime 27ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/org.eclipse.paho.mqttv5.client/src/main/java/org/eclipse/paho/mqttv5/client/websocket/WebSocketHandshake.java

http://github.com/eclipse/paho.mqtt.java
Java | 230 lines | 142 code | 23 blank | 65 comment | 29 complexity | 28499d8ecf12f50e1c9c24f1b6c9d6b2 MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception
  1. /*******************************************************************************
  2. * Copyright (c) 2009, 2014 IBM Corp.
  3. *
  4. * All rights reserved. This program and the accompanying materials
  5. * are made available under the terms of the Eclipse Public License v2.0
  6. * and Eclipse Distribution License v1.0 which accompany this distribution.
  7. *
  8. * The Eclipse Public License is available at
  9. * https://www.eclipse.org/legal/epl-2.0
  10. * and the Eclipse Distribution License is available at
  11. * https://www.eclipse.org/org/documents/edl-v10.php
  12. *
  13. * Contributors:
  14. * James Sutton - Bug 459142 - WebSocket support for the Java client.
  15. */
  16. package org.eclipse.paho.mqttv5.client.websocket;
  17. import java.io.BufferedReader;
  18. import java.io.IOException;
  19. import java.io.InputStream;
  20. import java.io.InputStreamReader;
  21. import java.io.OutputStream;
  22. import java.io.PrintWriter;
  23. import java.net.URI;
  24. import java.net.URISyntaxException;
  25. import java.security.MessageDigest;
  26. import java.security.NoSuchAlgorithmException;
  27. import java.util.ArrayList;
  28. import java.util.HashMap;
  29. import java.util.Map;
  30. import java.util.UUID;
  31. /**
  32. * Helper class to execute a WebSocket Handshake.
  33. */
  34. public class WebSocketHandshake {
  35. // Do not change: https://tools.ietf.org/html/rfc6455#section-1.3
  36. private static final String ACCEPT_SALT = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
  37. private static final String SHA1_PROTOCOL = "SHA1";
  38. private static final String HTTP_HEADER_SEC_WEBSOCKET_ACCEPT = "sec-websocket-accept";
  39. private static final String HTTP_HEADER_UPGRADE = "upgrade";
  40. private static final String HTTP_HEADER_UPGRADE_WEBSOCKET = "websocket";
  41. private static final String EMPTY = "";
  42. private static final String LINE_SEPARATOR = "\r\n";
  43. private static final String HTTP_HEADER_CONNECTION = "connection";
  44. private static final String HTTP_HEADER_CONNECTION_VALUE = "upgrade";
  45. private static final String HTTP_HEADER_SEC_WEBSOCKET_PROTOCOL = "sec-websocket-protocol";
  46. InputStream input;
  47. OutputStream output;
  48. String uri;
  49. String host;
  50. int port;
  51. Map<String, String> customWebSocketHeaders;
  52. public WebSocketHandshake(InputStream input, OutputStream output, String uri, String host, int port, Map<String, String> customWebSocketHeaders) {
  53. this.input = input;
  54. this.output = output;
  55. this.uri = uri;
  56. this.host = host;
  57. this.port = port;
  58. this.customWebSocketHeaders = customWebSocketHeaders;
  59. }
  60. /**
  61. * Executes a Websocket Handshake. Will throw an IOException if the handshake
  62. * fails
  63. *
  64. * @throws IOException
  65. * thrown if an exception occurs during the handshake
  66. */
  67. public void execute() throws IOException {
  68. byte[] key = new byte[16];
  69. System.arraycopy(UUID.randomUUID().toString().getBytes(), 0, key, 0, 16);
  70. String b64Key = Base64.encodeBytes(key);
  71. sendHandshakeRequest(b64Key);
  72. receiveHandshakeResponse(b64Key);
  73. }
  74. /**
  75. * Builds and sends the HTTP Header GET Request for the socket.
  76. *
  77. * @param key
  78. * Base64 encoded key
  79. * @throws IOException
  80. */
  81. private void sendHandshakeRequest(String key) {
  82. try {
  83. String path = "/mqtt";
  84. URI srvUri = new URI(uri);
  85. if (srvUri.getRawPath() != null && !srvUri.getRawPath().isEmpty()) {
  86. path = srvUri.getRawPath();
  87. if (srvUri.getRawQuery() != null && !srvUri.getRawQuery().isEmpty()) {
  88. path += "?" + srvUri.getRawQuery();
  89. }
  90. }
  91. PrintWriter pw = new PrintWriter(output);
  92. pw.print("GET " + path + " HTTP/1.1" + LINE_SEPARATOR);
  93. if (port != 80 && port != 443) {
  94. pw.print("Host: " + host + ":" + port + LINE_SEPARATOR);
  95. } else {
  96. pw.print("Host: " + host + LINE_SEPARATOR);
  97. }
  98. pw.print("Upgrade: websocket" + LINE_SEPARATOR);
  99. pw.print("Connection: Upgrade" + LINE_SEPARATOR);
  100. pw.print("Sec-WebSocket-Key: " + key + LINE_SEPARATOR);
  101. pw.print("Sec-WebSocket-Protocol: mqtt" + LINE_SEPARATOR);
  102. pw.print("Sec-WebSocket-Version: 13" + LINE_SEPARATOR);
  103. if (customWebSocketHeaders != null) {
  104. customWebSocketHeaders.entrySet().forEach(entry ->
  105. pw.print(entry.getKey() + ": " + entry.getValue() + LINE_SEPARATOR)
  106. );
  107. }
  108. String userInfo = srvUri.getUserInfo();
  109. if (userInfo != null) {
  110. pw.print("Authorization: Basic " + Base64.encode(userInfo) + LINE_SEPARATOR);
  111. }
  112. pw.print(LINE_SEPARATOR);
  113. pw.flush();
  114. } catch (URISyntaxException e) {
  115. throw new IllegalStateException(e.getMessage());
  116. }
  117. }
  118. /**
  119. * Receives the Handshake response and verifies that it is valid.
  120. *
  121. * @param key
  122. * Base64 encoded key
  123. * @throws IOException
  124. */
  125. private void receiveHandshakeResponse(String key) throws IOException {
  126. BufferedReader in = new BufferedReader(new InputStreamReader(input));
  127. ArrayList<String> responseLines = new ArrayList<>();
  128. String line = in.readLine();
  129. if (line == null) {
  130. throw new IOException(
  131. "WebSocket Response header: Invalid response from Server, It may not support WebSockets.");
  132. }
  133. while (!line.equals(EMPTY)) {
  134. responseLines.add(line);
  135. line = in.readLine();
  136. }
  137. Map<String, String> headerMap = getHeaders(responseLines);
  138. String connectionHeader = headerMap.get(HTTP_HEADER_CONNECTION);
  139. if (connectionHeader == null || connectionHeader.equalsIgnoreCase(HTTP_HEADER_CONNECTION_VALUE)) {
  140. throw new IOException("WebSocket Response header: Incorrect connection header");
  141. }
  142. String upgradeHeader = headerMap.get(HTTP_HEADER_UPGRADE);
  143. if (upgradeHeader == null || !upgradeHeader.toLowerCase().contains(HTTP_HEADER_UPGRADE_WEBSOCKET)) {
  144. throw new IOException("WebSocket Response header: Incorrect upgrade.");
  145. }
  146. String secWebsocketProtocolHeader = headerMap.get(HTTP_HEADER_SEC_WEBSOCKET_PROTOCOL);
  147. if (secWebsocketProtocolHeader == null) {
  148. throw new IOException("WebSocket Response header: empty sec-websocket-protocol");
  149. }
  150. if (!headerMap.containsKey(HTTP_HEADER_SEC_WEBSOCKET_ACCEPT)) {
  151. throw new IOException("WebSocket Response header: Missing Sec-WebSocket-Accept");
  152. }
  153. try {
  154. verifyWebSocketKey(key, (String) headerMap.get(HTTP_HEADER_SEC_WEBSOCKET_ACCEPT));
  155. } catch (NoSuchAlgorithmException e) {
  156. throw new IOException(e.getMessage());
  157. } catch (HandshakeFailedException e) {
  158. throw new IOException("WebSocket Response header: Incorrect Sec-WebSocket-Key");
  159. }
  160. }
  161. /**
  162. * Returns a Hashmap of HTTP headers
  163. *
  164. * @param headers
  165. * ArrayList<String> of headers
  166. * @return A Hashmap<String, String> of the headers
  167. */
  168. private Map<String, String> getHeaders(ArrayList<String> headers) {
  169. Map<String, String> headerMap = new HashMap<>();
  170. for (int i = 1; i < headers.size(); i++) {
  171. String headerPre = headers.get(i);
  172. String[] header = headerPre.split(":");
  173. headerMap.put(header[0].toLowerCase(), header[1]);
  174. }
  175. return headerMap;
  176. }
  177. /**
  178. * Verifies that the Accept key provided is correctly built from the original
  179. * key sent.
  180. *
  181. * @param key
  182. * @param accept
  183. * @throws NoSuchAlgorithmException
  184. * @throws HandshakeFailedException
  185. */
  186. private void verifyWebSocketKey(String key, String accept)
  187. throws NoSuchAlgorithmException, HandshakeFailedException {
  188. // We build up the accept in the same way the server should
  189. // then we check that the response is the same.
  190. byte[] sha1Bytes = sha1(key + ACCEPT_SALT);
  191. String encodedSha1Bytes = Base64.encodeBytes(sha1Bytes).trim();
  192. if (!encodedSha1Bytes.equals(accept.trim())) {
  193. throw new HandshakeFailedException();
  194. }
  195. }
  196. /**
  197. * Returns the sha1 byte array of the provided string.
  198. *
  199. * @param input
  200. * @return
  201. * @throws NoSuchAlgorithmException
  202. */
  203. private byte[] sha1(String input) throws NoSuchAlgorithmException {
  204. MessageDigest mDigest = MessageDigest.getInstance(SHA1_PROTOCOL);
  205. return mDigest.digest(input.getBytes());
  206. }
  207. }