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:
parent
f57c2a9140
commit
274c5c78c4
|
@ -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({
|
||||
|
|
|
@ -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;
|
|
@ -40,6 +40,10 @@ class RCTNetworking {
|
|||
static abortRequest(requestId) {
|
||||
RCTNetworkingNative.abortRequest(requestId);
|
||||
}
|
||||
|
||||
static clearCookies(callback) {
|
||||
RCTNetworkingNative.clearCookies(callback);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RCTNetworking;
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue