Skip to content

Commit 8a2d5c1

Browse files
committed
Improved support for WS/WSS connections.
1 parent c83c7b4 commit 8a2d5c1

File tree

3 files changed

+105
-28
lines changed

3 files changed

+105
-28
lines changed

build.gradle

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
11
group 'com.softwareverde'
2-
version '2.2.1'
2+
version '2.2.2'
33

44
apply plugin: 'java'
55
apply plugin: 'java-library'
66

7-
sourceCompatibility = 1.8
7+
java {
8+
sourceCompatibility = JavaVersion.VERSION_1_8
9+
targetCompatibility = JavaVersion.VERSION_1_8
10+
}
811

912
repositories {
1013
mavenCentral()
1114
maven { url "https://jitpack.io" }
1215
}
1316

1417
dependencies {
15-
api group: 'com.github.softwareverde', name: 'json', version: 'v2.0.0'
16-
implementation group: 'com.github.softwareverde', name: 'java-logging', version: 'v2.2.0'
17-
implementation group: 'com.github.softwareverde', name: 'java-util', version: 'v2.7.3'
18+
api group: 'com.github.softwareverde', name: 'json', version: 'v2.0.0'
19+
implementation group: 'com.github.softwareverde', name: 'java-logging', version: 'v2.2.0'
20+
implementation group: 'com.github.softwareverde', name: 'java-util', version: 'v2.7.3'
21+
implementation group: 'com.github.softwareverde', name: 'java-cryptography', version: 'v3.2.1'
1822

1923
testImplementation group: 'junit', name: 'junit', version: '4.12'
2024
}

src/main/java/com/softwareverde/http/HttpRequest.java

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22

33
import com.softwareverde.constable.bytearray.ByteArray;
44
import com.softwareverde.constable.bytearray.MutableByteArray;
5+
import com.softwareverde.cryptography.util.HashUtil;
56
import com.softwareverde.http.websocket.ConnectionLayer;
67
import com.softwareverde.http.websocket.WebSocket;
8+
import com.softwareverde.logging.Logger;
9+
import com.softwareverde.util.Base64Util;
710
import com.softwareverde.util.Container;
811
import com.softwareverde.util.StringUtil;
912
import com.softwareverde.util.Util;
@@ -21,6 +24,13 @@
2124
import java.util.concurrent.atomic.AtomicLong;
2225

2326
public class HttpRequest {
27+
static {
28+
// Allow for HttpUrlConnection to set non-trivial headers (required for WebSockets).
29+
System.setProperty("sun.net.http.allowRestrictedHeaders", "true");
30+
}
31+
32+
public static final String SEC_WEB_SOCKET_KEY = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
33+
2434
public interface Callback {
2535
void run(HttpResponse response);
2636
}
@@ -74,8 +84,28 @@ public static String getHeaderValue(final String key, final Map<String, List<Str
7484
return null;
7585
}
7686

77-
public static boolean containsUpgradeToWebSocketHeader(final Map<String, List<String>> headers) {
78-
return containsHeaderValue("upgrade", "websocket", headers);
87+
protected static String calculateSecWebSocketAcceptKey(final String wssKey) {
88+
// https://en.wikipedia.org/wiki/WebSocket#Protocol_handshake
89+
final byte[] preImage = StringUtil.stringToBytes(Util.coalesce(wssKey) + HttpRequest.SEC_WEB_SOCKET_KEY);
90+
final byte[] acceptKey = HashUtil.sha1(preImage);
91+
return Base64Util.toBase64String(acceptKey);
92+
}
93+
94+
public static boolean containsUpgradeToWebSocketHeader(final Map<String, List<String>> headers, final String wssKey) {
95+
if (HttpRequest.containsHeaderValue("upgrade", "websocket", headers)) { return true; }
96+
final String returnedWssKeyString = HttpRequest.getHeaderValue("sec-websocket-accept", headers);
97+
if ( (wssKey != null) && (returnedWssKeyString == null) ) { return false; }
98+
if (returnedWssKeyString != null) {
99+
if (wssKey == null) { return true; }
100+
101+
final String expectedWebSocketAcceptKey = HttpRequest.calculateSecWebSocketAcceptKey(wssKey);
102+
final Boolean keysAreEqual = Util.areEqual(expectedWebSocketAcceptKey, returnedWssKeyString);
103+
if (! keysAreEqual) {
104+
Logger.debug("sec-websocket-accept: " + expectedWebSocketAcceptKey + " != " + returnedWssKeyString);
105+
}
106+
return keysAreEqual;
107+
}
108+
return false;
79109
}
80110

81111
protected String _url;

src/main/java/com/softwareverde/http/HttpRequestExecutionThread.java

Lines changed: 64 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.softwareverde.constable.bytearray.ByteArray;
44
import com.softwareverde.constable.bytearray.MutableByteArray;
55
import com.softwareverde.logging.Logger;
6+
import com.softwareverde.util.Base64Util;
67
import com.softwareverde.util.IoUtil;
78
import com.softwareverde.util.ReflectionUtil;
89
import com.softwareverde.util.Util;
@@ -26,6 +27,7 @@ class HttpRequestExecutionThread extends Thread {
2627
protected HttpRequest.Callback _callback;
2728
protected final Integer _redirectCount;
2829
protected HttpURLConnection _connection;
30+
protected String _origin = null;
2931

3032
protected Socket _extractConnectionSocket() {
3133
Object httpConnectionHolder = null;
@@ -34,28 +36,53 @@ protected Socket _extractConnectionSocket() {
3436
final Object httpClient = ReflectionUtil.getValue(httpConnectionHolder, "http");
3537
return ReflectionUtil.getValue(httpClient, "serverSocket");
3638
}
37-
catch (final Exception exception1) {
39+
catch (final Exception exception) {
3840
if (httpConnectionHolder == null) {
39-
throw new RuntimeException("Unable to obtain connection socket via reflection", exception1);
41+
throw new RuntimeException("Unable to obtain connection socket via reflection.", exception);
4042
}
43+
4144
try {
42-
// unable to get standard http server socket, check for OkHttp implementation
45+
// Unable to get standard http server socket, check for OkHttp implementation.
4346
final Object httpEngine = ReflectionUtil.getValue(httpConnectionHolder, "httpEngine");
4447
final Object streamAllocation = ReflectionUtil.getValue(httpEngine, "streamAllocation");
4548
final Object realConnection = ReflectionUtil.getValue(streamAllocation, "connection");
46-
return (Socket) ReflectionUtil.getValue(realConnection, "socket");
49+
return ReflectionUtil.getValue(realConnection, "socket");
4750
}
4851
catch (final Exception exception2) {
49-
Logger.debug("Unable to get connection socket (1/2)", exception1);
50-
Logger.debug("Unable to get connection socket (2/2)", exception2);
51-
throw new RuntimeException("Unable to obtain connection socket via reflection");
52+
exception2.addSuppressed(exception);
53+
throw new RuntimeException("Unable to obtain connection socket via reflection.", exception2);
5254
}
5355
}
5456
}
5557

56-
protected void _configureRequestForWebSocketUpgrade() {
58+
protected String _configureRequestForWebSocketUpgrade(final Boolean isSecureWebSocket) {
59+
final SecureRandom secureRandom = new SecureRandom();
60+
final byte[] key = new byte[16];
61+
secureRandom.nextBytes(key);
62+
5763
_httpRequest.setAllowWebSocketUpgrade(true);
5864
_httpRequest.setHeader("Upgrade", "websocket");
65+
_httpRequest.setHeader("Connection", "upgrade");
66+
67+
if (isSecureWebSocket) {
68+
final String wssKey = Base64Util.toBase64String(key);
69+
_httpRequest.setHeader("Sec-WebSocket-Version", "13");
70+
_httpRequest.setHeader("Sec-WebSocket-Key", wssKey);
71+
_httpRequest.setHeader("Sec-WebSocket-Extensions", "permessage-deflate; client_max_window_bits");
72+
return wssKey;
73+
}
74+
75+
return null;
76+
}
77+
78+
protected ByteArray _readErrorStream(final InputStream inputStream) throws Exception {
79+
if (inputStream == null) { return null; }
80+
81+
// Only attempt to read from the stream if bytes are immediately available without blocking...
82+
// The inputStream type is HttpInputStream which appears to honor InputStream::available().
83+
if (inputStream.available() < 1) { return null; }
84+
85+
return MutableByteArray.wrap(IoUtil.readStreamOrThrow(inputStream));
5986
}
6087

6188
public HttpRequestExecutionThread(final String httpRequestUrl, final HttpRequest httpRequest, final HttpRequest.Callback callback, final Integer redirectCount) {
@@ -65,15 +92,26 @@ public HttpRequestExecutionThread(final String httpRequestUrl, final HttpRequest
6592
_redirectCount = redirectCount;
6693
}
6794

95+
public void setOrigin(final String origin) {
96+
_origin = origin;
97+
}
98+
6899
public void run() {
69100
try {
101+
final String wssKey;
70102
final String urlString;
71103
{
72-
final boolean isWebSocketRequest = _httpRequestUrl.startsWith("ws://") || _httpRequestUrl.startsWith("wss://");
73-
String requestUrl = _httpRequestUrl;
74-
if (isWebSocketRequest) {
75-
requestUrl = requestUrl.replaceFirst("ws", "http");
76-
_configureRequestForWebSocketUpgrade();
104+
final boolean isSecureWebSocketRequest = _httpRequestUrl.startsWith("wss://");
105+
final boolean isWebSocketRequest = _httpRequestUrl.startsWith("ws://");
106+
final String requestUrl;
107+
if (isWebSocketRequest || isSecureWebSocketRequest) {
108+
requestUrl = _httpRequestUrl.replaceFirst("ws", "http");
109+
final String generatedWssKey = _configureRequestForWebSocketUpgrade(isSecureWebSocketRequest);
110+
wssKey = (_httpRequest.validatesSslCertificates() ? generatedWssKey : null);
111+
}
112+
else {
113+
requestUrl = _httpRequestUrl;
114+
wssKey = null;
77115
}
78116
final String queryString = _httpRequest._queryString;
79117
if (! Util.isBlank(queryString)) {
@@ -86,7 +124,12 @@ public void run() {
86124

87125
final URL url = new URL(urlString);
88126

89-
_connection = (HttpURLConnection) (url.openConnection());
127+
if (_origin == null) {
128+
_origin = (url.getProtocol() + "://" + url.getHost());
129+
}
130+
_httpRequest.setHeader("Origin", _origin);
131+
132+
_connection = (HttpURLConnection) url.openConnection();
90133

91134
if (_connection instanceof HttpsURLConnection) {
92135
final HttpsURLConnection httpsConnection = ((HttpsURLConnection) _connection);
@@ -131,9 +174,9 @@ public void run() {
131174
if (postData != null) {
132175
_connection.setDoOutput(true);
133176

134-
try (final DataOutputStream out = new DataOutputStream(_connection.getOutputStream())) {
135-
out.write(postData.getBytes());
136-
out.flush();
177+
try (final DataOutputStream outputStream = new DataOutputStream(_connection.getOutputStream())) {
178+
outputStream.write(postData.getBytes());
179+
outputStream.flush();
137180
}
138181
}
139182
}
@@ -167,10 +210,10 @@ public void run() {
167210
}
168211
}
169212

170-
final boolean upgradeToWebSocket = (_httpRequest.allowsWebSocketUpgrade() && HttpRequest.containsUpgradeToWebSocketHeader(responseHeaders));
213+
final boolean upgradeToWebSocket = (_httpRequest.allowsWebSocketUpgrade() && HttpRequest.containsUpgradeToWebSocketHeader(responseHeaders, wssKey));
171214

172215
if (! upgradeToWebSocket) {
173-
if (responseCode >= 400) {
216+
if ( (responseCode >= 400) || (responseCode == 101) ) { // NOTE: Switching Protocols (101) when upgradeToWebSocket was not expected indicates a problem within the WebSocket handshake.
174217
InputStream errorStream = null;
175218
{ // Attempt to obtain the errorStream, but fallback to the inputStream if errorStream is unavailable.
176219
try {
@@ -185,11 +228,11 @@ public void run() {
185228
}
186229
}
187230

188-
httpResponse._rawResult = ((errorStream != null) ? MutableByteArray.wrap(IoUtil.readStreamOrThrow(errorStream)) : null);
231+
httpResponse._rawResult = _readErrorStream(errorStream);
189232
}
190233
else {
191234
final InputStream inputStream = _connection.getInputStream();
192-
httpResponse._rawResult = ((inputStream != null) ? MutableByteArray.wrap(IoUtil.readStreamOrThrow(inputStream)) : null);
235+
httpResponse._rawResult = (inputStream != null ? MutableByteArray.wrap(IoUtil.readStreamOrThrow(inputStream)) : null);
193236
}
194237

195238
// Close Connection

0 commit comments

Comments
 (0)