From 30b7385df578ab8bc07c03644d919ea7fb4de2c9 Mon Sep 17 00:00:00 2001 From: evgeny Date: Wed, 25 Sep 2024 12:39:28 +0100 Subject: [PATCH] feat: OkHttp implementation for making HTTP calls and WebSocket connections --- README.md | 61 +++++++++++++ gradle/libs.versions.toml | 2 + .../lib/transport/WebSocketTransport.java | 56 +++++++----- network-client-core/build.gradle.kts | 1 + network-client-core/gradle.properties | 4 + .../io/ably/lib/network/WebSocketEngine.java | 1 + .../lib/network/WebSocketEngineFactory.java | 2 +- network-client-default/build.gradle.kts | 2 +- .../lib/network/DefaultWebSocketEngine.java | 5 ++ network-client-okhttp/build.gradle.kts | 15 ++++ network-client-okhttp/gradle.properties | 4 + .../java/io/ably/lib/network/OkHttpCall.java | 45 ++++++++++ .../io/ably/lib/network/OkHttpEngine.java | 32 +++++++ .../ably/lib/network/OkHttpEngineFactory.java | 17 ++++ .../java/io/ably/lib/network/OkHttpUtils.java | 51 +++++++++++ .../lib/network/OkHttpWebSocketClient.java | 87 +++++++++++++++++++ .../lib/network/OkHttpWebSocketEngine.java | 32 +++++++ .../network/OkHttpWebSocketEngineFactory.java | 13 +++ settings.gradle.kts | 1 + 19 files changed, 407 insertions(+), 24 deletions(-) create mode 100644 network-client-core/gradle.properties create mode 100644 network-client-okhttp/build.gradle.kts create mode 100644 network-client-okhttp/gradle.properties create mode 100644 network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpCall.java create mode 100644 network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpEngine.java create mode 100644 network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpEngineFactory.java create mode 100644 network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpUtils.java create mode 100644 network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpWebSocketClient.java create mode 100644 network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpWebSocketEngine.java create mode 100644 network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpWebSocketEngineFactory.java diff --git a/README.md b/README.md index 69ea2b260..449ec77ea 100644 --- a/README.md +++ b/README.md @@ -500,6 +500,67 @@ realtime.setAndroidContext(context); realtime.push.activate(); ``` +## Using Ably SDK Under a Proxy + +When working in environments where outbound internet access is restricted, such as behind a corporate proxy, the Ably SDK allows you to configure a proxy server for HTTP and WebSocket connections. + +### Add the Required Dependency + +You need to use **OkHttp** library for making HTTP calls and WebSocket connections in the Ably SDK to get proxy support both for your Rest and Realtime clients. + +Add the following dependency to your `build.gradle` file: + +```groovy +dependencies { + runtimeOnly("io.ably:network-client-okhttp:1.2.43") +} +``` + +### Configure Proxy Settings + +After adding the required OkHttp dependency, you need to configure the proxy settings for your Ably client. This can be done by setting the proxy options in the `ClientOptions` object when you instantiate the Ably SDK. + +Here’s an example of how to configure and use a proxy: + +#### Java Example + +```java +import io.ably.lib.realtime.AblyRealtime; +import io.ably.lib.rest.AblyRest; +import io.ably.lib.transport.Defaults; +import io.ably.lib.types.ClientOptions; +import io.ably.lib.types.ProxyOptions; +import io.ably.lib.http.HttpAuth; + +public class AblyWithProxy { + public static void main(String[] args) throws Exception { + // Configure Ably Client options + ClientOptions options = new ClientOptions(); + + // Setup proxy settings + ProxyOptions proxy = new ProxyOptions(); + proxy.host = "your-proxy-host"; // Replace with your proxy host + proxy.port = 8080; // Replace with your proxy port + + // Optional: If the proxy requires authentication + proxy.username = "your-username"; // Replace with proxy username + proxy.password = "your-password"; // Replace with proxy password + proxy.prefAuthType = HttpAuth.Type.BASIC; // Choose your preferred authentication type (e.g., BASIC or DIGEST) + + // Attach the proxy settings to the client options + options.proxy = proxy; + + // Create an instance of Ably using the configured options + AblyRest ably = new AblyRest(options); + + // Alternatively, for real-time connections + AblyRealtime ablyRealtime = new AblyRealtime(options); + + // Use the Ably client as usual + } +} +``` + ## Resources Visit https://www.ably.com/docs for a complete API reference and more examples. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3545e89ea..d964ff095 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,6 +17,7 @@ dexmaker = "1.4" android-retrostreams = "1.7.4" maven-publish = "0.29.0" lombok = "8.10" +okhttp = "4.12.0" [libraries] gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" } @@ -38,6 +39,7 @@ dexmaker = { group = "com.crittercism.dexmaker", name = "dexmaker", version.ref dexmaker-dx = { group = "com.crittercism.dexmaker", name = "dexmaker-dx", version.ref = "dexmaker" } dexmaker-mockito = { group = "com.crittercism.dexmaker", name = "dexmaker-mockito", version.ref = "dexmaker" } android-retrostreams = { group = "net.sourceforge.streamsupport", name = "android-retrostreams", version.ref = "android-retrostreams" } +okhttp = { group ="com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } [bundles] common = ["msgpack", "vcdiff-core"] 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 cbcf58b5f..226c9a3e4 100644 --- a/lib/src/main/java/io/ably/lib/transport/WebSocketTransport.java +++ b/lib/src/main/java/io/ably/lib/transport/WebSocketTransport.java @@ -1,12 +1,13 @@ package io.ably.lib.transport; import io.ably.lib.http.HttpUtils; +import io.ably.lib.network.EngineType; +import io.ably.lib.network.NotConnectedException; 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; @@ -17,6 +18,8 @@ import javax.net.ssl.SSLContext; import java.nio.ByteBuffer; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; import java.util.Timer; import java.util.TimerTask; @@ -48,16 +51,42 @@ public class WebSocketTransport implements ITransport { private String wsUri; private ConnectListener connectListener; private WebSocketClient webSocketClient; + private final WebSocketEngine webSocketEngine; + /****************** * protected constructor ******************/ - protected WebSocketTransport(TransportParams params, ConnectionManager connectionManager) { this.params = params; this.connectionManager = connectionManager; this.channelBinaryMode = params.options.useBinaryProtocol; - /* We do not require Ably heartbeats, as we can use WebSocket pings instead. */ - params.heartbeats = false; + this.webSocketEngine = createWebSocketEngine(params); + params.heartbeats = !this.webSocketEngine.isSupportPingListener(); + + } + + private static WebSocketEngine createWebSocketEngine(TransportParams params) { + WebSocketEngineFactory engineFactory = WebSocketEngineFactory.getFirstAvailable(); + Log.v(TAG, String.format("Using %s WebSocket Engine", engineFactory.getEngineType().name())); + WebSocketEngineConfig.WebSocketEngineConfigBuilder configBuilder = WebSocketEngineConfig.builder(); + configBuilder + .tls(params.options.tls) + .host(params.host) + .proxy(ClientOptionsUtils.convertToProxyConfig(params.getClientOptions())); + + // OkHttp supports modern TLS algorithms by default + if (params.options.tls && engineFactory.getEngineType() != EngineType.OKHTTP) { + try { + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, null, null); + SafeSSLSocketFactory factory = new SafeSSLSocketFactory(sslContext.getSocketFactory()); + configBuilder.sslSocketFactory(factory); + } catch (NoSuchAlgorithmException | KeyManagementException e) { + throw new IllegalStateException("Can't get safe tls algorithms", e); + } + } + + return engineFactory.create(configBuilder.build()); } /****************** @@ -78,24 +107,7 @@ public void connect(ConnectListener connectListener) { Log.d(TAG, "connect(); wsUri = " + wsUri); synchronized (this) { - 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.convertToProxyConfig(params.getClientOptions())); - - if (isTls) { - SSLContext sslContext = SSLContext.getInstance("TLS"); - sslContext.init(null, null, null); - SafeSSLSocketFactory factory = new SafeSSLSocketFactory(sslContext.getSocketFactory()); - configBuilder.sslSocketFactory(factory); - } - - WebSocketEngine engine = engineFactory.create(configBuilder.build()); - webSocketClient = engine.create(wsUri, new WebSocketHandler(this::receive)); + webSocketClient = this.webSocketEngine.create(wsUri, new WebSocketHandler(this::receive)); } webSocketClient.connect(); } catch (AblyException e) { diff --git a/network-client-core/build.gradle.kts b/network-client-core/build.gradle.kts index 9b3ba996a..f7bb62dd6 100644 --- a/network-client-core/build.gradle.kts +++ b/network-client-core/build.gradle.kts @@ -1,6 +1,7 @@ plugins { `java-library` alias(libs.plugins.lombok) + alias(libs.plugins.maven.publish) } java { diff --git a/network-client-core/gradle.properties b/network-client-core/gradle.properties new file mode 100644 index 000000000..f37ee24fe --- /dev/null +++ b/network-client-core/gradle.properties @@ -0,0 +1,4 @@ +POM_ARTIFACT_ID=network-client-core +POM_NAME=Core HTTP client abstraction +POM_DESCRIPTION=Core HTTP client abstraction +POM_PACKAGING=jar 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 index 32bd92bdb..cf30edfac 100644 --- 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 @@ -2,4 +2,5 @@ public interface WebSocketEngine { WebSocketClient create(String url, WebSocketListener listener); + boolean isSupportPingListener(); } 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 index be0247cb5..d2f443cb3 100644 --- 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 @@ -16,7 +16,7 @@ static WebSocketEngineFactory getFirstAvailable() { static WebSocketEngineFactory tryGetOkWebSocketFactory() { try { - Class okWebSocketFactoryClass = Class.forName("io.ably.lib.network.OkWebSocketEngineFactory"); + Class okWebSocketFactoryClass = Class.forName("io.ably.lib.network.OkHttpWebSocketEngineFactory"); return (WebSocketEngineFactory) okWebSocketFactoryClass.getDeclaredConstructor().newInstance(); } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { diff --git a/network-client-default/build.gradle.kts b/network-client-default/build.gradle.kts index 4cf238353..9b19b174f 100644 --- a/network-client-default/build.gradle.kts +++ b/network-client-default/build.gradle.kts @@ -10,6 +10,6 @@ java { } dependencies { - api(project(":network-client-core")) + implementation(project(":network-client-core")) implementation(libs.java.websocket) } 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 index e8c5ae00e..652dc602c 100644 --- 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 @@ -17,4 +17,9 @@ public WebSocketClient create(String url, WebSocketListener listener) { } return client; } + + @Override + public boolean isSupportPingListener() { + return true; + } } diff --git a/network-client-okhttp/build.gradle.kts b/network-client-okhttp/build.gradle.kts new file mode 100644 index 000000000..7e3118764 --- /dev/null +++ b/network-client-okhttp/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + `java-library` + alias(libs.plugins.lombok) + alias(libs.plugins.maven.publish) +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +dependencies { + implementation(project(":network-client-core")) + implementation(libs.okhttp) +} diff --git a/network-client-okhttp/gradle.properties b/network-client-okhttp/gradle.properties new file mode 100644 index 000000000..4b648381c --- /dev/null +++ b/network-client-okhttp/gradle.properties @@ -0,0 +1,4 @@ +POM_ARTIFACT_ID=network-client-okhttp +POM_NAME=Default HTTP client +POM_DESCRIPTION=Default implementation for HTTP client +POM_PACKAGING=jar diff --git a/network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpCall.java b/network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpCall.java new file mode 100644 index 000000000..643697391 --- /dev/null +++ b/network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpCall.java @@ -0,0 +1,45 @@ +package io.ably.lib.network; + +import okhttp3.Call; +import okhttp3.Response; + +import java.io.IOException; +import java.net.ConnectException; +import java.net.NoRouteToHostException; +import java.net.SocketTimeoutException; +import java.net.UnknownHostException; + +public class OkHttpCall implements HttpCall { + private final Call call; + + public OkHttpCall(Call call) { + this.call = call; + } + + @Override + public HttpResponse execute() { + try (Response response = call.execute()) { + return HttpResponse.builder() + .headers(response.headers().toMultimap()) + .code(response.code()) + .message(response.message()) + .body( + response.body() != null && response.body().contentType() != null + ? new HttpBody(response.body().contentType().toString(), response.body().bytes()) + : null + ) + .build(); + + } catch (ConnectException | SocketTimeoutException | UnknownHostException | NoRouteToHostException fce) { + throw new FailedConnectionException(fce); + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + + } + + @Override + public void cancel() { + call.cancel(); + } +} 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..50faa3610 --- /dev/null +++ b/network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpEngine.java @@ -0,0 +1,32 @@ +package io.ably.lib.network; + +import okhttp3.Call; +import okhttp3.OkHttpClient; + +import java.util.concurrent.TimeUnit; + +public class OkHttpEngine implements HttpEngine { + + private final OkHttpClient client; + private final HttpEngineConfig config; + + public OkHttpEngine(OkHttpClient client, HttpEngineConfig config) { + this.client = client; + this.config = config; + } + + @Override + public HttpCall call(HttpRequest request) { + Call call = client.newBuilder() + .connectTimeout(request.getHttpOpenTimeout(), TimeUnit.MILLISECONDS) + .readTimeout(request.getHttpReadTimeout(), TimeUnit.MILLISECONDS) + .build() + .newCall(OkHttpUtils.toOkhttpRequest(request)); + return new OkHttpCall(call); + } + + @Override + public boolean isUsingProxy() { + return config.getProxy() != null; + } +} diff --git a/network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpEngineFactory.java b/network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpEngineFactory.java new file mode 100644 index 000000000..2cf65a9a8 --- /dev/null +++ b/network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpEngineFactory.java @@ -0,0 +1,17 @@ +package io.ably.lib.network; + +import okhttp3.OkHttpClient; + +public class OkHttpEngineFactory implements HttpEngineFactory { + @Override + public HttpEngine create(HttpEngineConfig config) { + OkHttpClient.Builder connectionBuilder = new OkHttpClient.Builder(); + OkHttpUtils.injectProxySetting(config.getProxy(), connectionBuilder); + return new OkHttpEngine(connectionBuilder.build(), config); + } + + @Override + public EngineType getEngineType() { + return EngineType.OKHTTP; + } +} diff --git a/network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpUtils.java b/network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpUtils.java new file mode 100644 index 000000000..2bd566153 --- /dev/null +++ b/network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpUtils.java @@ -0,0 +1,51 @@ +package io.ably.lib.network; + +import okhttp3.Credentials; +import okhttp3.Headers; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; + +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.util.List; +import java.util.Map; + +public class OkHttpUtils { + public static void injectProxySetting(ProxyConfig proxyConfig, OkHttpClient.Builder connectionBuilder) { + if (proxyConfig == null) return; + connectionBuilder.proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyConfig.getHost(), proxyConfig.getPort()))); + if (proxyConfig.getUsername() == null || proxyConfig.getAuthType() != ProxyAuthType.BASIC) return; + String username = proxyConfig.getUsername(); + String password = proxyConfig.getPassword(); + connectionBuilder.proxyAuthenticator((route, response) -> { + String credential = Credentials.basic(username, password); + return response.request().newBuilder() + .header("Proxy-Authorization", credential) + .build(); + }); + } + + public static Request toOkhttpRequest(HttpRequest request) { + Request.Builder builder = new Request.Builder() + .url(request.getUrl()); + + RequestBody body = null; + + if (request.getBody() != null) { + body = RequestBody.create(request.getBody().getContent(), MediaType.parse(request.getBody().getContentType())); + } + + builder.method(request.getMethod(), body); + for (Map.Entry> entry : request.getHeaders().entrySet()) { + String headerName = entry.getKey(); + List values = entry.getValue(); + for (String headerValue : values) { + builder.addHeader(headerName, headerValue); + } + } + + return builder.build(); + } +} diff --git a/network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpWebSocketClient.java b/network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpWebSocketClient.java new file mode 100644 index 000000000..7341eb71a --- /dev/null +++ b/network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpWebSocketClient.java @@ -0,0 +1,87 @@ +package io.ably.lib.network; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.WebSocket; +import okio.ByteString; + +import java.nio.ByteBuffer; + +public class OkHttpWebSocketClient implements WebSocketClient { + private final OkHttpClient connection; + private final Request request; + private final WebSocketListener listener; + private WebSocket webSocket; + + public OkHttpWebSocketClient(OkHttpClient connection, Request request, WebSocketListener listener) { + this.connection = connection; + this.request = request; + this.listener = listener; + } + + @Override + public void connect() { + webSocket = connection.newWebSocket(request, new WebSocketHandler(listener)); + } + + @Override + public void close() { + webSocket.close(1000, "Close"); + } + + @Override + public void close(int code, String reason) { + webSocket.close(code, reason); + } + + @Override + public void cancel(int code, String reason) { + webSocket.cancel(); + listener.onClose(code, reason); + } + + @Override + public void send(byte[] bytes) { + webSocket.send(ByteString.of(bytes)); + } + + @Override + public void send(String message) { + webSocket.send(message); + } + + private static class WebSocketHandler extends okhttp3.WebSocketListener { + private final WebSocketListener listener; + + private WebSocketHandler(WebSocketListener listener) { + super(); + this.listener = listener; + } + + @Override + public void onClosed(WebSocket webSocket, int code, String reason) { + listener.onClose(code, reason); + } + + @Override + public void onFailure(WebSocket webSocket, Throwable t, Response response) { + listener.onError(t); + } + + @Override + public void onMessage(WebSocket webSocket, String text) { + listener.onMessage(text); + } + + @Override + public void onMessage(WebSocket webSocket, ByteString bytes) { + listener.onMessage(ByteBuffer.wrap(bytes.toByteArray())); + } + + @Override + public void onOpen(WebSocket webSocket, Response response) { + listener.onOpen(); + } + } +} diff --git a/network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpWebSocketEngine.java b/network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpWebSocketEngine.java new file mode 100644 index 000000000..abc7b9d29 --- /dev/null +++ b/network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpWebSocketEngine.java @@ -0,0 +1,32 @@ +package io.ably.lib.network; + +import okhttp3.OkHttpClient; +import okhttp3.Request; + +public class OkHttpWebSocketEngine implements WebSocketEngine { + private final WebSocketEngineConfig config; + + public OkHttpWebSocketEngine(WebSocketEngineConfig config) { + this.config = config; + } + + @Override + public WebSocketClient create(String url, WebSocketListener listener) { + OkHttpClient.Builder connectionBuilder = new OkHttpClient.Builder(); + + Request.Builder requestBuilder = new Request.Builder().url(url); + + OkHttpUtils.injectProxySetting(config.getProxy(), connectionBuilder); + + if (config.getSslSocketFactory() != null) { + connectionBuilder.sslSocketFactory(config.getSslSocketFactory()); + } + + return new OkHttpWebSocketClient(connectionBuilder.build(), requestBuilder.build(), listener); + } + + @Override + public boolean isSupportPingListener() { + return false; + } +} diff --git a/network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpWebSocketEngineFactory.java b/network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpWebSocketEngineFactory.java new file mode 100644 index 000000000..24b7dcf20 --- /dev/null +++ b/network-client-okhttp/src/main/java/io/ably/lib/network/OkHttpWebSocketEngineFactory.java @@ -0,0 +1,13 @@ +package io.ably.lib.network; + +public class OkHttpWebSocketEngineFactory implements WebSocketEngineFactory { + @Override + public WebSocketEngine create(WebSocketEngineConfig config) { + return new OkHttpWebSocketEngine(config); + } + + @Override + public EngineType getEngineType() { + return EngineType.OKHTTP; + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index e905e3922..136b798ca 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -13,3 +13,4 @@ include("android") include("gradle-lint") include("network-client-core") include("network-client-default") +include("network-client-okhttp")