This commit is contained in:
candi
2025-09-04 04:43:31 +08:00
parent 2723b0ddd3
commit 212c19d40a
136 changed files with 96 additions and 766 deletions

View File

@@ -0,0 +1,95 @@
/*******************************************************************************
* Copyright (c) 2009, 2014 IBM Corp.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* and Eclipse Distribution License v1.0 which accompany this distribution.
*
* The Eclipse Public License is available at
* https://www.eclipse.org/legal/epl-2.0
* and the Eclipse Distribution License is available at
* https://www.eclipse.org/org/documents/edl-v10.php
*
* Contributors:
* James Sutton - Bug 459142 - WebSocket support for the Java client.
*/
package org.eclipse.paho.mqttv5.client.websocket;
import java.util.prefs.AbstractPreferences;
import java.util.prefs.BackingStoreException;
public class Base64 {
private static final Base64 instance = new Base64();
private static final Base64Encoder encoder = instance.new Base64Encoder();
public static String encode (String s){
encoder.putByteArray("akey", s.getBytes());
return encoder.getBase64String();
}
public static String encodeBytes (byte[] b){
encoder.putByteArray("aKey", b);
return encoder.getBase64String();
}
public class Base64Encoder extends AbstractPreferences {
private String base64String = null;
public Base64Encoder() {
super(null, "");
}
protected void putSpi(String key, String value) {
base64String = value;
}
public String getBase64String() {
return base64String;
}
protected String getSpi(String key) {
return null;
}
protected void removeSpi(String key) {
}
protected void removeNodeSpi() throws BackingStoreException {
}
protected String[] keysSpi() throws BackingStoreException {
return null;
}
protected String[] childrenNamesSpi() throws BackingStoreException {
return null;
}
protected AbstractPreferences childSpi(String name) {
return null;
}
protected void syncSpi() throws BackingStoreException {
}
protected void flushSpi() throws BackingStoreException {
}
}
}

View File

@@ -0,0 +1,47 @@
package org.eclipse.paho.mqttv5.client.websocket;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
class ExtendedByteArrayOutputStream extends ByteArrayOutputStream {
final WebSocketNetworkModule webSocketNetworkModule;
final WebSocketSecureNetworkModule webSocketSecureNetworkModule;
ExtendedByteArrayOutputStream(WebSocketNetworkModule module) {
this.webSocketNetworkModule = module;
this.webSocketSecureNetworkModule = null;
}
ExtendedByteArrayOutputStream(WebSocketSecureNetworkModule module) {
this.webSocketNetworkModule = null;
this.webSocketSecureNetworkModule = module;
}
public void flush() throws IOException {
final ByteBuffer byteBuffer;
synchronized (this) {
byteBuffer = ByteBuffer.wrap(toByteArray());
reset();
}
WebSocketFrame frame = new WebSocketFrame((byte)0x02, true, byteBuffer.array());
byte[] rawFrame = frame.encodeFrame();
getSocketOutputStream().write(rawFrame);
getSocketOutputStream().flush();
}
OutputStream getSocketOutputStream() throws IOException {
if(webSocketNetworkModule != null ){
return webSocketNetworkModule.getSocketOutputStream();
}
if(webSocketSecureNetworkModule != null){
return webSocketSecureNetworkModule.getSocketOutputStream();
}
return null;
}
}

View File

@@ -0,0 +1,22 @@
/*******************************************************************************
* Copyright (c) 2009, 2014 IBM Corp.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* and Eclipse Distribution License v1.0 which accompany this distribution.
*
* The Eclipse Public License is available at
* https://www.eclipse.org/legal/epl-2.0
* and the Eclipse Distribution License is available at
* https://www.eclipse.org/org/documents/edl-v10.php
*
* Contributors:
* James Sutton - Bug 459142 - WebSocket support for the Java client.
*/
package org.eclipse.paho.mqttv5.client.websocket;
public class HandshakeFailedException extends Exception {
private static final long serialVersionUID = 1L;
}

View File

@@ -0,0 +1,306 @@
/*******************************************************************************
* Copyright (c) 2009, 2014 IBM Corp.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* and Eclipse Distribution License v1.0 which accompany this distribution.
*
* The Eclipse Public License is available at
* https://www.eclipse.org/legal/epl-2.0
* and the Eclipse Distribution License is available at
* https://www.eclipse.org/org/documents/edl-v10.php
*
* Contributors:
* James Sutton - Bug 459142 - WebSocket support for the Java client.
*/
package org.eclipse.paho.mqttv5.client.websocket;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.security.SecureRandom;
public class WebSocketFrame {
public static final int frameLengthOverhead = 6;
private byte opcode;
private boolean fin;
private byte[] payload;
private boolean closeFlag = false;
public byte getOpcode() {
return opcode;
}
public boolean isFin() {
return fin;
}
public byte[] getPayload() {
return payload;
}
public boolean isCloseFlag() {
return closeFlag;
}
/**
* Initialise a new WebSocketFrame
* @param opcode WebSocket Opcode
* @param fin If it's final
* @param payload The payload
*/
public WebSocketFrame(byte opcode, boolean fin, byte[] payload){
this.opcode = opcode;
this.fin = fin;
this.payload = payload.clone();
}
/**
* Initialise WebSocketFrame from raw Data
* @param rawFrame The raw byte buffer
*/
public WebSocketFrame (byte[] rawFrame){
ByteBuffer buffer = ByteBuffer.wrap(rawFrame);
// First Byte: Fin, Reserved, Opcode
byte b = buffer.get();
setFinAndOpCode(b);
// Second Byte Masked & Initial Length
b = buffer.get();
boolean masked = ((b & 0x80) != 0);
int payloadLength = (byte)(0x7F & b);
int byteCount = 0;
if(payloadLength == 0X7F){
// 8 Byte Extended payload length
byteCount = 8;
} else if (payloadLength == 0X7E){
// 2 bytes extended payload length
byteCount = 2;
}
// Decode the extended payload length
while (--byteCount > 0){
b = buffer.get();
payloadLength |= (b & 0xFF) << (8 * byteCount);
}
// Get the Masking key if masked
byte[] maskingKey = null;
if(masked) {
maskingKey = new byte[4];
buffer.get(maskingKey,0,4);
}
this.payload = new byte[payloadLength];
buffer.get(this.payload,0,payloadLength);
// Demask payload if needed
if(masked)
{
for(int i = 0; i < this.payload.length; i++){
this.payload[i] ^= maskingKey[i % 4];
}
}
return;
}
/**
* Sets the frames Fin flag and opcode.
* @param incomingByte
*/
private void setFinAndOpCode(byte incomingByte){
this.fin = ((incomingByte & 0x80) !=0);
// Reserved bits, unused right now.
// boolean rsv1 = ((incomingByte & 0x40) != 0);
// boolean rsv2 = ((incomingByte & 0x20) != 0);
// boolean rsv3 = ((incomingByte & 0x10) != 0);
this.opcode = (byte)(incomingByte & 0x0F);
}
/**
* Takes an input stream and parses it into a Websocket frame.
* @param input The incoming {@link InputStream}
* @throws IOException if an exception occurs whilst reading the input stream
*/
public WebSocketFrame(InputStream input) throws IOException {
byte firstByte = (byte) input.read();
setFinAndOpCode(firstByte);
if(this.opcode == 2){
byte maskLengthByte = (byte) input.read();
boolean masked = ((maskLengthByte & 0x80) != 0);
int payloadLength = (byte)(0x7F & maskLengthByte);
int byteCount = 0;
if(payloadLength == 0X7F){
// 8 Byte Extended payload length
byteCount = 8;
} else if (payloadLength == 0X7E){
// 2 bytes extended payload length
byteCount = 2;
}
// Decode the payload length
if(byteCount > 0){
payloadLength = 0;
}
while (--byteCount >= 0){
maskLengthByte = (byte) input.read();
payloadLength |= (maskLengthByte & 0xFF) << (8 * byteCount);
}
// Get the masking key
byte[] maskingKey = null;
if(masked) {
maskingKey = new byte[4];
input.read(maskingKey,0,4);
}
this.payload = new byte[payloadLength];
int offsetIndex = 0;
int tempLength = payloadLength;
int bytesRead = 0;
while (offsetIndex != payloadLength){
bytesRead = input.read(this.payload,offsetIndex,tempLength);
offsetIndex += bytesRead;
tempLength -= bytesRead;
}
// Demask if needed
if(masked)
{
for(int i = 0; i < this.payload.length; i++){
this.payload[i] ^= maskingKey[i % 4];
}
}
return;
} else if(this.opcode == 8){
// Closing connection with server
closeFlag = true;
} else {
throw new IOException("Invalid Frame: Opcode: " +this.opcode);
}
}
/**
* Encodes the this WebSocketFrame into a byte array.
* @return byte array
*/
public byte[] encodeFrame(){
int length = this.payload.length + frameLengthOverhead;
// Calculating overhead
if(this.payload.length > 65535){
length += 8;
} else if(this.payload.length >= 126) {
length += 2;
}
ByteBuffer buffer = ByteBuffer.allocate(length);
appendFinAndOpCode(buffer, this.opcode, this.fin);
byte[] mask = generateMaskingKey();
appendLengthAndMask(buffer, this.payload.length, mask);
for(int i = 0; i < this.payload.length; i ++){
buffer.put((byte)(this.payload[i] ^=mask[i % 4]));
}
buffer.flip();
return buffer.array();
}
/**
* Appends the Length and Mask to the buffer
* @param buffer the outgoing {@link ByteBuffer}
* @param length the length of the frame
* @param mask The WebSocket Mask
*/
public static void appendLengthAndMask(ByteBuffer buffer, int length, byte[] mask){
if(mask != null){
appendLength(buffer, length, true);
buffer.put(mask);
} else {
appendLength(buffer, length, false);
}
}
/**
* Appends the Length of the payload to the buffer
* @param buffer
* @param length
* @param b
*/
private static void appendLength(ByteBuffer buffer, int length, boolean masked) {
if(length < 0){
throw new IllegalArgumentException("Length cannot be negative");
}
byte b = (masked?(byte)0x80:0x00);
if(length > 0xFFFF){
buffer.put((byte) (b | 0x7F));
buffer.put((byte)0x00);
buffer.put((byte)0x00);
buffer.put((byte)0x00);
buffer.put((byte)0x00);
buffer.put((byte)((length >> 24) & 0xFF));
buffer.put((byte)((length >> 16) & 0xFF));
buffer.put((byte)((length >> 8) & 0xFF));
buffer.put((byte)(length & 0xFF));
} else if(length >= 0x7E){
buffer.put((byte)(b | 0x7E));
buffer.put((byte)(length >> 8));
buffer.put((byte)(length & 0xFF));
} else {
buffer.put((byte)(b | length));
}
}
/**
* Appends the Fin flag and the OpCode
* @param buffer The outgoing buffer
* @param opcode The Websocket OpCode
* @param fin if this is a final frame
*/
public static void appendFinAndOpCode(ByteBuffer buffer, byte opcode, boolean fin){
byte b = 0x00;
// Add Fin flag
if(fin){
b |= 0x80;
}
//RSV 1,2,3 aren't important
// Add opcode
b |= opcode & 0x0F;
buffer.put(b);
}
/**
* Generates a random masking key
* Nothing super secure, but enough
* for websockets.
* @return ByteArray containing the key;
*/
public static byte[] generateMaskingKey(){
SecureRandom secureRandomGenerator = new SecureRandom();
int a = secureRandomGenerator.nextInt(255);
int b = secureRandomGenerator.nextInt(255);
int c = secureRandomGenerator.nextInt(255);
int d = secureRandomGenerator.nextInt(255);
return new byte[] {(byte) a,(byte) b,(byte) c,(byte) d};
}
}

View File

@@ -0,0 +1,230 @@
/*******************************************************************************
* Copyright (c) 2009, 2014 IBM Corp.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* and Eclipse Distribution License v1.0 which accompany this distribution.
*
* The Eclipse Public License is available at
* https://www.eclipse.org/legal/epl-2.0
* and the Eclipse Distribution License is available at
* https://www.eclipse.org/org/documents/edl-v10.php
*
* Contributors:
* James Sutton - Bug 459142 - WebSocket support for the Java client.
*/
package org.eclipse.paho.mqttv5.client.websocket;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* Helper class to execute a WebSocket Handshake.
*/
public class WebSocketHandshake {
// Do not change: https://tools.ietf.org/html/rfc6455#section-1.3
private static final String ACCEPT_SALT = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
private static final String SHA1_PROTOCOL = "SHA1";
private static final String HTTP_HEADER_SEC_WEBSOCKET_ACCEPT = "sec-websocket-accept";
private static final String HTTP_HEADER_UPGRADE = "upgrade";
private static final String HTTP_HEADER_UPGRADE_WEBSOCKET = "websocket";
private static final String EMPTY = "";
private static final String LINE_SEPARATOR = "\r\n";
private static final String HTTP_HEADER_CONNECTION = "connection";
private static final String HTTP_HEADER_CONNECTION_VALUE = "upgrade";
private static final String HTTP_HEADER_SEC_WEBSOCKET_PROTOCOL = "sec-websocket-protocol";
InputStream input;
OutputStream output;
String uri;
String host;
int port;
Map<String, String> customWebSocketHeaders;
public WebSocketHandshake(InputStream input, OutputStream output, String uri, String host, int port, Map<String, String> customWebSocketHeaders) {
this.input = input;
this.output = output;
this.uri = uri;
this.host = host;
this.port = port;
this.customWebSocketHeaders = customWebSocketHeaders;
}
/**
* Executes a Websocket Handshake. Will throw an IOException if the handshake
* fails
*
* @throws IOException
* thrown if an exception occurs during the handshake
*/
public void execute() throws IOException {
byte[] key = new byte[16];
System.arraycopy(UUID.randomUUID().toString().getBytes(), 0, key, 0, 16);
String b64Key = Base64.encodeBytes(key);
sendHandshakeRequest(b64Key);
receiveHandshakeResponse(b64Key);
}
/**
* Builds and sends the HTTP Header GET Request for the socket.
*
* @param key
* Base64 encoded key
* @throws IOException
*/
private void sendHandshakeRequest(String key) {
try {
String path = "/mqtt";
URI srvUri = new URI(uri);
if (srvUri.getRawPath() != null && !srvUri.getRawPath().isEmpty()) {
path = srvUri.getRawPath();
if (srvUri.getRawQuery() != null && !srvUri.getRawQuery().isEmpty()) {
path += "?" + srvUri.getRawQuery();
}
}
PrintWriter pw = new PrintWriter(output);
pw.print("GET " + path + " HTTP/1.1" + LINE_SEPARATOR);
if (port != 80 && port != 443) {
pw.print("Host: " + host + ":" + port + LINE_SEPARATOR);
} else {
pw.print("Host: " + host + LINE_SEPARATOR);
}
pw.print("Upgrade: websocket" + LINE_SEPARATOR);
pw.print("Connection: Upgrade" + LINE_SEPARATOR);
pw.print("Sec-WebSocket-Key: " + key + LINE_SEPARATOR);
pw.print("Sec-WebSocket-Protocol: mqtt" + LINE_SEPARATOR);
pw.print("Sec-WebSocket-Version: 13" + LINE_SEPARATOR);
if (customWebSocketHeaders != null) {
customWebSocketHeaders.entrySet().forEach(entry ->
pw.print(entry.getKey() + ": " + entry.getValue() + LINE_SEPARATOR)
);
}
String userInfo = srvUri.getUserInfo();
if (userInfo != null) {
pw.print("Authorization: Basic " + Base64.encode(userInfo) + LINE_SEPARATOR);
}
pw.print(LINE_SEPARATOR);
pw.flush();
} catch (URISyntaxException e) {
throw new IllegalStateException(e.getMessage());
}
}
/**
* Receives the Handshake response and verifies that it is valid.
*
* @param key
* Base64 encoded key
* @throws IOException
*/
private void receiveHandshakeResponse(String key) throws IOException {
BufferedReader in = new BufferedReader(new InputStreamReader(input));
ArrayList<String> responseLines = new ArrayList<>();
String line = in.readLine();
if (line == null) {
throw new IOException(
"WebSocket Response header: Invalid response from Server, It may not support WebSockets.");
}
while (!line.equals(EMPTY)) {
responseLines.add(line);
line = in.readLine();
}
Map<String, String> headerMap = getHeaders(responseLines);
String connectionHeader = headerMap.get(HTTP_HEADER_CONNECTION);
if (connectionHeader == null || connectionHeader.equalsIgnoreCase(HTTP_HEADER_CONNECTION_VALUE)) {
throw new IOException("WebSocket Response header: Incorrect connection header");
}
String upgradeHeader = headerMap.get(HTTP_HEADER_UPGRADE);
if (upgradeHeader == null || !upgradeHeader.toLowerCase().contains(HTTP_HEADER_UPGRADE_WEBSOCKET)) {
throw new IOException("WebSocket Response header: Incorrect upgrade.");
}
String secWebsocketProtocolHeader = headerMap.get(HTTP_HEADER_SEC_WEBSOCKET_PROTOCOL);
if (secWebsocketProtocolHeader == null) {
throw new IOException("WebSocket Response header: empty sec-websocket-protocol");
}
if (!headerMap.containsKey(HTTP_HEADER_SEC_WEBSOCKET_ACCEPT)) {
throw new IOException("WebSocket Response header: Missing Sec-WebSocket-Accept");
}
try {
verifyWebSocketKey(key, (String) headerMap.get(HTTP_HEADER_SEC_WEBSOCKET_ACCEPT));
} catch (NoSuchAlgorithmException e) {
throw new IOException(e.getMessage());
} catch (HandshakeFailedException e) {
throw new IOException("WebSocket Response header: Incorrect Sec-WebSocket-Key");
}
}
/**
* Returns a Hashmap of HTTP headers
*
* @param headers
* ArrayList<String> of headers
* @return A Hashmap<String, String> of the headers
*/
private Map<String, String> getHeaders(ArrayList<String> headers) {
Map<String, String> headerMap = new HashMap<>();
for (int i = 1; i < headers.size(); i++) {
String headerPre = headers.get(i);
String[] header = headerPre.split(":");
headerMap.put(header[0].toLowerCase(), header[1]);
}
return headerMap;
}
/**
* Verifies that the Accept key provided is correctly built from the original
* key sent.
*
* @param key
* @param accept
* @throws NoSuchAlgorithmException
* @throws HandshakeFailedException
*/
private void verifyWebSocketKey(String key, String accept)
throws NoSuchAlgorithmException, HandshakeFailedException {
// We build up the accept in the same way the server should
// then we check that the response is the same.
byte[] sha1Bytes = sha1(key + ACCEPT_SALT);
String encodedSha1Bytes = Base64.encodeBytes(sha1Bytes).trim();
if (!encodedSha1Bytes.equals(accept.trim())) {
throw new HandshakeFailedException();
}
}
/**
* Returns the sha1 byte array of the provided string.
*
* @param input
* @return
* @throws NoSuchAlgorithmException
*/
private byte[] sha1(String input) throws NoSuchAlgorithmException {
MessageDigest mDigest = MessageDigest.getInstance(SHA1_PROTOCOL);
return mDigest.digest(input.getBytes());
}
}

View File

@@ -0,0 +1,111 @@
/*******************************************************************************
* Copyright (c) 2009, 2014 IBM Corp.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* and Eclipse Distribution License v1.0 which accompany this distribution.
*
* The Eclipse Public License is available at
* https://www.eclipse.org/legal/epl-2.0
* and the Eclipse Distribution License is available at
* https://www.eclipse.org/org/documents/edl-v10.php
*
* Contributors:
* James Sutton - Bug 459142 - WebSocket support for the Java client.
*/
package org.eclipse.paho.mqttv5.client.websocket;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PipedInputStream;
import java.nio.ByteBuffer;
import java.util.Collections;
import java.util.Map;
import javax.net.SocketFactory;
import org.eclipse.paho.mqttv5.client.internal.TCPNetworkModule;
import org.eclipse.paho.mqttv5.client.logging.Logger;
import org.eclipse.paho.mqttv5.client.logging.LoggerFactory;
import org.eclipse.paho.mqttv5.common.MqttException;
public class WebSocketNetworkModule extends TCPNetworkModule {
private static final String CLASS_NAME = WebSocketNetworkModule.class.getName();
private Logger log = LoggerFactory.getLogger(LoggerFactory.MQTT_CLIENT_MSG_CAT, CLASS_NAME);
private String uri;
private String host;
private int port;
private PipedInputStream pipedInputStream;
private WebSocketReceiver webSocketReceiver;
ByteBuffer recievedPayload;
Map<String, String> customWebSocketHeaders;
/**
* Overrides the flush method.
* This allows us to encode the MQTT payload into a WebSocket
* Frame before passing it through to the real socket.
*/
private ByteArrayOutputStream outputStream = new ExtendedByteArrayOutputStream(this);
public WebSocketNetworkModule(SocketFactory factory, String uri, String host, int port, String resourceContext){
super(factory, host, port, resourceContext);
this.uri = uri;
this.host = host;
this.port = port;
this.pipedInputStream = new PipedInputStream();
log.setResourceName(resourceContext);
}
public void start() throws IOException, MqttException {
super.start();
WebSocketHandshake handshake = new WebSocketHandshake(getSocketInputStream(), getSocketOutputStream(), uri, host, port, customWebSocketHeaders);
handshake.execute();
this.webSocketReceiver = new WebSocketReceiver(getSocketInputStream(), pipedInputStream);
webSocketReceiver.start("webSocketReceiver");
}
OutputStream getSocketOutputStream() throws IOException {
return super.getOutputStream();
}
InputStream getSocketInputStream() throws IOException {
return super.getInputStream();
}
public InputStream getInputStream() throws IOException {
return pipedInputStream;
}
public OutputStream getOutputStream() throws IOException {
return outputStream;
}
public void setCustomWebSocketHeaders(Map<String, String> customWebSocketHeaders) {
this.customWebSocketHeaders = customWebSocketHeaders;
}
/**
* Stops the module, by closing the TCP socket.
*/
public void stop() throws IOException {
// Creating Close Frame
WebSocketFrame frame = new WebSocketFrame((byte)0x08, true, "1000".getBytes());
byte[] rawFrame = frame.encodeFrame();
getSocketOutputStream().write(rawFrame);
getSocketOutputStream().flush();
if(webSocketReceiver != null){
webSocketReceiver.stop();
}
super.stop();
}
public String getServerURI() {
return "ws://" + host + ":" + port;
}
}

View File

@@ -0,0 +1,61 @@
/*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* and Eclipse Distribution License v1.0 which accompany this distribution.
*
* The Eclipse Public License is available at
* https://www.eclipse.org/legal/epl-2.0
* and the Eclipse Distribution License is available at
* https://www.eclipse.org/org/documents/edl-v10.php
*/
package org.eclipse.paho.mqttv5.client.websocket;
import java.net.URI;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import org.eclipse.paho.mqttv5.client.MqttClientException;
import org.eclipse.paho.mqttv5.client.MqttConnectionOptions;
import org.eclipse.paho.mqttv5.client.internal.NetworkModule;
import org.eclipse.paho.mqttv5.client.spi.NetworkModuleFactory;
import org.eclipse.paho.mqttv5.common.ExceptionHelper;
import org.eclipse.paho.mqttv5.common.MqttException;
public class WebSocketNetworkModuleFactory implements NetworkModuleFactory {
@Override
public Set<String> getSupportedUriSchemes() {
return Collections.unmodifiableSet(new HashSet<>(Arrays.asList("ws")));
}
@Override
public void validateURI(URI brokerUri) throws IllegalArgumentException {
// so specific requirements so far
}
@Override
public NetworkModule createNetworkModule(URI brokerUri, MqttConnectionOptions options, String clientId)
throws MqttException
{
String host = brokerUri.getHost();
int port = brokerUri.getPort(); // -1 if not defined
if (port == -1) {
port = 80;
}
SocketFactory factory = options.getSocketFactory();
if (factory == null) {
factory = SocketFactory.getDefault();
} else if (factory instanceof SSLSocketFactory) {
throw ExceptionHelper.createMqttException(MqttClientException.REASON_CODE_SOCKET_FACTORY_MISMATCH);
}
WebSocketNetworkModule netModule = new WebSocketNetworkModule(factory, brokerUri.toString(), host, port,
clientId);
netModule.setConnectTimeout(options.getConnectionTimeout());
netModule.setCustomWebSocketHeaders(options.getCustomWebSocketHeaders());
return netModule;
}
}

View File

@@ -0,0 +1,149 @@
/*******************************************************************************
* Copyright (c) 2009, 2014 IBM Corp.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* and Eclipse Distribution License v1.0 which accompany this distribution.
*
* The Eclipse Public License is available at
* https://www.eclipse.org/legal/epl-2.0
* and the Eclipse Distribution License is available at
* https://www.eclipse.org/org/documents/edl-v10.php
*
* Contributors:
* James Sutton - Bug 459142 - WebSocket support for the Java client.
*/
package org.eclipse.paho.mqttv5.client.websocket;
import java.io.IOException;
import java.io.InputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.net.SocketTimeoutException;
import org.eclipse.paho.mqttv5.client.logging.Logger;
import org.eclipse.paho.mqttv5.client.logging.LoggerFactory;
public class WebSocketReceiver implements Runnable{
private static final String CLASS_NAME = WebSocketReceiver.class.getName();
private Logger log = LoggerFactory.getLogger(LoggerFactory.MQTT_CLIENT_MSG_CAT, CLASS_NAME);
private boolean running = false;
private boolean stopping = false;
private final Object lifecycle = new Object();
private InputStream input;
private Thread receiverThread = null;
private volatile boolean receiving;
private PipedOutputStream pipedOutputStream;
public WebSocketReceiver(InputStream input, PipedInputStream pipedInputStream) throws IOException{
this.input = input;
this.pipedOutputStream = new PipedOutputStream();
pipedInputStream.connect(pipedOutputStream);
}
/**
* Starts up the WebSocketReceiver's thread
* @param threadName The name of the thread
*/
public void start(String threadName){
final String methodName = "start";
//@TRACE 855=starting
log.fine(CLASS_NAME, methodName, "855");
synchronized (lifecycle) {
if(!running) {
running = true;
receiverThread = new Thread(this, threadName);
receiverThread.start();
}
}
}
/**
* Stops this WebSocketReceiver's thread.
* This call will block.
*/
public void stop() {
final String methodName = "stop";
stopping = true;
boolean closed = false;
synchronized (lifecycle) {
//@TRACE 850=stopping
log.fine(CLASS_NAME,methodName, "850");
if(running) {
running = false;
receiving = false;
closed = true;
closeOutputStream();
}
}
if(closed && !Thread.currentThread().equals(receiverThread) && (receiverThread != null)) {
try {
// Wait for the thread to finish
//This must not happen in the synchronized block, otherwise we can deadlock ourselves!
receiverThread.join();
} catch (InterruptedException ex) {
// Interrupted Exception
}
}
receiverThread = null;
//@TRACE 851=stopped
log.fine(CLASS_NAME, methodName, "851");
}
public void run() {
final String methodName = "run";
while (running && (input != null)) {
try {
//@TRACE 852=network read message
log.fine(CLASS_NAME, methodName, "852");
receiving = input.available() > 0;
WebSocketFrame incomingFrame = new WebSocketFrame(input);
if(!incomingFrame.isCloseFlag()){
for(int i = 0; i < incomingFrame.getPayload().length; i++){
pipedOutputStream.write(incomingFrame.getPayload()[i]);
}
pipedOutputStream.flush();
} else {
if(!stopping){
throw new IOException("Server sent a WebSocket Frame with the Stop OpCode");
}
}
receiving = false;
} catch (SocketTimeoutException ex) {
// Ignore SocketTimeoutException
} catch (IOException ex) {
// Exception occurred whilst reading the stream.
this.stop();
}
}
}
private void closeOutputStream(){
try {
pipedOutputStream.close();
} catch (IOException e) {
}
}
public boolean isRunning() {
return running;
}
/**
* Returns the receiving state.
*
* @return true if the receiver is receiving data, false otherwise.
*/
public boolean isReceiving(){
return receiving;
}
}

View File

@@ -0,0 +1,111 @@
/*******************************************************************************
* Copyright (c) 2009, 2014 IBM Corp.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* and Eclipse Distribution License v1.0 which accompany this distribution.
*
* The Eclipse Public License is available at
* https://www.eclipse.org/legal/epl-2.0
* and the Eclipse Distribution License is available at
* https://www.eclipse.org/org/documents/edl-v10.php
*
* Contributors:
* James Sutton - Bug 459142 - WebSocket support for the Java client.
*/
package org.eclipse.paho.mqttv5.client.websocket;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PipedInputStream;
import java.nio.ByteBuffer;
import java.util.Map;
import javax.net.ssl.SSLSocketFactory;
import org.eclipse.paho.mqttv5.client.internal.SSLNetworkModule;
import org.eclipse.paho.mqttv5.client.logging.Logger;
import org.eclipse.paho.mqttv5.client.logging.LoggerFactory;
import org.eclipse.paho.mqttv5.common.MqttException;
public class WebSocketSecureNetworkModule extends SSLNetworkModule{
private static final String CLASS_NAME = WebSocketSecureNetworkModule.class.getName();
private Logger log = LoggerFactory.getLogger(LoggerFactory.MQTT_CLIENT_MSG_CAT, CLASS_NAME);
private PipedInputStream pipedInputStream;
private WebSocketReceiver webSocketReceiver;
private String uri;
private String host;
private int port;
ByteBuffer recievedPayload;
Map<String, String> customWebSocketHeaders;
/**
* Overrides the flush method.
* This allows us to encode the MQTT payload into a WebSocket
* Frame before passing it through to the real socket.
*/
private ByteArrayOutputStream outputStream = new ExtendedByteArrayOutputStream(this);
public WebSocketSecureNetworkModule(SSLSocketFactory factory, String uri, String host, int port, String clientId) {
super(factory, host, port, clientId);
this.uri = uri;
this.host = host;
this.port = port;
this.pipedInputStream = new PipedInputStream();
log.setResourceName(clientId);
}
public void start() throws IOException, MqttException {
super.start();
WebSocketHandshake handshake = new WebSocketHandshake(super.getInputStream(), super.getOutputStream(), uri, host, port, customWebSocketHeaders);
handshake.execute();
this.webSocketReceiver = new WebSocketReceiver(getSocketInputStream(), pipedInputStream);
webSocketReceiver.start("WssSocketReceiver");
}
OutputStream getSocketOutputStream() throws IOException {
return super.getOutputStream();
}
InputStream getSocketInputStream() throws IOException {
return super.getInputStream();
}
public InputStream getInputStream() throws IOException {
return pipedInputStream;
}
public OutputStream getOutputStream() throws IOException {
return outputStream;
}
public void setCustomWebSocketHeaders(Map<String, String> customWebSocketHeaders) {
this.customWebSocketHeaders = customWebSocketHeaders;
}
public void stop() throws IOException {
// Creating Close Frame
WebSocketFrame frame = new WebSocketFrame((byte)0x08, true, "1000".getBytes());
byte[] rawFrame = frame.encodeFrame();
getSocketOutputStream().write(rawFrame);
getSocketOutputStream().flush();
if(webSocketReceiver != null){
webSocketReceiver.stop();
}
super.stop();
}
public String getServerURI() {
return "wss://" + host + ":" + port;
}
}

View File

@@ -0,0 +1,82 @@
/*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* and Eclipse Distribution License v1.0 which accompany this distribution.
*
* The Eclipse Public License is available at
* https://www.eclipse.org/legal/epl-2.0
* and the Eclipse Distribution License is available at
* https://www.eclipse.org/org/documents/edl-v10.php
*/
package org.eclipse.paho.mqttv5.client.websocket;
import java.net.URI;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import org.eclipse.paho.mqttv5.client.MqttClientException;
import org.eclipse.paho.mqttv5.client.MqttConnectionOptions;
import org.eclipse.paho.mqttv5.client.internal.NetworkModule;
import org.eclipse.paho.mqttv5.client.internal.SSLNetworkModule;
import org.eclipse.paho.mqttv5.client.security.SSLSocketFactoryFactory;
import org.eclipse.paho.mqttv5.client.spi.NetworkModuleFactory;
import org.eclipse.paho.mqttv5.common.ExceptionHelper;
import org.eclipse.paho.mqttv5.common.MqttException;
public class WebSocketSecureNetworkModuleFactory implements NetworkModuleFactory {
@Override
public Set<String> getSupportedUriSchemes() {
return Collections.unmodifiableSet(new HashSet<>(Arrays.asList("wss")));
}
@Override
public void validateURI(URI brokerUri) throws IllegalArgumentException {
// so specific requirements so far
}
@Override
public NetworkModule createNetworkModule(URI brokerUri, MqttConnectionOptions options, String clientId)
throws MqttException
{
String host = brokerUri.getHost();
int port = brokerUri.getPort(); // -1 if not defined
if (port == -1) {
port = 443;
}
SocketFactory factory = options.getSocketFactory();
SSLSocketFactoryFactory wSSFactoryFactory = null;
if (factory == null) {
wSSFactoryFactory = new SSLSocketFactoryFactory();
Properties sslClientProps = options.getSSLProperties();
if (null != sslClientProps) {
wSSFactoryFactory.initialize(sslClientProps, null);
}
factory = wSSFactoryFactory.createSocketFactory(null);
} else if ((factory instanceof SSLSocketFactory) == false) {
throw ExceptionHelper.createMqttException(MqttClientException.REASON_CODE_SOCKET_FACTORY_MISMATCH);
}
// Create the network module...
WebSocketSecureNetworkModule netModule = new WebSocketSecureNetworkModule((SSLSocketFactory) factory,
brokerUri.toString(), host, port, clientId);
netModule.setSSLhandshakeTimeout(options.getConnectionTimeout());
netModule.setSSLHostnameVerifier(options.getSSLHostnameVerifier());
netModule.setHttpsHostnameVerificationEnabled(options.isHttpsHostnameVerificationEnabled());
netModule.setCustomWebSocketHeaders(options.getCustomWebSocketHeaders());
// Ciphers suites need to be set, if they are available
if (wSSFactoryFactory != null) {
String[] enabledCiphers = wSSFactoryFactory.getEnabledCipherSuites(null);
if (enabledCiphers != null) {
((SSLNetworkModule) netModule).setEnabledCiphers(enabledCiphers);
}
}
return netModule;
}
}