Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use OkHttp for network calls #431

Merged
merged 5 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ jobs:
install-yarn: true
node-version: 'latest'
- install_android_sdk
- run: brew install git-lfs
- run: git lfs install
- run: git lfs pull
Comment on lines +39 to +41
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Despite my efforts here, CircleCI is still broken. I will address this in a separate commit.

- run: yarn install --frozen-lockfile
- run: yarn build
- run: gem install cocoapods
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# 2.13.0

### Other
- [#431](https://github.com/okta/okta-react-native/pull/431) Use OkHttp for networking calls in Android

# 2.12.0

### Other
Expand Down
157 changes: 76 additions & 81 deletions android/src/main/java/com/oktareactnative/HttpClientImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,166 +12,161 @@

package com.oktareactnative;

import android.annotation.SuppressLint;
import static com.okta.oidc.net.ConnectionParameters.USER_AGENT;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new implementation is mostly taken from here: https://github.com/okta/okta-oidc-android/blob/ce298473dc2b1e2eaced212c98f3d3acee636f5c/app/src/main/java/com/okta/oidc/example/OkHttp.java
There are a few changes to the reference implementation, namely customizable timeouts, and user-agent response headers as required by this SDK


import android.net.Uri;
import android.os.Build;

import androidx.annotation.NonNull;

import com.okta.oidc.BuildConfig;
import com.okta.oidc.net.ConnectionParameters;
import com.okta.oidc.net.OktaHttpClient;
import com.okta.oidc.net.request.TLSSocketFactory;

import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import javax.net.ssl.HttpsURLConnection;

import static com.okta.oidc.net.ConnectionParameters.USER_AGENT;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.FormBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;

public class HttpClientImpl implements OktaHttpClient {
private final String userAgentTemplate;
private final int connectTimeoutMs;
private final int readTimeoutMs;

private HttpURLConnection mUrlConnection;
protected static OkHttpClient sOkHttpClient;
protected volatile Call mCall;
protected Response mResponse;
protected Exception mException;

HttpClientImpl(String userAgentTemplate, int connectTimeoutMs, int readTimeoutMs) {
this.userAgentTemplate = userAgentTemplate;
this.connectTimeoutMs = connectTimeoutMs;
this.readTimeoutMs = readTimeoutMs;
}

/*
* TLS v1.1, v1.2 in Android supports starting from API 16.
* But it enabled by default starting from API 20.
* This method enable these TLS versions on API < 20.
* */
@SuppressLint("RestrictedApi")
private void enableTlsV1_2(HttpURLConnection urlConnection) {
try {
((HttpsURLConnection) urlConnection)
.setSSLSocketFactory(new TLSSocketFactory());
} catch (NoSuchAlgorithmException | KeyManagementException e) {
throw new RuntimeException("Cannot create SSLContext.", e);
}
}

private String getUserAgent() {
String sdkVersion = "okta-oidc-android/" + BuildConfig.VERSION_NAME;
return userAgentTemplate.replace("$UPSTREAM_SDK", sdkVersion);
}

protected HttpURLConnection openConnection(URL url, ConnectionParameters params)
throws IOException {
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
if (mUrlConnection instanceof HttpsURLConnection &&
Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP) {
enableTlsV1_2(mUrlConnection);
protected Request buildRequest(Uri uri, ConnectionParameters param) {
if (sOkHttpClient == null) {
sOkHttpClient = new OkHttpClient.Builder()
.connectTimeout(connectTimeoutMs, TimeUnit.MILLISECONDS)
.readTimeout(readTimeoutMs, TimeUnit.MILLISECONDS)
.build();
}
Comment on lines +61 to 66
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's perfectly okay if this code gets called by multiple callers at the same time. okHttpClient doesn't need to be singleton for correctness, but only for performance reasons.


conn.setConnectTimeout(connectTimeoutMs);
conn.setReadTimeout(readTimeoutMs);
conn.setInstanceFollowRedirects(false);

Map<String, String> requestProperties = params.requestProperties();
String userAgent = getUserAgent();
requestProperties.put(USER_AGENT, userAgent);
for (String property : requestProperties.keySet()) {
conn.setRequestProperty(property, requestProperties.get(property));
Request.Builder requestBuilder = new Request.Builder().url(uri.toString());
requestBuilder.addHeader(USER_AGENT, getUserAgent());
for (Map.Entry<String, String> headerEntry : param.requestProperties().entrySet()) {
String key = headerEntry.getKey();
requestBuilder.addHeader(key, headerEntry.getValue());
}

ConnectionParameters.RequestMethod requestMethod = params.requestMethod();
Map<String, String> postParameters = params.postParameters();
conn.setRequestMethod(requestMethod.name());
if (requestMethod == ConnectionParameters.RequestMethod.GET) {
conn.setDoInput(true);
} else if (requestMethod == ConnectionParameters.RequestMethod.POST) {
conn.setDoOutput(true);
if (postParameters != null && !postParameters.isEmpty()) {
DataOutputStream out = new DataOutputStream(conn.getOutputStream());
out.write(params.getEncodedPostParameters());
out.close();
if (param.requestMethod() == ConnectionParameters.RequestMethod.GET) {
requestBuilder = requestBuilder.get();
} else {
Map<String, String> postParameters = param.postParameters();
if (postParameters != null) {
FormBody.Builder formBuilder = new FormBody.Builder();
for (Map.Entry<String, String> postEntry : postParameters.entrySet()) {
String key = postEntry.getKey();
formBuilder.add(key, postEntry.getValue());
}
RequestBody formBody = formBuilder.build();
requestBuilder.post(formBody);
} else {
requestBuilder.post(RequestBody.create(null, ""));
}
}
return conn;
return requestBuilder.build();
}

@Override
public InputStream connect(@NonNull Uri uri, @NonNull ConnectionParameters params)
throws Exception {
Request request = buildRequest(uri, params);
mCall = sOkHttpClient.newCall(request);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mCall is a class level variable here. It's only useful to keep track of it in case of cancellation. We can only track and cancel one call at a time with this implementation, but this was also a limitation with the previous implementation.
This SDK also doesn't make use of the cancel function

final CountDownLatch latch = new CountDownLatch(1);
mCall.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
mException = e;
latch.countDown();
}

mUrlConnection = openConnection(new URL(uri.toString()), params);
mUrlConnection.connect();
try {
return mUrlConnection.getInputStream();
} catch (IOException e) {
return mUrlConnection.getErrorStream();
@Override
public void onResponse(Call call, Response response) {
mResponse = response;
latch.countDown();
}
});
latch.await();
if (mException != null) {
throw mException;
}
if (mResponse != null && mResponse.body() != null) {
return mResponse.body().byteStream();
}
return null;
}


@Override
public void cleanUp() {
mUrlConnection = null;
//NO-OP
}

@Override
public void cancel() {
if (mUrlConnection != null) {
mUrlConnection.disconnect();
if (mCall != null) {
mCall.cancel();
}
}

@Override
public Map<String, List<String>> getHeaderFields() {
if (mUrlConnection != null) {
return mUrlConnection.getHeaderFields();
if (mResponse != null) {
return mResponse.headers().toMultimap();
}
return null;
}

@Override
public String getHeader(String header) {
if (mUrlConnection != null) {
return mUrlConnection.getHeaderField(header);
if (mResponse != null) {
return mResponse.header(header);
}
return null;
}

@Override
public int getResponseCode() throws IOException {
if (mUrlConnection != null) {
return mUrlConnection.getResponseCode();
if (mResponse != null) {
return mResponse.code();
}
return -1;
}

@Override
public int getContentLength() {
if (mUrlConnection != null) {
return mUrlConnection.getContentLength();
if (mResponse != null && mResponse.body() != null) {
return (int) mResponse.body().contentLength();
}
return -1;
}

@Override
public String getResponseMessage() throws IOException {
if (mUrlConnection != null) {
return mUrlConnection.getResponseMessage();
if (mResponse != null) {
return mResponse.message();
}
return null;
}

public HttpURLConnection getUrlConnection() {
return mUrlConnection;
}
}
1 change: 1 addition & 0 deletions e2e/android/forceVersions.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ def forceVersions(ConfigurationContainer configurations) {
force 'junit:junit:4.13.2'
force 'commons-io:commons-io:2.15.1'
force 'commons-codec:commons-codec:1.17.0'
force 'io.netty:netty-common:4.1.93.Final'
}
}
}
Expand Down
1 change: 0 additions & 1 deletion e2e/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
"react-native-reanimated": "^3.11.0",
"react-native-safe-area-context": "4.10.1",
"react-native-screens": "~3.31.1",
"react-native-web": "~0.19.12",
"semver": "^7.6.2"
},
"resolutions": {
Expand Down
Loading
Loading