diff --git a/build.gradle b/build.gradle index 8cf9d7745..0eeb9d023 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,7 @@ plugins { id 'io.codearte.nexus-staging' version '0.21.1' + id 'io.freefair.lombok' version '5.0.1' apply false } repositories { diff --git a/dependencies.gradle b/dependencies.gradle index 6d1b9ea12..de4a7ee34 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -2,9 +2,10 @@ // in java/build.gradle and android/build.gradle for maven. dependencies { implementation 'org.msgpack:msgpack-core:0.8.11' - implementation 'org.java-websocket:Java-WebSocket:1.5.3' implementation 'com.google.code.gson:gson:2.9.0' implementation 'com.davidehrmann.vcdiff:vcdiff-core:0.1.1' + implementation project(':network-client-core') + implementation project(':network-client-default') testImplementation 'org.hamcrest:hamcrest-all:1.3' testImplementation 'junit:junit:4.12' testImplementation 'org.nanohttpd:nanohttpd:2.3.0' diff --git a/lib/src/main/java/io/ably/lib/debug/DebugOptions.java b/lib/src/main/java/io/ably/lib/debug/DebugOptions.java index 0aec7c196..984e73a5f 100644 --- a/lib/src/main/java/io/ably/lib/debug/DebugOptions.java +++ b/lib/src/main/java/io/ably/lib/debug/DebugOptions.java @@ -1,10 +1,10 @@ package io.ably.lib.debug; -import java.net.HttpURLConnection; import java.util.List; import java.util.Map; import io.ably.lib.http.HttpCore; +import io.ably.lib.network.HttpRequest; import io.ably.lib.transport.ITransport; import io.ably.lib.types.AblyException; import io.ably.lib.types.ClientOptions; @@ -19,7 +19,7 @@ public interface RawProtocolListener { } public interface RawHttpListener { - HttpCore.Response onRawHttpRequest(String id, HttpURLConnection conn, String method, String authHeader, Map> requestHeaders, HttpCore.RequestBody requestBody); + HttpCore.Response onRawHttpRequest(String id, HttpRequest request, String authHeader, Map> requestHeaders, HttpCore.RequestBody requestBody); void onRawHttpResponse(String id, String method, HttpCore.Response response); void onRawHttpException(String id, String method, Throwable t); } diff --git a/lib/src/main/java/io/ably/lib/http/HttpCore.java b/lib/src/main/java/io/ably/lib/http/HttpCore.java index 7b3bb64bb..b860b0d9b 100644 --- a/lib/src/main/java/io/ably/lib/http/HttpCore.java +++ b/lib/src/main/java/io/ably/lib/http/HttpCore.java @@ -2,7 +2,12 @@ import com.google.gson.JsonParseException; import io.ably.lib.debug.DebugOptions; -import io.ably.lib.debug.DebugOptions.RawHttpListener; +import io.ably.lib.network.HttpEngine; +import io.ably.lib.network.HttpEngineConfig; +import io.ably.lib.network.HttpEngineFactory; +import io.ably.lib.network.HttpRequest; +import io.ably.lib.network.HttpBody; +import io.ably.lib.network.HttpResponse; import io.ably.lib.rest.Auth; import io.ably.lib.transport.Defaults; import io.ably.lib.transport.Hosts; @@ -14,17 +19,13 @@ import io.ably.lib.types.ProxyOptions; import io.ably.lib.util.AgentHeaderCreator; import io.ably.lib.util.Base64Coder; +import io.ably.lib.util.ClientOptionsUtils; import io.ably.lib.util.Log; import io.ably.lib.util.PlatformAgentProvider; -import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; import java.lang.reflect.Field; import java.net.HttpURLConnection; -import java.net.InetSocketAddress; -import java.net.Proxy; import java.net.URL; import java.util.HashMap; import java.util.List; @@ -62,10 +63,9 @@ public class HttpCore { final ClientOptions options; final Hosts hosts; private final Auth auth; - private final ProxyOptions proxyOptions; private final PlatformAgentProvider platformAgentProvider; private HttpAuth proxyAuth; - private Proxy proxy = Proxy.NO_PROXY; + private final HttpEngine engine; /************************* * Public API @@ -78,8 +78,7 @@ public HttpCore(ClientOptions options, Auth auth, PlatformAgentProvider platform this.scheme = options.tls ? "https://" : "http://"; this.port = Defaults.getPort(options); this.hosts = new Hosts(options.restHost, Defaults.HOST_REST, options); - - this.proxyOptions = options.proxy; + ProxyOptions proxyOptions = options.proxy; if (proxyOptions != null) { String proxyHost = proxyOptions.host; if (proxyHost == null) { @@ -89,7 +88,6 @@ public HttpCore(ClientOptions options, Auth auth, PlatformAgentProvider platform if (proxyPort == 0) { throw AblyException.fromErrorInfo(new ErrorInfo("Unable to configure proxy without proxy port", 40000, 400)); } - this.proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort)); String proxyUser = proxyOptions.username; if (proxyUser != null) { String proxyPassword = proxyOptions.password; @@ -99,6 +97,9 @@ public HttpCore(ClientOptions options, Auth auth, PlatformAgentProvider platform proxyAuth = new HttpAuth(proxyUser, proxyPassword, proxyOptions.prefAuthType); } } + HttpEngineFactory engineFactory = HttpEngineFactory.getFirstAvailable(); + Log.v(TAG, String.format("Using %s HTTP Engine", engineFactory.getEngineType().name())); + this.engine = engineFactory.create(new HttpEngineConfig(ClientOptionsUtils.covertToProxyConfig(options))); } /** @@ -119,7 +120,7 @@ public T httpExecuteWithRetry(URL url, String method, Param[] headers, Reque } while (true) { try { - return httpExecute(url, getProxy(url), method, headers, requestBody, true, responseHandler); + return httpExecute(url, method, headers, requestBody, true, responseHandler); } catch (AuthRequiredException are) { if (are.authChallenge != null && requireAblyAuth) { if (are.expired && renewPending) { @@ -177,7 +178,6 @@ void authorize(boolean renew) throws AblyException { * Make a synchronous HTTP request specified by URL and proxy * * @param url - * @param proxy * @param method * @param headers * @param requestBody @@ -186,25 +186,14 @@ void authorize(boolean renew) throws AblyException { * @return * @throws AblyException */ - public T httpExecute(URL url, Proxy proxy, String method, Param[] headers, RequestBody requestBody, boolean withCredentials, ResponseHandler responseHandler) throws AblyException { - HttpURLConnection conn = null; - try { - conn = (HttpURLConnection) url.openConnection(proxy); - boolean withProxyCredentials = (proxy != Proxy.NO_PROXY) && (proxyAuth != null); - return httpExecute(conn, method, headers, requestBody, withCredentials, withProxyCredentials, responseHandler); - } catch (IOException ioe) { - throw AblyException.fromThrowable(ioe); - } finally { - if (conn != null) { - conn.disconnect(); - } - } + public T httpExecute(URL url, String method, Param[] headers, RequestBody requestBody, boolean withCredentials, ResponseHandler responseHandler) throws AblyException { + boolean withProxyCredentials = engine.isUsingProxy() && (proxyAuth != null); + return httpExecute(url, method, headers, requestBody, withCredentials, withProxyCredentials, responseHandler); } /** * Make a synchronous HTTP request with a given HttpURLConnection * - * @param conn * @param method * @param headers * @param requestBody @@ -213,111 +202,120 @@ public T httpExecute(URL url, Proxy proxy, String method, Param[] headers, R * @return * @throws AblyException */ - T httpExecute(HttpURLConnection conn, String method, Param[] headers, RequestBody requestBody, boolean withCredentials, boolean withProxyCredentials, ResponseHandler responseHandler) throws AblyException { - Response response; - boolean credentialsIncluded = false; - RawHttpListener rawHttpListener = null; - String id = null; - try { - /* prepare connection */ - conn.setRequestMethod(method); - conn.setConnectTimeout(options.httpOpenTimeout); - conn.setReadTimeout(options.httpRequestTimeout); - conn.setDoInput(true); - - String authHeader = Param.getFirst(headers, HttpConstants.Headers.AUTHORIZATION); - if (authHeader == null && auth != null) { - authHeader = auth.getAuthorizationHeader(); - } - if (withCredentials && authHeader != null) { - conn.setRequestProperty(HttpConstants.Headers.AUTHORIZATION, authHeader); - credentialsIncluded = true; - } - if (withProxyCredentials && proxyAuth.hasChallenge()) { - byte[] encodedRequestBody = (requestBody != null) ? requestBody.getEncoded() : null; - String proxyAuthorizationHeader = proxyAuth.getAuthorizationHeader(method, conn.getURL().getPath(), encodedRequestBody); - conn.setRequestProperty(HttpConstants.Headers.PROXY_AUTHORIZATION, proxyAuthorizationHeader); + T httpExecute(URL url, String method, Param[] headers, RequestBody requestBody, boolean withCredentials, boolean withProxyCredentials, ResponseHandler responseHandler) throws AblyException { + HttpRequest.HttpRequestBuilder requestBuilder = HttpRequest.builder(); + /* prepare connection */ + requestBuilder + .url(url) + .method(method) + .httpOpenTimeout(options.httpOpenTimeout) + .httpReadTimeout(options.httpRequestTimeout) + .body(requestBody != null ? new HttpBody(requestBody.getContentType(), requestBody.getEncoded()) : null); + + Map requestHeaders = collectRequestHeaders(url, method, headers, requestBody, withCredentials, withProxyCredentials); + boolean credentialsIncluded = requestHeaders.containsKey(HttpConstants.Headers.AUTHORIZATION); + String authHeader = requestHeaders.get(HttpConstants.Headers.AUTHORIZATION); + + requestBuilder.headers(requestHeaders); + HttpRequest request = requestBuilder.build(); + + // Check the logging level to avoid performance hit associated with building the message + if (Log.level <= Log.VERBOSE && request.getBody() != null && request.getBody().getContent() != null) + Log.v(TAG, System.lineSeparator() + new String(request.getBody().getContent())); + + /* log raw request details */ + Map> requestProperties = request.getHeaders(); + // Check the logging level to avoid performance hit associated with building the message + if (Log.level <= Log.VERBOSE) { + Log.v(TAG, "HTTP request: " + url + " " + method); + if (credentialsIncluded) + Log.v(TAG, " " + HttpConstants.Headers.AUTHORIZATION + ": " + authHeader); + + for (Map.Entry> entry : requestProperties.entrySet()) + for (String val : entry.getValue()) + Log.v(TAG, " " + entry.getKey() + ": " + val); + + if (requestBody != null) { + Log.v(TAG, " " + HttpConstants.Headers.CONTENT_TYPE + ": " + requestBody.getContentType()); + Log.v(TAG, " " + HttpConstants.Headers.CONTENT_LENGTH + ": " + (requestBody.getEncoded() != null ? requestBody.getEncoded().length : 0)); } - boolean acceptSet = false; - if (headers != null) { - for (Param header : headers) { - conn.setRequestProperty(header.key, header.value); - if (header.key.equals(HttpConstants.Headers.ACCEPT)) { - acceptSet = true; - } + } + + DebugOptions.RawHttpListener rawHttpListener = null; + String id = null; + + if (options instanceof DebugOptions) { + rawHttpListener = ((DebugOptions) options).httpListener; + if (rawHttpListener != null) { + id = String.valueOf(Math.random()).substring(2); + Response response = rawHttpListener.onRawHttpRequest(id, request, (credentialsIncluded ? authHeader : null), requestProperties, requestBody); + if (response != null) { + return handleResponse(credentialsIncluded, response, responseHandler); } } - if (!acceptSet) { - conn.setRequestProperty(HttpConstants.Headers.ACCEPT, HttpConstants.ContentTypes.JSON); - } + } - /* pass required headers */ - conn.setRequestProperty(Defaults.ABLY_PROTOCOL_VERSION_HEADER, Defaults.ABLY_PROTOCOL_VERSION); // RSC7a - conn.setRequestProperty(Defaults.ABLY_AGENT_HEADER, AgentHeaderCreator.create(options.agents, platformAgentProvider)); - if (options.clientId != null) - conn.setRequestProperty(Defaults.ABLY_CLIENT_ID_HEADER, Base64Coder.encodeString(options.clientId)); + Response response = executeRequest(request); - /* prepare request body */ - byte[] body = null; - if (requestBody != null) { - body = prepareRequestBody(requestBody, conn); - // Check the logging level to avoid performance hit associated with building the message - if (Log.level <= Log.VERBOSE) - Log.v(TAG, System.lineSeparator() + new String(body)); - } + if (rawHttpListener != null) { + rawHttpListener.onRawHttpResponse(id, method, response); + } - /* log raw request details */ - Map> requestProperties = conn.getRequestProperties(); - // Check the logging level to avoid performance hit associated with building the message - if (Log.level <= Log.VERBOSE) { - Log.v(TAG, "HTTP request: " + conn.getURL() + " " + method); - if (credentialsIncluded) - Log.v(TAG, " " + HttpConstants.Headers.AUTHORIZATION + ": " + authHeader); - for (Map.Entry> entry : requestProperties.entrySet()) - for (String val : entry.getValue()) - Log.v(TAG, " " + entry.getKey() + ": " + val); - } + return handleResponse(credentialsIncluded, response, responseHandler); + } - if (options instanceof DebugOptions) { - rawHttpListener = ((DebugOptions) options).httpListener; - if (rawHttpListener != null) { - id = String.valueOf(Math.random()).substring(2); - response = rawHttpListener.onRawHttpRequest(id, conn, method, (credentialsIncluded ? authHeader : null), requestProperties, requestBody); - if (response != null) { - return handleResponse(conn, credentialsIncluded, response, responseHandler); - } + private Map collectRequestHeaders(URL url, String method, Param[] headers, RequestBody requestBody, boolean withCredentials, boolean withProxyCredentials) throws AblyException { + Map requestHeaders = new HashMap<>(); + + String authHeader = Param.getFirst(headers, HttpConstants.Headers.AUTHORIZATION); + if (authHeader == null && auth != null) { + authHeader = auth.getAuthorizationHeader(); + } + + if (withCredentials && authHeader != null) { + requestHeaders.put(HttpConstants.Headers.AUTHORIZATION, authHeader); + } + + if (withProxyCredentials && proxyAuth.hasChallenge()) { + byte[] encodedRequestBody = (requestBody != null) ? requestBody.getEncoded() : null; + String proxyAuthorizationHeader = proxyAuth.getAuthorizationHeader(method, url.getPath(), encodedRequestBody); + requestHeaders.put(HttpConstants.Headers.PROXY_AUTHORIZATION, proxyAuthorizationHeader); + } + + boolean acceptSet = false; + + if (headers != null) { + for (Param header : headers) { + requestHeaders.put(header.key, header.value); + if (header.key.equals(HttpConstants.Headers.ACCEPT)) { + acceptSet = true; } } + } - /* send request body */ - if (requestBody != null) { - writeRequestBody(body, conn); - } - response = readResponse(conn); - if (rawHttpListener != null) { - rawHttpListener.onRawHttpResponse(id, method, response); - } - } catch (IOException ioe) { - if (rawHttpListener != null) { - rawHttpListener.onRawHttpException(id, method, ioe); - } - throw AblyException.fromThrowable(ioe); + if (!acceptSet) { + requestHeaders.put(HttpConstants.Headers.ACCEPT, HttpConstants.ContentTypes.JSON); } - return handleResponse(conn, credentialsIncluded, response, responseHandler); + /* pass required headers */ + requestHeaders.put(Defaults.ABLY_PROTOCOL_VERSION_HEADER, Defaults.ABLY_PROTOCOL_VERSION); // RSC7a + requestHeaders.put(Defaults.ABLY_AGENT_HEADER, AgentHeaderCreator.create(options.agents, platformAgentProvider)); + if (options.clientId != null) + requestHeaders.put(Defaults.ABLY_CLIENT_ID_HEADER, Base64Coder.encodeString(options.clientId)); + + return requestHeaders; } /** * Handle HTTP response * - * @param conn * @param credentialsIncluded * @param response * @param responseHandler * @return * @throws AblyException */ - private T handleResponse(HttpURLConnection conn, boolean credentialsIncluded, Response response, ResponseHandler responseHandler) throws AblyException { + private T handleResponse(boolean credentialsIncluded, Response response, ResponseHandler responseHandler) throws AblyException { if (response.statusCode == 0) { return null; } @@ -358,8 +356,8 @@ private T handleResponse(HttpURLConnection conn, boolean credentialsIncluded /* handle error details in header */ if (error == null) { - String errorCodeHeader = conn.getHeaderField("X-Ably-ErrorCode"); - String errorMessageHeader = conn.getHeaderField("X-Ably-ErrorMessage"); + String errorCodeHeader = response.getHeaderFields("X-Ably-ErrorCode").get(0); + String errorMessageHeader = response.getHeaderFields("X-Ably-ErrorMessage").get(0); if (errorCodeHeader != null) { try { error = new ErrorInfo(errorMessageHeader, response.statusCode, Integer.parseInt(errorCodeHeader)); @@ -389,18 +387,19 @@ private T handleResponse(HttpURLConnection conn, boolean credentialsIncluded } } } + /* handle proxy-authenticate */ if (response.statusCode == 407) { List proxyAuthHeaders = response.getHeaderFields(HttpConstants.Headers.PROXY_AUTHENTICATE); - if (proxyAuthHeaders != null && proxyAuthHeaders.size() > 0) { + if (proxyAuthHeaders != null && !proxyAuthHeaders.isEmpty()) { AuthRequiredException exception = new AuthRequiredException(null, error); exception.proxyAuthChallenge = HttpAuth.sortAuthenticateHeaders(proxyAuthHeaders); throw exception; } } + if (error == null) { error = ErrorInfo.fromResponseStatus(response.statusLine, response.statusCode); - } else { } Log.e(TAG, "Error response from server: err = " + error); if (responseHandler != null) { @@ -409,44 +408,19 @@ private T handleResponse(HttpURLConnection conn, boolean credentialsIncluded throw AblyException.fromErrorInfo(error); } - /** - * Emit the request body for an HTTP request - * - * @param requestBody - * @param conn - * @return body - * @throws IOException - */ - private byte[] prepareRequestBody(RequestBody requestBody, HttpURLConnection conn) throws IOException { - conn.setDoOutput(true); - byte[] body = requestBody.getEncoded(); - int length = body.length; - conn.setFixedLengthStreamingMode(length); - conn.setRequestProperty(HttpConstants.Headers.CONTENT_TYPE, requestBody.getContentType()); - conn.setRequestProperty(HttpConstants.Headers.CONTENT_LENGTH, Integer.toString(length)); - return body; - } - - private void writeRequestBody(byte[] body, HttpURLConnection conn) throws IOException { - OutputStream os = conn.getOutputStream(); - os.write(body); - } - /** * Read the response for an HTTP request - * - * @param connection - * @return - * @throws IOException */ - private Response readResponse(HttpURLConnection connection) throws IOException { + private Response executeRequest(HttpRequest request) { + HttpResponse rawResponse = engine.call(request).execute(); + Response response = new Response(); - response.statusCode = connection.getResponseCode(); - response.statusLine = connection.getResponseMessage(); + response.statusCode = rawResponse.getCode(); + response.statusLine = rawResponse.getMessage(); /* Store all header field names in lower-case to eliminate case insensitivity */ Log.v(TAG, "HTTP response:"); - Map> caseSensitiveHeaders = connection.getHeaderFields(); + Map> caseSensitiveHeaders = rawResponse.getHeaders(); response.headers = new HashMap<>(caseSensitiveHeaders.size(), 1f); for (Map.Entry> entry : caseSensitiveHeaders.entrySet()) { @@ -459,84 +433,20 @@ private Response readResponse(HttpURLConnection connection) throws IOException { } } - if (response.statusCode == HttpURLConnection.HTTP_NO_CONTENT) { + if (response.statusCode == HttpURLConnection.HTTP_NO_CONTENT || rawResponse.getBody() == null) { return response; } - response.contentType = connection.getContentType(); - response.contentLength = connection.getContentLength(); - - InputStream is = null; - try { - is = connection.getInputStream(); - } catch (Throwable e) { - } - if (is == null) - is = connection.getErrorStream(); + response.contentType = rawResponse.getBody().getContentType(); + response.body = rawResponse.getBody().getContent(); + response.contentLength = response.body == null ? 0 : response.body.length; - try { - response.body = readInputStream(is, response.contentLength); + if (Log.level <= Log.VERBOSE && response.body != null) Log.v(TAG, System.lineSeparator() + new String(response.body)); - } catch (NullPointerException e) { - /* nothing to read */ - } finally { - if (is != null) { - try { - is.close(); - } catch (IOException e) { - } - } - } return response; } - private byte[] readInputStream(InputStream inputStream, int bytes) throws IOException { - /* If there is nothing to read */ - if (inputStream == null) { - throw new NullPointerException("inputStream == null"); - } - - int bytesRead = 0; - - if (bytes == -1) { - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - byte[] buffer = new byte[4 * 1024]; - while ((bytesRead = inputStream.read(buffer)) > -1) { - outputStream.write(buffer, 0, bytesRead); - } - - return outputStream.toByteArray(); - } else { - int idx = 0; - byte[] output = new byte[bytes]; - while ((bytesRead = inputStream.read(output, idx, bytes - idx)) > -1) { - idx += bytesRead; - } - - return output; - } - } - - Proxy getProxy(URL url) { - String host = url.getHost(); - return getProxy(host); - } - - private Proxy getProxy(String host) { - if (proxyOptions != null) { - String[] nonProxyHosts = proxyOptions.nonProxyHosts; - if (nonProxyHosts != null) { - for (String nonProxyHostPattern : nonProxyHosts) { - if (host.matches(nonProxyHostPattern)) { - return null; - } - } - } - } - return proxy; - } - /** * Interface for an entity that supplies an httpCore request body */ diff --git a/lib/src/main/java/io/ably/lib/http/HttpScheduler.java b/lib/src/main/java/io/ably/lib/http/HttpScheduler.java index 55efe19bd..343a7f728 100644 --- a/lib/src/main/java/io/ably/lib/http/HttpScheduler.java +++ b/lib/src/main/java/io/ably/lib/http/HttpScheduler.java @@ -1,6 +1,5 @@ package io.ably.lib.http; -import java.net.HttpURLConnection; import java.net.URL; import java.util.Locale; import java.util.concurrent.ExecutionException; @@ -9,6 +8,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import io.ably.lib.network.HttpCall; import io.ably.lib.types.AblyException; import io.ably.lib.types.Callback; import io.ably.lib.types.ErrorInfo; @@ -331,15 +331,15 @@ protected void setError(ErrorInfo err) { } } protected synchronized boolean disposeConnection() { - boolean hasConnection = conn != null; + boolean hasConnection = httpCall != null; if(hasConnection) { - conn.disconnect(); - conn = null; + httpCall.cancel(); + httpCall = null; } return hasConnection; } - protected HttpURLConnection conn; + protected HttpCall httpCall; protected T result; protected ErrorInfo err; diff --git a/lib/src/main/java/io/ably/lib/transport/WebSocketTransport.java b/lib/src/main/java/io/ably/lib/transport/WebSocketTransport.java index c389be18c..42e31a899 100644 --- a/lib/src/main/java/io/ably/lib/transport/WebSocketTransport.java +++ b/lib/src/main/java/io/ably/lib/transport/WebSocketTransport.java @@ -1,24 +1,21 @@ package io.ably.lib.transport; import io.ably.lib.http.HttpUtils; +import io.ably.lib.network.WebSocketClient; +import io.ably.lib.network.WebSocketEngine; +import io.ably.lib.network.WebSocketEngineConfig; +import io.ably.lib.network.WebSocketEngineFactory; +import io.ably.lib.network.WebSocketListener; +import io.ably.lib.network.NotConnectedException; import io.ably.lib.types.AblyException; import io.ably.lib.types.ErrorInfo; import io.ably.lib.types.Param; import io.ably.lib.types.ProtocolMessage; import io.ably.lib.types.ProtocolSerializer; +import io.ably.lib.util.ClientOptionsUtils; import io.ably.lib.util.Log; -import org.java_websocket.WebSocket; -import org.java_websocket.client.WebSocketClient; -import org.java_websocket.exceptions.WebsocketNotConnectedException; -import org.java_websocket.framing.CloseFrame; -import org.java_websocket.framing.Framedata; -import org.java_websocket.handshake.ServerHandshake; - -import javax.net.ssl.HttpsURLConnection; + import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLParameters; -import javax.net.ssl.SSLSession; -import java.net.URI; import java.nio.ByteBuffer; import java.util.Timer; import java.util.TimerTask; @@ -50,7 +47,7 @@ public class WebSocketTransport implements ITransport { private final boolean channelBinaryMode; private String wsUri; private ConnectListener connectListener; - private WsClient wsConnection; + private WebSocketClient webSocketClient; /****************** * protected constructor ******************/ @@ -81,15 +78,26 @@ public void connect(ConnectListener connectListener) { Log.d(TAG, "connect(); wsUri = " + wsUri); synchronized (this) { - wsConnection = new WsClient(URI.create(wsUri), this::receive); + WebSocketEngineFactory engineFactory = WebSocketEngineFactory.getFirstAvailable(); + Log.v(TAG, String.format("Using %s WebSocket Engine", engineFactory.getEngineType().name())); + + WebSocketEngineConfig.WebSocketEngineConfigBuilder configBuilder = WebSocketEngineConfig.builder(); + configBuilder + .tls(isTls) + .host(params.host) + .proxy(ClientOptionsUtils.covertToProxyConfig(params.getClientOptions())); + if (isTls) { SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, null, null); SafeSSLSocketFactory factory = new SafeSSLSocketFactory(sslContext.getSocketFactory()); - wsConnection.setSocketFactory(factory); + configBuilder.sslSocketFactory(factory); } + + WebSocketEngine engine = engineFactory.create(configBuilder.build()); + webSocketClient = engine.create(wsUri, new WebSocketHandler(this::receive)); } - wsConnection.connect(); + webSocketClient.connect(); } catch (AblyException e) { Log.e(TAG, "Unexpected exception attempting connection; wsUri = " + wsUri, e); connectListener.onTransportUnavailable(this, e.errorInfo); @@ -103,9 +111,9 @@ public void connect(ConnectListener connectListener) { public void close() { Log.d(TAG, "close()"); synchronized (this) { - if (wsConnection != null) { - wsConnection.close(); - wsConnection = null; + if (webSocketClient != null) { + webSocketClient.close(); + webSocketClient = null; } } } @@ -127,14 +135,14 @@ public void send(ProtocolMessage msg) throws AblyException { ProtocolMessage decodedMsg = ProtocolSerializer.readMsgpack(encodedMsg); Log.v(TAG, "send(): " + decodedMsg.action + ": " + new String(ProtocolSerializer.writeJSON(decodedMsg))); } - wsConnection.send(encodedMsg); + webSocketClient.send(encodedMsg); } else { // Check the logging level to avoid performance hit associated with building the message if (Log.level <= Log.VERBOSE) Log.v(TAG, "send(): " + new String(ProtocolSerializer.writeJSON(msg))); - wsConnection.send(ProtocolSerializer.writeJSON(msg)); + webSocketClient.send(ProtocolSerializer.writeJSON(msg)); } - } catch (WebsocketNotConnectedException e) { + } catch (NotConnectedException e) { if (connectListener != null) { connectListener.onTransportUnavailable(this, AblyException.fromThrowable(e).errorInfo); } else @@ -180,7 +188,7 @@ public WebSocketTransport getTransport(TransportParams params, ConnectionManager * WebSocketHandler methods **************************/ - class WsClient extends WebSocketClient { + class WebSocketHandler implements WebSocketListener { private final WebSocketReceiver receiver; /*************************** * WsClient private members @@ -189,38 +197,16 @@ class WsClient extends WebSocketClient { private Timer timer = new Timer(); private TimerTask activityTimerTask = null; private long lastActivityTime; - private boolean shouldExplicitlyVerifyHostname = true; - WsClient(URI serverUri, WebSocketReceiver receiver) { - super(serverUri); + WebSocketHandler(WebSocketReceiver receiver) { this.receiver = receiver; } @Override - public void onOpen(ServerHandshake handshakedata) { + public void onOpen() { Log.d(TAG, "onOpen()"); - if (params.options.tls && shouldExplicitlyVerifyHostname && !isHostnameVerified(params.host)) { - close(); - } else { - connectListener.onTransportAvailable(WebSocketTransport.this); - flagActivity(); - } - } - - /** - * Added because we had to override the onSetSSLParameters() that usually performs this verification. - * When the minSdkVersion will be updated to 24 we should remove this method and its usages. - * https://github.com/TooTallNate/Java-WebSocket/wiki/No-such-method-error-setEndpointIdentificationAlgorithm#workaround - */ - private boolean isHostnameVerified(String hostname) { - final SSLSession session = getSSLSession(); - if (HttpsURLConnection.getDefaultHostnameVerifier().verify(hostname, session)) { - Log.v(TAG, "Successfully verified hostname"); - return true; - } else { - Log.e(TAG, "Hostname verification failed, expected " + hostname + ", found " + session.getPeerHost()); - return false; - } + connectListener.onTransportAvailable(WebSocketTransport.this); + flagActivity(); } @Override @@ -253,16 +239,14 @@ public void onMessage(String string) { /* This allows us to detect a websocket ping, so we don't need Ably pings. */ @Override - public void onWebsocketPing(WebSocket conn, Framedata f) { + public void onWebsocketPing() { Log.d(TAG, "onWebsocketPing()"); - /* Call superclass to ensure the pong is sent. */ - super.onWebsocketPing(conn, f); flagActivity(); } @Override - public void onClose(final int wsCode, final String wsReason, final boolean remote) { - Log.d(TAG, "onClose(): wsCode = " + wsCode + "; wsReason = " + wsReason + "; remote = " + remote); + public void onClose(final int wsCode, final String wsReason) { + Log.d(TAG, "onClose(): wsCode = " + wsCode + "; wsReason = " + wsReason + "; remote = " + false); ErrorInfo reason; switch (wsCode) { @@ -301,23 +285,14 @@ public void onClose(final int wsCode, final String wsReason, final boolean remot } @Override - public void onError(final Exception e) { - Log.e(TAG, "Connection error ", e); - connectListener.onTransportUnavailable(WebSocketTransport.this, new ErrorInfo(e.getMessage(), 503, 80000)); + public void onError(Throwable throwable) { + Log.e(TAG, "Connection error ", throwable); + connectListener.onTransportUnavailable(WebSocketTransport.this, new ErrorInfo(throwable.getMessage(), 503, 80000)); } @Override - protected void onSetSSLParameters(SSLParameters sslParameters) { - try { - super.onSetSSLParameters(sslParameters); - shouldExplicitlyVerifyHostname = false; - } catch (NoSuchMethodError exception) { - // This error will be thrown on Android below level 24. - // When the minSdkVersion will be updated to 24 we should remove this overridden method. - // https://github.com/TooTallNate/Java-WebSocket/wiki/No-such-method-error-setEndpointIdentificationAlgorithm#workaround - Log.w(TAG, "Error when trying to set SSL parameters, most likely due to an old Java API version", exception); - shouldExplicitlyVerifyHostname = true; - } + public void onOldJavaVersionDetected(Throwable throwable) { + Log.w(TAG, "Error when trying to set SSL parameters, most likely due to an old Java API version", throwable); } private synchronized void dispose() { @@ -391,7 +366,7 @@ private synchronized void onActivityTimerExpiry() { // If we have no time remaining, then close the connection if (timeRemaining <= 0) { Log.e(TAG, "No activity for " + getActivityTimeout() + "ms, closing connection"); - closeConnection(CloseFrame.ABNORMAL_CLOSE, "timed out"); + webSocketClient.cancel(ABNORMAL_CLOSE, "timed out"); return; } diff --git a/lib/src/main/java/io/ably/lib/util/ClientOptionsUtils.java b/lib/src/main/java/io/ably/lib/util/ClientOptionsUtils.java new file mode 100644 index 000000000..51535f57b --- /dev/null +++ b/lib/src/main/java/io/ably/lib/util/ClientOptionsUtils.java @@ -0,0 +1,34 @@ +package io.ably.lib.util; + +import io.ably.lib.network.ProxyAuthType; +import io.ably.lib.network.ProxyConfig; +import io.ably.lib.types.ClientOptions; + +import java.util.List; + +public class ClientOptionsUtils { + + public static ProxyConfig covertToProxyConfig(ClientOptions clientOptions) { + if (clientOptions.proxy == null) return null; + + ProxyConfig.ProxyConfigBuilder builder = ProxyConfig.builder(); + + builder + .host(clientOptions.proxy.host) + .port(clientOptions.proxy.port) + .username(clientOptions.proxy.username) + .password(clientOptions.proxy.password) + .nonProxyHosts(List.of(clientOptions.proxy.nonProxyHosts)); + + switch (clientOptions.proxy.prefAuthType) { + case BASIC: + builder.authType(ProxyAuthType.BASIC); + break; + case DIGEST: + builder.authType(ProxyAuthType.DIGEST); + break; + } + + return builder.build(); + } +} diff --git a/lib/src/test/java/io/ably/lib/test/common/Helpers.java b/lib/src/test/java/io/ably/lib/test/common/Helpers.java index 80d3c5b80..5b0f328c8 100644 --- a/lib/src/test/java/io/ably/lib/test/common/Helpers.java +++ b/lib/src/test/java/io/ably/lib/test/common/Helpers.java @@ -3,7 +3,6 @@ import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.net.HttpURLConnection; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; @@ -35,6 +34,7 @@ import io.ably.lib.debug.DebugOptions.RawProtocolListener; import io.ably.lib.http.HttpCore; import io.ably.lib.http.HttpUtils; +import io.ably.lib.network.HttpRequest; import io.ably.lib.realtime.AblyRealtime; import io.ably.lib.realtime.Channel; import io.ably.lib.realtime.Channel.MessageListener; @@ -972,7 +972,6 @@ public static boolean equalNullableStrings(String one, String two) { public static class RawHttpRequest { public String id; public URL url; - public HttpURLConnection conn; public String method; public String authHeader; public Map> requestHeaders; @@ -988,7 +987,7 @@ public static class RawHttpTracker extends LinkedHashMap private AsyncWaiter requestWaiter = null; @Override - public HttpCore.Response onRawHttpRequest(String id, HttpURLConnection conn, String method, String authHeader, Map> requestHeaders, + public HttpCore.Response onRawHttpRequest(String id, HttpRequest request, String authHeader, Map> requestHeaders, HttpCore.RequestBody requestBody) { /* duplicating if necessary, ensure lower-case versions of header names are present */ @@ -1001,9 +1000,8 @@ public HttpCore.Response onRawHttpRequest(String id, HttpURLConnection conn, Str } RawHttpRequest req = new RawHttpRequest(); req.id = id; - req.url = conn.getURL(); - req.conn = conn; - req.method = method; + req.url = request.getUrl(); + req.method = request.getMethod(); req.authHeader = authHeader; req.requestHeaders = normalisedHeaders; req.requestBody = requestBody; @@ -1076,7 +1074,7 @@ public String getRequestParam(String id, String param) { String result = null; RawHttpRequest req = get(id); if(req != null) { - String query = req.conn.getURL().getQuery(); + String query = req.url.getQuery(); if(query != null && !query.isEmpty()) { result = HttpUtils.decodeParams(query).get(param).value; } diff --git a/lib/src/test/java/io/ably/lib/test/rest/HttpTest.java b/lib/src/test/java/io/ably/lib/test/rest/HttpTest.java index 51e3add6d..fc96a1359 100644 --- a/lib/src/test/java/io/ably/lib/test/rest/HttpTest.java +++ b/lib/src/test/java/io/ably/lib/test/rest/HttpTest.java @@ -36,7 +36,6 @@ import java.io.IOException; import java.net.MalformedURLException; -import java.net.Proxy; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; @@ -137,12 +136,12 @@ public void http_ably_execute_fallback() throws AblyException { List urlArgumentStack; @Override - public T httpExecute(URL url, Proxy proxy, String method, Param[] headers, RequestBody requestBody, boolean withCredentials, ResponseHandler responseHandler) throws AblyException { + public T httpExecute(URL url, String method, Param[] headers, RequestBody requestBody, boolean withCredentials, ResponseHandler responseHandler) throws AblyException { // Store a copy of given argument urlArgumentStack.add(url.getHost()); // Execute the original method without changing behavior - return super.httpExecute(url, proxy, method, headers, requestBody, withCredentials, responseHandler); + return super.httpExecute(url, method, headers, requestBody, withCredentials, responseHandler); } public HttpCore setUrlArgumentStack(List urlArgumentStack) { @@ -273,7 +272,6 @@ public void http_ably_execute_first_attempt_to_default() throws AblyException { .when(httpCore) /* when following method is executed on {@code HttpCore} instance */ .httpExecute( url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid fallback url */ - any(Proxy.class), /* Ignore */ anyString(), /* Ignore */ aryEq(new Param[0]), /* Ignore */ any(HttpCore.RequestBody.class), /* Ignore */ @@ -316,7 +314,6 @@ public void http_ably_execute_first_attempt_to_default() throws AblyException { verify(httpCore, times(3)) .httpExecute( /* Just validating call counter. Ignore following parameters */ any(URL.class), /* Ignore */ - any(Proxy.class), /* Ignore */ anyString(), /* Ignore */ aryEq(new Param[0]), /* Ignore */ any(HttpCore.RequestBody.class), /* Ignore */ @@ -362,7 +359,6 @@ public void http_ably_execute_overriden_host() throws AblyException { .when(httpCore) /* when following method is executed on {@code HttpCore} instance */ .httpExecute( url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid fallback url */ - any(Proxy.class), /* Ignore */ anyString(), /* Ignore */ aryEq(new Param[0]), /* Ignore */ any(HttpCore.RequestBody.class), /* Ignore */ @@ -414,7 +410,6 @@ public void http_ably_execute_overriden_host() throws AblyException { verify(httpCore, times(2)) .httpExecute( /* Just validating call counter. Ignore following parameters */ any(URL.class), /* Ignore */ - any(Proxy.class), /* Ignore */ anyString(), /* Ignore */ aryEq(new Param[0]), /* Ignore */ any(HttpCore.RequestBody.class), /* Ignore */ @@ -458,7 +453,6 @@ public void http_ably_execute_empty_fallback_array() throws AblyException { .when(httpCore) /* when following method is executed on {@code HttpCore} instance */ .httpExecute( url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid fallback url */ - any(Proxy.class), /* Ignore */ anyString(), /* Ignore */ aryEq(new Param[0]), /* Ignore */ any(HttpCore.RequestBody.class), /* Ignore */ @@ -488,7 +482,6 @@ public void http_ably_execute_empty_fallback_array() throws AblyException { verify(httpCore, times(1)) .httpExecute( /* Just validating call counter. Ignore following parameters */ any(URL.class), /* Ignore */ - any(Proxy.class), /* Ignore */ anyString(), /* Ignore */ aryEq(new Param[0]), /* Ignore */ any(HttpCore.RequestBody.class), /* Ignore */ @@ -536,7 +529,6 @@ public void http_ably_execute_custom_fallback_array() throws AblyException { .when(httpCore) /* when following method is executed on {@code HttpCore} instance */ .httpExecute( url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid fallback url */ - any(Proxy.class), /* Ignore */ anyString(), /* Ignore */ aryEq(new Param[0]), /* Ignore */ any(HttpCore.RequestBody.class), /* Ignore */ @@ -559,7 +551,6 @@ public void http_ably_execute_custom_fallback_array() throws AblyException { verify(httpCore, times(expectedCallCount)) .httpExecute( /* Just validating call counter. Ignore following parameters */ any(URL.class), /* Ignore */ - any(Proxy.class), /* Ignore */ anyString(), /* Ignore */ aryEq(new Param[0]), /* Ignore */ any(HttpCore.RequestBody.class), /* Ignore */ @@ -664,7 +655,6 @@ public void http_execute_nofallback() throws Exception { .when(httpCore) /* when following method is executed on {@code HttpCore} instance */ .httpExecute( url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid rest host */ - any(Proxy.class), /* Ignore */ anyString(), /* Ignore */ aryEq(new Param[0]), /* Ignore */ any(HttpCore.RequestBody.class), /* Ignore */ @@ -691,7 +681,6 @@ public void http_execute_nofallback() throws Exception { verify(httpCore, times(1)) .httpExecute( /* Just validating call counter. Ignore following parameters */ url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid rest host */ - any(Proxy.class), /* Ignore */ anyString(), /* Ignore */ aryEq(new Param[0]), /* Ignore */ any(HttpCore.RequestBody.class), /* Ignore */ @@ -733,7 +722,6 @@ public void http_execute_singlefallback() throws Exception { .when(httpCore) /* when following method is executed on {@code HttpCore} instance */ .httpExecute( url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid rest host */ - any(Proxy.class), /* Ignore */ anyString(), /* Ignore */ aryEq(new Param[0]), /* Ignore */ any(HttpCore.RequestBody.class), /* Ignore */ @@ -762,7 +750,6 @@ public void http_execute_singlefallback() throws Exception { verify(httpCore, times(2)) .httpExecute( /* Just validating call counter. Ignore following parameters */ url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid rest host */ - any(Proxy.class), /* Ignore */ anyString(), /* Ignore */ aryEq(new Param[0]), /* Ignore */ any(HttpCore.RequestBody.class), /* Ignore */ @@ -804,7 +791,6 @@ public void http_execute_multiplefallback() throws Exception { .when(httpCore) /* when following method is executed on {@code HttpCore} instance */ .httpExecute( url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid rest host */ - any(Proxy.class), /* Ignore */ anyString(), /* Ignore */ aryEq(new Param[0]), /* Ignore */ any(HttpCore.RequestBody.class), /* Ignore */ @@ -841,7 +827,6 @@ public void http_execute_multiplefallback() throws Exception { verify(httpCore, times(3)) .httpExecute( /* Just validating call counter. Ignore following parameters */ url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid rest host */ - any(Proxy.class), /* Ignore */ anyString(), /* Ignore */ aryEq(new Param[0]), /* Ignore */ any(HttpCore.RequestBody.class), /* Ignore */ @@ -883,7 +868,6 @@ public void http_execute_fallback_success_timeout_unexpired() throws Exception { .when(httpCore) /* when following method is executed on {@code HttpCore} instance */ .httpExecute( url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid rest host */ - any(Proxy.class), /* Ignore */ anyString(), /* Ignore */ aryEq(new Param[0]), /* Ignore */ any(HttpCore.RequestBody.class), /* Ignore */ @@ -915,7 +899,6 @@ public void http_execute_fallback_success_timeout_unexpired() throws Exception { .when(httpCore) /* when following method is executed on {@code HttpCore} instance */ .httpExecute( url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid rest host */ - any(Proxy.class), /* Ignore */ anyString(), /* Ignore */ aryEq(new Param[0]), /* Ignore */ any(HttpCore.RequestBody.class), /* Ignore */ @@ -970,7 +953,6 @@ public void http_execute_fallback_failure_timeout_unexpired() throws Exception { .when(httpCore) /* when following method is executed on {@code HttpCore} instance */ .httpExecute( url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid rest host */ - any(Proxy.class), /* Ignore */ anyString(), /* Ignore */ aryEq(new Param[0]), /* Ignore */ any(HttpCore.RequestBody.class), /* Ignore */ @@ -1007,7 +989,6 @@ public void http_execute_fallback_failure_timeout_unexpired() throws Exception { .when(httpCore) /* when following method is executed on {@code HttpCore} instance */ .httpExecute( url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid rest host */ - any(Proxy.class), /* Ignore */ anyString(), /* Ignore */ aryEq(new Param[0]), /* Ignore */ any(HttpCore.RequestBody.class), /* Ignore */ @@ -1061,7 +1042,6 @@ public void http_execute_fallback_timeout_expired() throws Exception { .when(httpCore) /* when following method is executed on {@code HttpCore} instance */ .httpExecute( url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid rest host */ - any(Proxy.class), /* Ignore */ anyString(), /* Ignore */ aryEq(new Param[0]), /* Ignore */ any(HttpCore.RequestBody.class), /* Ignore */ @@ -1092,7 +1072,6 @@ public void http_execute_fallback_timeout_expired() throws Exception { .when(httpCore) /* when following method is executed on {@code HttpCore} instance */ .httpExecute( url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid rest host */ - any(Proxy.class), /* Ignore */ anyString(), /* Ignore */ aryEq(new Param[0]), /* Ignore */ any(HttpCore.RequestBody.class), /* Ignore */ @@ -1145,7 +1124,6 @@ public void http_execute_excessivefallback() throws AblyException { .when(httpCore) /* when following method is executed on {@code HttpCore} instance */ .httpExecute( url.capture(), /* capture url arguments passed down httpExecute to assert fallback behavior executed with valid rest host */ - any(Proxy.class), /* Ignore */ anyString(), /* Ignore */ aryEq(new Param[0]), /* Ignore */ any(HttpCore.RequestBody.class), /* Ignore */ diff --git a/lib/src/test/java/io/ably/lib/test/rest/RestAuthTest.java b/lib/src/test/java/io/ably/lib/test/rest/RestAuthTest.java index 9ba2a80ce..d3553e7a8 100644 --- a/lib/src/test/java/io/ably/lib/test/rest/RestAuthTest.java +++ b/lib/src/test/java/io/ably/lib/test/rest/RestAuthTest.java @@ -5,6 +5,7 @@ import io.ably.lib.debug.DebugOptions; import io.ably.lib.http.HttpConstants; import io.ably.lib.http.HttpCore; +import io.ably.lib.network.HttpRequest; import io.ably.lib.rest.AblyRest; import io.ably.lib.rest.Auth; import io.ably.lib.rest.Auth.AuthMethod; @@ -33,7 +34,6 @@ import java.io.IOException; import java.io.UnsupportedEncodingException; -import java.net.HttpURLConnection; import java.net.SocketTimeoutException; import java.util.ArrayList; import java.util.List; @@ -1378,7 +1378,7 @@ public void auth_clientid_publish_implicit() { DebugOptions options = new DebugOptions(testVars.keys[0].keyStr) {{ this.httpListener = new RawHttpListener() { @Override - public HttpCore.Response onRawHttpRequest(String id, HttpURLConnection conn, String method, String authHeader, + public HttpCore.Response onRawHttpRequest(String id, HttpRequest request, String authHeader, Map> requestHeaders, HttpCore.RequestBody requestBody) { try { if(testParams.useBinaryProtocol) { @@ -1443,7 +1443,7 @@ public void auth_clientid_publish_explicit_in_message() { DebugOptions options = new DebugOptions(testVars.keys[0].keyStr) {{ this.httpListener = new RawHttpListener() { @Override - public HttpCore.Response onRawHttpRequest(String id, HttpURLConnection conn, String method, String authHeader, + public HttpCore.Response onRawHttpRequest(String id, HttpRequest request, String authHeader, Map> requestHeaders, HttpCore.RequestBody requestBody) { try { if(testParams.useBinaryProtocol) { diff --git a/lib/src/test/java/io/ably/lib/test/rest/RestChannelPublishTest.java b/lib/src/test/java/io/ably/lib/test/rest/RestChannelPublishTest.java index 5f18c5fd3..59f9c709f 100644 --- a/lib/src/test/java/io/ably/lib/test/rest/RestChannelPublishTest.java +++ b/lib/src/test/java/io/ably/lib/test/rest/RestChannelPublishTest.java @@ -2,6 +2,7 @@ import io.ably.lib.debug.DebugOptions; import io.ably.lib.http.HttpCore; +import io.ably.lib.network.HttpRequest; import io.ably.lib.rest.AblyRest; import io.ably.lib.rest.Auth; import io.ably.lib.rest.Channel; @@ -19,7 +20,6 @@ import org.junit.Before; import org.junit.Test; -import java.net.HttpURLConnection; import java.util.HashMap; import java.util.List; import java.util.Locale; @@ -135,10 +135,10 @@ public void channel_idempotent_publish_client_generated_single() { opts.useBinaryProtocol = true; opts.httpListener = new DebugOptions.RawHttpListener() { @Override - public HttpCore.Response onRawHttpRequest(String id, HttpURLConnection conn, String method, String authHeader, Map> requestHeaders, HttpCore.RequestBody requestBody) { + public HttpCore.Response onRawHttpRequest(String id, HttpRequest request, String authHeader, Map> requestHeaders, HttpCore.RequestBody requestBody) { /* verify request body contains the supplied ids */ try { - if(method.equalsIgnoreCase("POST")) { + if(request.getMethod().equalsIgnoreCase("POST")) { Message[] requestedMessages = MessageSerializer.readMsgpack(requestBody.getEncoded()); assertEquals(requestedMessages[0].id, messageWithId.id); } @@ -196,10 +196,10 @@ public void channel_idempotent_publish_client_generated_multiple() { opts.useBinaryProtocol = true; opts.httpListener = new DebugOptions.RawHttpListener() { @Override - public HttpCore.Response onRawHttpRequest(String id, HttpURLConnection conn, String method, String authHeader, Map> requestHeaders, HttpCore.RequestBody requestBody) { + public HttpCore.Response onRawHttpRequest(String id, HttpRequest request, String authHeader, Map> requestHeaders, HttpCore.RequestBody requestBody) { /* verify request body contains the supplied ids */ try { - if(method.equalsIgnoreCase("POST")) { + if(request.getMethod().equalsIgnoreCase("POST")) { Message[] requestedMessages = MessageSerializer.readMsgpack(requestBody.getEncoded()); assertEquals(requestedMessages[0].id, messageWithId0.id); assertEquals(requestedMessages[1].id, messageWithId1.id); @@ -254,10 +254,10 @@ static class FailFirstRequest implements DebugOptions.RawHttpListener { } @Override - public HttpCore.Response onRawHttpRequest(String id, HttpURLConnection conn, String method, String authHeader, Map> requestHeaders, HttpCore.RequestBody requestBody) { + public HttpCore.Response onRawHttpRequest(String id, HttpRequest request, String authHeader, Map> requestHeaders, HttpCore.RequestBody requestBody) { /* verify request body contains the supplied ids */ try { - if(method.equalsIgnoreCase("POST")) { + if(request.getMethod().equalsIgnoreCase("POST")) { ++postRequestCount; Message[] requestedMessages = MessageSerializer.readMsgpack(requestBody.getEncoded()); if(expectedId != null) { @@ -343,10 +343,10 @@ public void channel_idempotent_publish_library_generated_multiple() { opts.useBinaryProtocol = true; opts.httpListener = new DebugOptions.RawHttpListener() { @Override - public HttpCore.Response onRawHttpRequest(String id, HttpURLConnection conn, String method, String authHeader, Map> requestHeaders, HttpCore.RequestBody requestBody) { + public HttpCore.Response onRawHttpRequest(String id, HttpRequest request, String authHeader, Map> requestHeaders, HttpCore.RequestBody requestBody) { /* verify request body contains the library-generated ids */ try { - if(method.equalsIgnoreCase("POST")) { + if(request.getMethod().equalsIgnoreCase("POST")) { Message[] requestedMessages = MessageSerializer.readMsgpack(requestBody.getEncoded()); assertTrue(requestedMessages[0].id.endsWith(":0")); assertTrue(requestedMessages[1].id.endsWith(":1")); diff --git a/network-client-core/build.gradle.kts b/network-client-core/build.gradle.kts new file mode 100644 index 000000000..162f399b5 --- /dev/null +++ b/network-client-core/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + id("java-library") + id("io.freefair.lombok") +} + +tasks.compileJava { + sourceCompatibility = "1.8" + targetCompatibility = "1.8" +} + +repositories { + mavenCentral() +} diff --git a/network-client-core/src/main/java/io/ably/lib/network/EngineType.java b/network-client-core/src/main/java/io/ably/lib/network/EngineType.java new file mode 100644 index 000000000..d3984de23 --- /dev/null +++ b/network-client-core/src/main/java/io/ably/lib/network/EngineType.java @@ -0,0 +1,6 @@ +package io.ably.lib.network; + +public enum EngineType { + DEFAULT, + OKHTTP +} diff --git a/network-client-core/src/main/java/io/ably/lib/network/HttpBody.java b/network-client-core/src/main/java/io/ably/lib/network/HttpBody.java new file mode 100644 index 000000000..00102dbc5 --- /dev/null +++ b/network-client-core/src/main/java/io/ably/lib/network/HttpBody.java @@ -0,0 +1,14 @@ +package io.ably.lib.network; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.Setter; + +@Data +@Setter(AccessLevel.NONE) +@AllArgsConstructor +public class HttpBody { + private final String contentType; + private final byte[] content; +} diff --git a/network-client-core/src/main/java/io/ably/lib/network/HttpCall.java b/network-client-core/src/main/java/io/ably/lib/network/HttpCall.java new file mode 100644 index 000000000..0d9226cbd --- /dev/null +++ b/network-client-core/src/main/java/io/ably/lib/network/HttpCall.java @@ -0,0 +1,6 @@ +package io.ably.lib.network; + +public interface HttpCall { + HttpResponse execute(); + void cancel(); +} diff --git a/network-client-core/src/main/java/io/ably/lib/network/HttpConnectionException.java b/network-client-core/src/main/java/io/ably/lib/network/HttpConnectionException.java new file mode 100644 index 000000000..5952f11db --- /dev/null +++ b/network-client-core/src/main/java/io/ably/lib/network/HttpConnectionException.java @@ -0,0 +1,7 @@ +package io.ably.lib.network; + +public class HttpConnectionException extends RuntimeException { + public HttpConnectionException(Throwable cause) { + super(cause); + } +} diff --git a/network-client-core/src/main/java/io/ably/lib/network/HttpEngine.java b/network-client-core/src/main/java/io/ably/lib/network/HttpEngine.java new file mode 100644 index 000000000..0b4fa29f3 --- /dev/null +++ b/network-client-core/src/main/java/io/ably/lib/network/HttpEngine.java @@ -0,0 +1,6 @@ +package io.ably.lib.network; + +public interface HttpEngine { + HttpCall call(HttpRequest request); + boolean isUsingProxy(); +} diff --git a/network-client-core/src/main/java/io/ably/lib/network/HttpEngineConfig.java b/network-client-core/src/main/java/io/ably/lib/network/HttpEngineConfig.java new file mode 100644 index 000000000..e19c63029 --- /dev/null +++ b/network-client-core/src/main/java/io/ably/lib/network/HttpEngineConfig.java @@ -0,0 +1,15 @@ +package io.ably.lib.network; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.Setter; + +@Data +@Setter(AccessLevel.NONE) +@Builder +@AllArgsConstructor +public class HttpEngineConfig { + private final ProxyConfig proxy; +} diff --git a/network-client-core/src/main/java/io/ably/lib/network/HttpEngineFactory.java b/network-client-core/src/main/java/io/ably/lib/network/HttpEngineFactory.java new file mode 100644 index 000000000..e93812db9 --- /dev/null +++ b/network-client-core/src/main/java/io/ably/lib/network/HttpEngineFactory.java @@ -0,0 +1,35 @@ +package io.ably.lib.network; + +import java.lang.reflect.InvocationTargetException; + +public interface HttpEngineFactory { + + HttpEngine create(HttpEngineConfig config); + EngineType getEngineType(); + + static HttpEngineFactory getFirstAvailable() { + HttpEngineFactory okHttpFactory = tryGetOkHttpFactory(); + if (okHttpFactory != null) return okHttpFactory; + HttpEngineFactory defaultFactory = tryGetDefaultFactory(); + if (defaultFactory != null) return defaultFactory; + throw new IllegalStateException("No engines are available"); + } + + static HttpEngineFactory tryGetOkHttpFactory() { + try { + Class okHttpFactoryClass = Class.forName("io.ably.lib.network.OkHttpEngineFactory"); + return (HttpEngineFactory) okHttpFactoryClass.getDeclaredConstructor().newInstance(); + } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { + return null; + } + } + + static HttpEngineFactory tryGetDefaultFactory() { + try { + Class defaultFactoryClass = Class.forName("io.ably.lib.network.DefaultHttpEngineFactory"); + return (HttpEngineFactory) defaultFactoryClass.getDeclaredConstructor().newInstance(); + } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { + return null; + } + } +} diff --git a/network-client-core/src/main/java/io/ably/lib/network/HttpRequest.java b/network-client-core/src/main/java/io/ably/lib/network/HttpRequest.java new file mode 100644 index 000000000..2e65c4027 --- /dev/null +++ b/network-client-core/src/main/java/io/ably/lib/network/HttpRequest.java @@ -0,0 +1,84 @@ +package io.ably.lib.network; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.Setter; + +import java.net.URL; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Data +@Setter(AccessLevel.NONE) +@AllArgsConstructor +public class HttpRequest { + private final URL url; + private final String method; + private final int httpOpenTimeout; + private final int httpReadTimeout; + private final HttpBody body; + private final Map> headers; + + public static HttpRequestBuilder builder() { + return new HttpRequestBuilder(); + } + + public static class HttpRequestBuilder { + private URL url; + private String method; + private int httpOpenTimeout; + private int httpReadTimeout; + private HttpBody body; + private Map> headers; + + HttpRequestBuilder() { + } + + public HttpRequestBuilder url(URL url) { + this.url = url; + return this; + } + + public HttpRequestBuilder method(String method) { + this.method = method; + return this; + } + + public HttpRequestBuilder httpOpenTimeout(int httpOpenTimeout) { + this.httpOpenTimeout = httpOpenTimeout; + return this; + } + + public HttpRequestBuilder httpReadTimeout(int httpReadTimeout) { + this.httpReadTimeout = httpReadTimeout; + return this; + } + + public HttpRequestBuilder body(HttpBody body) { + this.body = body; + return this; + } + + public HttpRequestBuilder headers(Map headers) { + Map> result = new HashMap<>(); + for (Map.Entry entry : headers.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + result.put(key, Collections.singletonList(value)); + } + this.headers = Collections.unmodifiableMap(result); + return this; + } + + public HttpRequest build() { + return new HttpRequest(this.url, this.method, this.httpOpenTimeout, this.httpReadTimeout, this.body, this.headers); + } + + public String toString() { + return "HttpRequest.HttpRequestBuilder(url=" + this.url + ", method=" + this.method + ", httpOpenTimeout=" + this.httpOpenTimeout + ", httpReadTimeout=" + this.httpReadTimeout + ", body=" + this.body + ", headers=" + this.headers + ")"; + } + } +} diff --git a/network-client-core/src/main/java/io/ably/lib/network/HttpResponse.java b/network-client-core/src/main/java/io/ably/lib/network/HttpResponse.java new file mode 100644 index 000000000..e2cf4103c --- /dev/null +++ b/network-client-core/src/main/java/io/ably/lib/network/HttpResponse.java @@ -0,0 +1,21 @@ +package io.ably.lib.network; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.Setter; + +import java.util.List; +import java.util.Map; + +@Data +@Setter(AccessLevel.NONE) +@Builder +@AllArgsConstructor +public class HttpResponse { + private final int code; + private final String message; + private final HttpBody body; + private final Map> headers; +} diff --git a/network-client-core/src/main/java/io/ably/lib/network/NotConnectedException.java b/network-client-core/src/main/java/io/ably/lib/network/NotConnectedException.java new file mode 100644 index 000000000..166549e81 --- /dev/null +++ b/network-client-core/src/main/java/io/ably/lib/network/NotConnectedException.java @@ -0,0 +1,7 @@ +package io.ably.lib.network; + +public class NotConnectedException extends RuntimeException { + public NotConnectedException(Throwable cause) { + super(cause); + } +} diff --git a/network-client-core/src/main/java/io/ably/lib/network/ProxyAuthType.java b/network-client-core/src/main/java/io/ably/lib/network/ProxyAuthType.java new file mode 100644 index 000000000..ca4cb57a5 --- /dev/null +++ b/network-client-core/src/main/java/io/ably/lib/network/ProxyAuthType.java @@ -0,0 +1,6 @@ +package io.ably.lib.network; + +public enum ProxyAuthType { + BASIC, + DIGEST +} diff --git a/network-client-core/src/main/java/io/ably/lib/network/ProxyConfig.java b/network-client-core/src/main/java/io/ably/lib/network/ProxyConfig.java new file mode 100644 index 000000000..8b87a6846 --- /dev/null +++ b/network-client-core/src/main/java/io/ably/lib/network/ProxyConfig.java @@ -0,0 +1,22 @@ +package io.ably.lib.network; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.Setter; + +import java.util.List; + +@Data +@Setter(AccessLevel.NONE) +@Builder +@AllArgsConstructor +public class ProxyConfig { + private String host; + private int port; + private String username; + private String password; + private List nonProxyHosts; + private ProxyAuthType authType; +} diff --git a/network-client-core/src/main/java/io/ably/lib/network/WebSocketClient.java b/network-client-core/src/main/java/io/ably/lib/network/WebSocketClient.java new file mode 100644 index 000000000..b3cd58108 --- /dev/null +++ b/network-client-core/src/main/java/io/ably/lib/network/WebSocketClient.java @@ -0,0 +1,33 @@ +package io.ably.lib.network; + +public interface WebSocketClient { + + void connect(); + + /** + * Sends the closing handshake. May be sent in response to any other handshake. + */ + void close(); + + /** + * Sends the closing handshake. May be sent in response to any other handshake. + * + * @param code the closing code + * @param reason the closing message + */ + void close(int code, String reason); + + /** + * This will close the connection immediately without a proper close handshake. The code and the + * message therefore won't be transferred over the wire also they will be forwarded to `onClose`. + * + * @param code the closing code + * @param reason the closing message + **/ + void cancel(int code, String reason); + + void send(byte[] message); + + void send(String message); + +} diff --git a/network-client-core/src/main/java/io/ably/lib/network/WebSocketEngine.java b/network-client-core/src/main/java/io/ably/lib/network/WebSocketEngine.java new file mode 100644 index 000000000..32bd92bdb --- /dev/null +++ b/network-client-core/src/main/java/io/ably/lib/network/WebSocketEngine.java @@ -0,0 +1,5 @@ +package io.ably.lib.network; + +public interface WebSocketEngine { + WebSocketClient create(String url, WebSocketListener listener); +} diff --git a/network-client-core/src/main/java/io/ably/lib/network/WebSocketEngineConfig.java b/network-client-core/src/main/java/io/ably/lib/network/WebSocketEngineConfig.java new file mode 100644 index 000000000..b294c58c0 --- /dev/null +++ b/network-client-core/src/main/java/io/ably/lib/network/WebSocketEngineConfig.java @@ -0,0 +1,20 @@ +package io.ably.lib.network; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.Setter; + +import javax.net.ssl.SSLSocketFactory; + +@Data +@Setter(AccessLevel.NONE) +@Builder +@AllArgsConstructor +public class WebSocketEngineConfig { + private final ProxyConfig proxy; + private final boolean tls; + private final String host; + private final SSLSocketFactory sslSocketFactory; +} diff --git a/network-client-core/src/main/java/io/ably/lib/network/WebSocketEngineFactory.java b/network-client-core/src/main/java/io/ably/lib/network/WebSocketEngineFactory.java new file mode 100644 index 000000000..be0247cb5 --- /dev/null +++ b/network-client-core/src/main/java/io/ably/lib/network/WebSocketEngineFactory.java @@ -0,0 +1,35 @@ +package io.ably.lib.network; + +import java.lang.reflect.InvocationTargetException; + +public interface WebSocketEngineFactory { + WebSocketEngine create(WebSocketEngineConfig config); + EngineType getEngineType(); + + static WebSocketEngineFactory getFirstAvailable() { + WebSocketEngineFactory okWebSocketFactory = tryGetOkWebSocketFactory(); + if (okWebSocketFactory != null) return okWebSocketFactory; + WebSocketEngineFactory defaultFactory = tryGetDefaultFactory(); + if (defaultFactory != null) return defaultFactory; + throw new IllegalStateException("No engines are available"); + } + + static WebSocketEngineFactory tryGetOkWebSocketFactory() { + try { + Class okWebSocketFactoryClass = Class.forName("io.ably.lib.network.OkWebSocketEngineFactory"); + return (WebSocketEngineFactory) okWebSocketFactoryClass.getDeclaredConstructor().newInstance(); + } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException | + InvocationTargetException e) { + return null; + } + } + + static WebSocketEngineFactory tryGetDefaultFactory() { + try { + Class defaultFactoryClass = Class.forName("io.ably.lib.network.DefaultWebSocketEngineFactory"); + return (WebSocketEngineFactory) defaultFactoryClass.getDeclaredConstructor().newInstance(); + } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { + return null; + } + } +} diff --git a/network-client-core/src/main/java/io/ably/lib/network/WebSocketListener.java b/network-client-core/src/main/java/io/ably/lib/network/WebSocketListener.java new file mode 100644 index 000000000..c3c223326 --- /dev/null +++ b/network-client-core/src/main/java/io/ably/lib/network/WebSocketListener.java @@ -0,0 +1,13 @@ +package io.ably.lib.network; + +import java.nio.ByteBuffer; + +public interface WebSocketListener { + void onOpen(); + void onMessage(ByteBuffer blob); + void onMessage(String string); + void onWebsocketPing(); + void onClose(int code, String reason); + void onError(Throwable throwable); + void onOldJavaVersionDetected(Throwable throwable); +} diff --git a/network-client-default/build.gradle.kts b/network-client-default/build.gradle.kts new file mode 100644 index 000000000..85c2da32f --- /dev/null +++ b/network-client-default/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + id("java-library") + id("io.freefair.lombok") +} + +tasks.compileJava { + sourceCompatibility = "1.8" + targetCompatibility = "1.8" +} + +repositories { + mavenCentral() +} + +dependencies { + api(project(":network-client-core")) + implementation("org.java-websocket:Java-WebSocket:1.5.3") +} diff --git a/network-client-default/src/main/java/io/ably/lib/network/DefaultHttpCall.java b/network-client-default/src/main/java/io/ably/lib/network/DefaultHttpCall.java new file mode 100644 index 000000000..73f164b5a --- /dev/null +++ b/network-client-default/src/main/java/io/ably/lib/network/DefaultHttpCall.java @@ -0,0 +1,162 @@ +package io.ably.lib.network; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.Proxy; +import java.net.URL; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +class DefaultHttpCall implements HttpCall { + public static final String CONTENT_LENGTH = "Content-Length"; + public static final String CONTENT_TYPE = "Content-Type"; + + private final Proxy proxy; + private final HttpRequest request; + private HttpURLConnection connection; + + DefaultHttpCall(HttpRequest request, Proxy proxy) { + this.request = request; + this.proxy = proxy; + } + + @Override + public HttpResponse execute() { + URL url = request.getUrl(); + try { + connection = (HttpURLConnection) url.openConnection(proxy); + /* prepare connection */ + connection.setRequestMethod(request.getMethod()); + connection.setConnectTimeout(request.getHttpOpenTimeout()); + connection.setReadTimeout(request.getHttpReadTimeout()); + connection.setDoInput(true); + + for (Map.Entry> entry : request.getHeaders().entrySet()) { + String headerName = entry.getKey(); + List values = entry.getValue(); + values.forEach(headerValue -> connection.setRequestProperty(headerName, headerValue)); + } + + /* prepare request body */ + if (request.getBody() != null) { + byte[] body = prepareRequestBody(request.getBody()); + writeRequestBody(body); + } + + return readResponse(); + } catch (IOException ioe) { + throw new HttpConnectionException(ioe); + } finally { + cancel(); + } + } + + @Override + public void cancel() { + if (connection != null) { + connection.disconnect(); + } + } + + /** + * Emit the request body for an HTTP request + */ + private byte[] prepareRequestBody(HttpBody requestBody) throws IOException { + connection.setDoOutput(true); + byte[] body = requestBody.getContent(); + int length = body.length; + connection.setFixedLengthStreamingMode(length); + connection.setRequestProperty(CONTENT_TYPE, requestBody.getContentType()); + connection.setRequestProperty(CONTENT_LENGTH, Integer.toString(length)); + return body; + } + + + private void writeRequestBody(byte[] body) throws IOException { + OutputStream os = connection.getOutputStream(); + os.write(body); + } + + private HttpResponse readResponse() throws IOException { + HttpResponse.HttpResponseBuilder builder = HttpResponse.builder(); + int statusCode = connection.getResponseCode(); + + builder + .code(statusCode) + .message(connection.getResponseMessage()); + + /* Store all header field names in lower-case to eliminate case insensitivity */ + Map> caseSensitiveHeaders = connection.getHeaderFields(); + Map> headers = new HashMap<>(caseSensitiveHeaders.size(), 1f); + + for (Map.Entry> entry : caseSensitiveHeaders.entrySet()) { + if (entry.getKey() != null) { + headers.put(entry.getKey().toLowerCase(Locale.ROOT), entry.getValue()); + } + } + + builder.headers(headers); + + if (statusCode == HttpURLConnection.HTTP_NO_CONTENT) { + return builder.build(); + } + + String contentType = connection.getContentType(); + int contentLength = connection.getContentLength(); + + InputStream is = null; + try { + is = connection.getInputStream(); + } catch (Throwable ignored) {} + + if (is == null) is = connection.getErrorStream(); + + try { + byte[] body = readInputStream(is, contentLength); + builder.body(new HttpBody(contentType, body)); + } catch (NullPointerException e) { + /* nothing to read */ + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + } + } + } + + return builder.build(); + } + + private byte[] readInputStream(InputStream inputStream, int bytes) throws IOException { + /* If there is nothing to read */ + if (inputStream == null) { + throw new NullPointerException("inputStream == null"); + } + + int bytesRead = 0; + + if (bytes == -1) { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + byte[] buffer = new byte[4 * 1024]; + while ((bytesRead = inputStream.read(buffer)) > -1) { + outputStream.write(buffer, 0, bytesRead); + } + + return outputStream.toByteArray(); + } else { + int idx = 0; + byte[] output = new byte[bytes]; + while ((bytesRead = inputStream.read(output, idx, bytes - idx)) > -1) { + idx += bytesRead; + } + + return output; + } + } +} diff --git a/network-client-default/src/main/java/io/ably/lib/network/DefaultHttpEngine.java b/network-client-default/src/main/java/io/ably/lib/network/DefaultHttpEngine.java new file mode 100644 index 000000000..e61b58d95 --- /dev/null +++ b/network-client-default/src/main/java/io/ably/lib/network/DefaultHttpEngine.java @@ -0,0 +1,26 @@ +package io.ably.lib.network; + +import java.net.InetSocketAddress; +import java.net.Proxy; + +public class DefaultHttpEngine implements HttpEngine { + + private final HttpEngineConfig config; + + public DefaultHttpEngine(HttpEngineConfig config) { + this.config = config; + } + + @Override + public HttpCall call(HttpRequest request) { + Proxy proxy = isUsingProxy() + ? new Proxy(Proxy.Type.HTTP, new InetSocketAddress(config.getProxy().getHost(), config.getProxy().getPort())) + : Proxy.NO_PROXY; + return new DefaultHttpCall(request, proxy); + } + + @Override + public boolean isUsingProxy() { + return config.getProxy() != null; + } +} diff --git a/network-client-default/src/main/java/io/ably/lib/network/DefaultHttpEngineFactory.java b/network-client-default/src/main/java/io/ably/lib/network/DefaultHttpEngineFactory.java new file mode 100644 index 000000000..533f06d91 --- /dev/null +++ b/network-client-default/src/main/java/io/ably/lib/network/DefaultHttpEngineFactory.java @@ -0,0 +1,14 @@ +package io.ably.lib.network; + +public class DefaultHttpEngineFactory implements HttpEngineFactory { + + @Override + public HttpEngine create(HttpEngineConfig config) { + return new DefaultHttpEngine(config); + } + + @Override + public EngineType getEngineType() { + return EngineType.DEFAULT; + } +} diff --git a/network-client-default/src/main/java/io/ably/lib/network/DefaultWebSocketClient.java b/network-client-default/src/main/java/io/ably/lib/network/DefaultWebSocketClient.java new file mode 100644 index 000000000..3cd4b068e --- /dev/null +++ b/network-client-default/src/main/java/io/ably/lib/network/DefaultWebSocketClient.java @@ -0,0 +1,106 @@ +package io.ably.lib.network; + +import org.java_websocket.WebSocket; +import org.java_websocket.exceptions.WebsocketNotConnectedException; +import org.java_websocket.framing.Framedata; +import org.java_websocket.handshake.ServerHandshake; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSession; +import java.net.URI; +import java.nio.ByteBuffer; + +public class DefaultWebSocketClient extends org.java_websocket.client.WebSocketClient implements WebSocketClient { + + private final WebSocketListener listener; + private final WebSocketEngineConfig config; + + private boolean shouldExplicitlyVerifyHostname = true; + + public DefaultWebSocketClient(URI serverUri, WebSocketListener listener, WebSocketEngineConfig config) { + super(serverUri); + this.listener = listener; + this.config = config; + } + + @Override + public void onOpen(ServerHandshake serverHandshake) { + if (config.isTls() && shouldExplicitlyVerifyHostname && !isHostnameVerified(config.getHost())) { + close(); + } else { + listener.onOpen(); + } + } + + @Override + public void onMessage(String s) { + listener.onMessage(s); + } + + @Override + public void onMessage(ByteBuffer blob) { + listener.onMessage(blob); + } + + /* This allows us to detect a websocket ping, so we don't need Ably pings. */ + @Override + public void onWebsocketPing(WebSocket conn, Framedata f) { + /* Call superclass to ensure the pong is sent. */ + super.onWebsocketPing(conn, f); + listener.onWebsocketPing(); + } + + @Override + public void onClose(int code, String reason, boolean remote) { + listener.onClose(code, reason); + } + + @Override + public void onError(Exception e) { + listener.onError(e); + } + + @Override + public void cancel(int code, String reason) { + closeConnection(code, reason); + } + + @Override + protected void onSetSSLParameters(SSLParameters sslParameters) { + try { + super.onSetSSLParameters(sslParameters); + shouldExplicitlyVerifyHostname = false; + } catch (NoSuchMethodError exception) { + // This error will be thrown on Android below level 24. + // When the minSdkVersion will be updated to 24 we should remove this overridden method. + // https://github.com/TooTallNate/Java-WebSocket/wiki/No-such-method-error-setEndpointIdentificationAlgorithm#workaround + shouldExplicitlyVerifyHostname = true; + listener.onOldJavaVersionDetected(exception); + } + } + + @Override + public void send(String text) { + try { + super.send(text); + } catch (WebsocketNotConnectedException e) { + throw new NotConnectedException(e); + } + } + + /** + * Added because we had to override the onSetSSLParameters() that usually performs this verification. + * When the minSdkVersion will be updated to 24 we should remove this method and its usages. + * https://github.com/TooTallNate/Java-WebSocket/wiki/No-such-method-error-setEndpointIdentificationAlgorithm#workaround + */ + private boolean isHostnameVerified(String hostname) { + final SSLSession session = getSSLSession(); + if (HttpsURLConnection.getDefaultHostnameVerifier().verify(hostname, session)) { + return true; + } else { + listener.onError(new IllegalArgumentException("Hostname verification failed, expected " + hostname + ", found " + session.getPeerHost())); + return false; + } + } +} diff --git a/network-client-default/src/main/java/io/ably/lib/network/DefaultWebSocketEngine.java b/network-client-default/src/main/java/io/ably/lib/network/DefaultWebSocketEngine.java new file mode 100644 index 000000000..e8c5ae00e --- /dev/null +++ b/network-client-default/src/main/java/io/ably/lib/network/DefaultWebSocketEngine.java @@ -0,0 +1,20 @@ +package io.ably.lib.network; + +import java.net.URI; + +public class DefaultWebSocketEngine implements WebSocketEngine { + private final WebSocketEngineConfig config; + + public DefaultWebSocketEngine(WebSocketEngineConfig config) { + this.config = config; + } + + @Override + public WebSocketClient create(String url, WebSocketListener listener) { + DefaultWebSocketClient client = new DefaultWebSocketClient(URI.create(url), listener, config); + if (config.isTls()) { + client.setSocketFactory(config.getSslSocketFactory()); + } + return client; + } +} diff --git a/network-client-default/src/main/java/io/ably/lib/network/DefaultWebSocketEngineFactory.java b/network-client-default/src/main/java/io/ably/lib/network/DefaultWebSocketEngineFactory.java new file mode 100644 index 000000000..48b564e2c --- /dev/null +++ b/network-client-default/src/main/java/io/ably/lib/network/DefaultWebSocketEngineFactory.java @@ -0,0 +1,14 @@ +package io.ably.lib.network; + +public class DefaultWebSocketEngineFactory implements WebSocketEngineFactory { + + @Override + public WebSocketEngine create(WebSocketEngineConfig config) { + return new DefaultWebSocketEngine(config); + } + + @Override + public EngineType getEngineType() { + return EngineType.DEFAULT; + } +} diff --git a/network-client-okhttp/build.gradle.kts b/network-client-okhttp/build.gradle.kts new file mode 100644 index 000000000..c9bc57ba5 --- /dev/null +++ b/network-client-okhttp/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + id("java-library") + id("io.freefair.lombok") +} + +tasks.compileJava { + sourceCompatibility = "1.8" + targetCompatibility = "1.8" +} + +repositories { + mavenCentral() +} + +dependencies { + api(project(":network-client-core")) +} diff --git a/network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpEngine.java b/network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpEngine.java new file mode 100644 index 000000000..3f5b8a11a --- /dev/null +++ b/network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpEngine.java @@ -0,0 +1,14 @@ +package io.ably.lib.network; + +public class OkHttpEngine implements HttpEngine { + + @Override + public HttpCall call(HttpRequest request) { + return null; + } + + @Override + public boolean isUsingProxy() { + return false; + } +} diff --git a/settings.gradle b/settings.gradle index 9ee5ac941..491c02b9a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,7 @@ rootProject.name = 'ably-java' include 'java', 'android', - 'gradle-lint' + 'gradle-lint', + 'network-client-core', + 'network-client-default', + 'network-client-okhttp'