Support cookies on Android

Summary: This adds a persistent cookie store that shares cookies with WebView.

Add a `ForwardingCookieHandler` to OkHttp that uses the underlying Android webkit `CookieManager`.
Use a `LazyCookieHandler` to defer initialization of `CookieManager` as this will in turn trigger initialization of the Chromium stack in KitKat+ which takes some time. This was we will incur this cost on a background network thread instead of during startup.
Also add a `clearCookies()` method to the network module.

Add a cookies example to the XHR example. This example should also work for iOS (except for the clear cookies part). They are for now just scoped to Android.

Closes #2792.

public

Reviewed By: andreicoman11

Differential Revision: D2615550

fb-gh-sync-id: ff726a35f0fc3c7124d2f755448fe24c9d1caf21
This commit is contained in:
Alexander Blom 2015-11-23 02:16:51 -08:00 committed by facebook-github-bot-6
parent f57c2a9140
commit 274c5c78c4
7 changed files with 419 additions and 15 deletions

View File

@ -27,6 +27,8 @@ var {
} = React;
var XHRExampleHeaders = require('./XHRExampleHeaders');
var XHRExampleCookies = require('./XHRExampleCookies');
// TODO t7093728 This is a simlified XHRExample.ios.js.
// Once we have Camera roll, Toast, Intent (for opening URLs)
@ -284,6 +286,11 @@ exports.examples = [{
render() {
return <XHRExampleHeaders/>;
}
}, {
title: 'Cookies',
render() {
return <XHRExampleCookies/>;
}
}];
var styles = StyleSheet.create({

View File

@ -0,0 +1,128 @@
/**
* The examples provided by Facebook are for non-commercial testing and
* evaluation purposes only.
*
* Facebook reserves all rights not expressly granted.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
* FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
* @flow
*/
'use strict';
var React = require('react-native');
var {
StyleSheet,
Text,
TouchableHighlight,
View,
} = React;
var RCTNetworking = require('RCTNetworking');
class XHRExampleCookies extends React.Component {
constructor(props: any) {
super(props);
this.cancelled = false;
this.state = {
status: '',
a: 1,
b: 2,
};
}
setCookie(domain: string) {
var {a, b} = this.state;
var url = `https://${domain}/cookies/set?a=${a}&b=${b}`;
fetch(url).then((response) => {
this.setStatus(`Cookies a=${a}, b=${b} set`);
});
this.setState({
status: 'Setting cookies...',
a: a + 1,
b: b + 2,
});
}
getCookies(domain: string) {
fetch(`https://${domain}/cookies`).then((response) => {
return response.json();
}).then((data) => {
this.setStatus(`Got cookies ${JSON.stringify(data.cookies)} from server`);
});
this.setStatus('Getting cookies...');
}
clearCookies() {
RCTNetworking.clearCookies((cleared) => {
this.setStatus('Cookies cleared, had cookies=' + cleared);
});
}
setStatus(status: string) {
this.setState({status});
}
render() {
return (
<View>
<TouchableHighlight
style={styles.wrapper}
onPress={this.setCookie.bind(this, 'httpbin.org')}>
<View style={styles.button}>
<Text>Set cookie</Text>
</View>
</TouchableHighlight>
<TouchableHighlight
style={styles.wrapper}
onPress={this.setCookie.bind(this, 'eu.httpbin.org')}>
<View style={styles.button}>
<Text>Set cookie (EU)</Text>
</View>
</TouchableHighlight>
<TouchableHighlight
style={styles.wrapper}
onPress={this.getCookies.bind(this, 'httpbin.org')}>
<View style={styles.button}>
<Text>Get cookies</Text>
</View>
</TouchableHighlight>
<TouchableHighlight
style={styles.wrapper}
onPress={this.getCookies.bind(this, 'eu.httpbin.org')}>
<View style={styles.button}>
<Text>Get cookies (EU)</Text>
</View>
</TouchableHighlight>
<TouchableHighlight
style={styles.wrapper}
onPress={this.clearCookies.bind(this)}>
<View style={styles.button}>
<Text>Clear cookies</Text>
</View>
</TouchableHighlight>
<Text>{this.state.status}</Text>
</View>
);
}
}
var styles = StyleSheet.create({
wrapper: {
borderRadius: 5,
marginBottom: 5,
},
button: {
backgroundColor: '#eeeeee',
padding: 8,
},
});
module.exports = XHRExampleCookies;

View File

@ -40,6 +40,10 @@ class RCTNetworking {
static abortRequest(requestId) {
RCTNetworkingNative.abortRequest(requestId);
}
static clearCookies(callback) {
RCTNetworkingNative.clearCookies(callback);
}
}
module.exports = RCTNetworking;

View File

@ -80,7 +80,8 @@ public class FrescoModule extends ReactContextBaseJavaModule implements
}
Context context = this.getReactApplicationContext().getApplicationContext();
OkHttpClient okHttpClient = OkHttpClientProvider.getOkHttpClient();
OkHttpClient okHttpClient =
OkHttpClientProvider.getCookieAwareOkHttpClient(getReactApplicationContext());
ImagePipelineConfig.Builder builder =
OkHttpImagePipelineConfigFactory.newBuilder(context, okHttpClient);

View File

@ -0,0 +1,230 @@
// Copyright 2004-present Facebook. All Rights Reserved.
package com.facebook.react.modules.network;
import javax.annotation.Nullable;
import java.io.IOException;
import java.net.CookieHandler;
import java.net.URI;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.text.TextUtils;
import android.webkit.CookieManager;
import android.webkit.CookieSyncManager;
import android.webkit.ValueCallback;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.GuardedAsyncTask;
import com.facebook.react.bridge.GuardedResultAsyncTask;
import com.facebook.react.bridge.ReactContext;
/**
* Cookie handler that forwards all cookies to the WebView CookieManager.
*
* This class relies on CookieManager to persist cookies to disk so cookies may be lost if the
* application is terminated before it syncs.
*/
public class ForwardingCookieHandler extends CookieHandler {
private static final String VERSION_ZERO_HEADER = "Set-cookie";
private static final String VERSION_ONE_HEADER = "Set-cookie2";
private static final String COOKIE_HEADER = "Cookie";
// As CookieManager was synchronous before API 21 this class emulates the async behavior on <21.
private static final boolean USES_LEGACY_STORE = Build.VERSION.SDK_INT < 21;
private final CookieSaver mCookieSaver;
private final ReactContext mContext;
private @Nullable CookieManager mCookieManager;
public ForwardingCookieHandler(ReactContext context) {
mContext = context;
mCookieSaver = new CookieSaver();
}
@Override
public Map<String, List<String>> get(URI uri, Map<String, List<String>> headers)
throws IOException {
String cookies = getCookieManager().getCookie(uri.toString());
if (TextUtils.isEmpty(cookies)) {
return Collections.emptyMap();
}
return Collections.singletonMap(COOKIE_HEADER, Collections.singletonList(cookies));
}
@Override
public void put(URI uri, Map<String, List<String>> headers) throws IOException {
String url = uri.toString();
for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
String key = entry.getKey();
if (key != null && isCookieHeader(key)) {
addCookies(url, entry.getValue());
}
}
}
public void clearCookies(final Callback callback) {
if (USES_LEGACY_STORE) {
new GuardedResultAsyncTask<Boolean>(mContext) {
@Override
protected Boolean doInBackgroundGuarded() {
getCookieManager().removeAllCookie();
mCookieSaver.onCookiesModified();
return true;
}
@Override
protected void onPostExecuteGuarded(Boolean result) {
callback.invoke(result);
}
}.execute();
} else {
clearCookiesAsync(callback);
}
}
private void clearCookiesAsync(final Callback callback) {
getCookieManager().removeAllCookies(
new ValueCallback<Boolean>() {
@Override
public void onReceiveValue(Boolean value) {
mCookieSaver.onCookiesModified();
callback.invoke(value);
}
});
}
public void destroy() {
if (USES_LEGACY_STORE) {
getCookieManager().removeExpiredCookie();
mCookieSaver.persistCookies();
}
}
private void addCookies(final String url, final List<String> cookies) {
if (USES_LEGACY_STORE) {
runInBackground(
new Runnable() {
@Override
public void run() {
for (String cookie : cookies) {
getCookieManager().setCookie(url, cookie);
}
mCookieSaver.onCookiesModified();
}
});
} else {
for (String cookie : cookies) {
addCookieAsync(url, cookie);
}
mCookieSaver.onCookiesModified();
}
}
@TargetApi(21)
private void addCookieAsync(String url, String cookie) {
getCookieManager().setCookie(url, cookie, null);
}
private static boolean isCookieHeader(String name) {
return name.equalsIgnoreCase(VERSION_ZERO_HEADER) || name.equalsIgnoreCase(VERSION_ONE_HEADER);
}
private void runInBackground(final Runnable runnable) {
new GuardedAsyncTask<Void, Void>(mContext) {
@Override
protected void doInBackgroundGuarded(Void... params) {
runnable.run();
}
}.execute();
}
/**
* Instantiating CookieManager in KitKat+ will load the Chromium task taking a 100ish ms so we
* do it lazily to make sure it's done on a background thread as needed.
*/
private CookieManager getCookieManager() {
if (mCookieManager == null) {
possiblyWorkaroundSyncManager(mContext);
mCookieManager = CookieManager.getInstance();
if (USES_LEGACY_STORE) {
mCookieManager.removeExpiredCookie();
}
}
return mCookieManager;
}
private static void possiblyWorkaroundSyncManager(Context context) {
if (USES_LEGACY_STORE) {
// This is to work around a bug where CookieManager may fail to instantiate if
// CookieSyncManager has never been created. Note that the sync() may not be required but is
// here of legacy reasons.
CookieSyncManager syncManager = CookieSyncManager.createInstance(context);
syncManager.sync();
}
}
/**
* Responsible for flushing cookies to disk. Flushes to disk with a maximum delay of 30 seconds.
* This class is only active if we are on API < 21.
*/
private class CookieSaver {
private static final int MSG_PERSIST_COOKIES = 1;
private static final int TIMEOUT = 30 * 1000; // 30 seconds
private final Handler mHandler;
public CookieSaver() {
mHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
if (msg.what == MSG_PERSIST_COOKIES) {
persistCookies();
return true;
} else {
return false;
}
}
});
}
public void onCookiesModified() {
if (USES_LEGACY_STORE) {
mHandler.sendEmptyMessageDelayed(MSG_PERSIST_COOKIES, TIMEOUT);
}
}
public void persistCookies() {
mHandler.removeMessages(MSG_PERSIST_COOKIES);
runInBackground(
new Runnable() {
@Override
public void run() {
if (USES_LEGACY_STORE) {
CookieSyncManager syncManager = CookieSyncManager.getInstance();
syncManager.sync();
} else {
flush();
}
}
});
}
@TargetApi(21)
private void flush() {
getCookieManager().flush();
}
}
}

View File

@ -14,6 +14,7 @@ import javax.annotation.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.net.CookieHandler;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.GuardedAsyncTask;
@ -72,19 +73,19 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
}
/**
* @param reactContext the ReactContext of the application
* @param context the ReactContext of the application
*/
public NetworkingModule(ReactApplicationContext reactContext) {
this(reactContext, null, OkHttpClientProvider.getOkHttpClient());
public NetworkingModule(final ReactApplicationContext context) {
this(context, null, OkHttpClientProvider.getCookieAwareOkHttpClient(context));
}
/**
* @param reactContext the ReactContext of the application
* @param context the ReactContext of the application
* @param defaultUserAgent the User-Agent header that will be set for all requests where the
* caller does not provide one explicitly
*/
public NetworkingModule(ReactApplicationContext reactContext, String defaultUserAgent) {
this(reactContext, defaultUserAgent, OkHttpClientProvider.getOkHttpClient());
public NetworkingModule(ReactApplicationContext context, String defaultUserAgent) {
this(context, defaultUserAgent, OkHttpClientProvider.getCookieAwareOkHttpClient(context));
}
public NetworkingModule(ReactApplicationContext reactContext, OkHttpClient client) {
@ -100,6 +101,11 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
public void onCatalystInstanceDestroy() {
mShuttingDown = true;
mClient.cancel(null);
CookieHandler cookieHandler = mClient.getCookieHandler();
if (cookieHandler instanceof ForwardingCookieHandler) {
((ForwardingCookieHandler) cookieHandler).destroy();
}
}
@ReactMethod
@ -311,6 +317,14 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
}.execute();
}
@ReactMethod
public void clearCookies(com.facebook.react.bridge.Callback callback) {
CookieHandler cookieHandler = mClient.getCookieHandler();
if (cookieHandler instanceof ForwardingCookieHandler) {
((ForwardingCookieHandler) cookieHandler).clearCookies(callback);
}
}
private @Nullable MultipartBuilder constructMultipartBody(
ReadableArray body,
String contentType,

View File

@ -9,7 +9,12 @@
package com.facebook.react.modules.network;
import javax.annotation.Nullable;
import java.util.concurrent.TimeUnit;
import com.facebook.react.bridge.ReactContext;
import com.squareup.okhttp.OkHttpClient;
/**
@ -19,18 +24,33 @@ import com.squareup.okhttp.OkHttpClient;
public class OkHttpClientProvider {
// Centralized OkHttpClient for all networking requests.
private static OkHttpClient sClient;
private static @Nullable OkHttpClient sClient;
private static ForwardingCookieHandler sCookieHandler;
public static OkHttpClient getOkHttpClient() {
if (sClient == null) {
// TODO: #7108751 plug in stetho
sClient = new OkHttpClient();
// No timeouts by default
sClient.setConnectTimeout(0, TimeUnit.MILLISECONDS);
sClient.setReadTimeout(0, TimeUnit.MILLISECONDS);
sClient.setWriteTimeout(0, TimeUnit.MILLISECONDS);
sClient = createClient();
}
return sClient;
}
public static OkHttpClient getCookieAwareOkHttpClient(ReactContext context) {
if (sCookieHandler == null) {
sCookieHandler = new ForwardingCookieHandler(context);
getOkHttpClient().setCookieHandler(sCookieHandler);
}
return getOkHttpClient();
}
private static OkHttpClient createClient() {
// TODO: #7108751 plug in stetho
OkHttpClient client = new OkHttpClient();
// No timeouts by default
client.setConnectTimeout(0, TimeUnit.MILLISECONDS);
client.setReadTimeout(0, TimeUnit.MILLISECONDS);
client.setWriteTimeout(0, TimeUnit.MILLISECONDS);
return client;
}
}