Skip to content

Instantly share code, notes, and snippets.

@nuald
Forked from franmontiel/PersistentCookieStore.java
Last active July 27, 2022 03:07
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save nuald/ad776c9f7f52d3f6865142bda58c6d3f to your computer and use it in GitHub Desktop.
A persistent CookieStore implementation for use in Android with HTTPUrlConnection or OkHttp 2. -- For a OkHttp 3 persistent CookieJar implementation you can use this library: https://github.com/franmontiel/PersistentCookieJar
/*
* Copyright (c) 2015 Fran Montiel
* Copyright (c) 2016 Alexander Slesarev
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import java.net.CookieStore;
import java.net.HttpCookie;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
class PersistentCookieStore implements CookieStore {
private static final String TAG = PersistentCookieStore.class.getSimpleName();
// Persistence
private static final String SP_COOKIE_STORE = "cookieStore";
private static final String SP_KEY_DELIMITER = "|"; // Unusual char in URL
private static final String SP_KEY_DELIMITER_REGEX = "\\" + SP_KEY_DELIMITER;
private final SharedPreferences cookieStore;
// For session cookies
private static final Map<String, String> memoryStore = new HashMap<>();
// In memory
private final Map<URI, Set<SerializableHttpCookie>> allCookies = new HashMap<>();
PersistentCookieStore(Context context) {
cookieStore = context.getSharedPreferences(SP_COOKIE_STORE, Context.MODE_PRIVATE);
loadAllFromPersistence(cookieStore.getAll());
loadAllFromPersistence(memoryStore);
}
private void loadAllFromPersistence(Map<String, ?> allPairs) {
for (Map.Entry<String, ?> entry : allPairs.entrySet()) {
String[] uriAndName = entry.getKey().split(SP_KEY_DELIMITER_REGEX, 2);
try {
URI uri = new URI(uriAndName[0]);
String encodedCookie = (String) entry.getValue();
SerializableHttpCookie cookie = SerializableHttpCookie.decode(encodedCookie);
Set<SerializableHttpCookie> targetCookies = allCookies.get(uri);
if (targetCookies == null) {
targetCookies = new HashSet<>();
allCookies.put(uri, targetCookies);
}
if (cookie != null) {
targetCookies.add(cookie);
}
} catch (URISyntaxException e) {
Log.w(TAG, e);
}
}
}
@Override
public synchronized void add(URI uri, HttpCookie cookie) {
uri = cookieUri(uri, cookie);
Set<SerializableHttpCookie> targetCookies = allCookies.get(uri);
SerializableHttpCookie normalizedCookie = new SerializableHttpCookie(cookie);
if (targetCookies == null) {
targetCookies = new HashSet<>();
allCookies.put(uri, targetCookies);
} else {
targetCookies.remove(normalizedCookie);
}
targetCookies.add(normalizedCookie);
saveToPersistence(uri, normalizedCookie);
}
/**
* Get the real URI from the cookie "domain" and "path" attributes, if they
* are not set then uses the URI provided (coming from the response)
*
* @param uri The response URI
* @param cookie The cookie
* @return The real URI
*/
private static URI cookieUri(URI uri, HttpCookie cookie) {
URI cookieUri = uri;
if (cookie.getDomain() != null) {
// Remove the starting dot character of the domain, if exists (e.g: .domain.com -> domain.com)
String domain = cookie.getDomain();
if (domain.charAt(0) == '.') {
domain = domain.substring(1);
}
try {
cookieUri = new URI(
uri.getScheme() == null ? "http": uri.getScheme(),
domain,
cookie.getPath() == null ? "/" : cookie.getPath(),
null
);
} catch (URISyntaxException e) {
Log.w(TAG, e);
}
}
return cookieUri;
}
private void saveToPersistence(URI uri, SerializableHttpCookie cookie) {
String key = uri.toString() + SP_KEY_DELIMITER + cookie.getHttpCookie().getName();
String value = cookie.encode();
if (cookie.getHttpCookie().getMaxAge() > 0) {
SharedPreferences.Editor editor = cookieStore.edit();
editor.putString(key, value);
editor.apply();
} else {
memoryStore.put(key, value);
}
}
@Override
public synchronized List<HttpCookie> get(URI uri) {
return getValidCookies(uri);
}
@Override
public synchronized List<HttpCookie> getCookies() {
List<HttpCookie> allValidCookies = new ArrayList<>();
for (URI storedUri : allCookies.keySet()) {
allValidCookies.addAll(getValidCookies(storedUri));
}
return allValidCookies;
}
private List<HttpCookie> getValidCookies(URI uri) {
List<SerializableHttpCookie> targetCookies = new ArrayList<>();
// If the stored URI does not have a path then it must match any URI in
// the same domain
for (URI storedUri : allCookies.keySet()) {
// Check ith the domains match according to RFC 6265
if (checkDomainsMatch(storedUri.getHost(), uri.getHost())) {
// Check if the paths match according to RFC 6265
if (checkPathsMatch(storedUri.getPath(), uri.getPath())) {
targetCookies.addAll(allCookies.get(storedUri));
}
}
}
List<HttpCookie> httpCookies = new ArrayList<>();
List<SerializableHttpCookie> cookiesToRemoveFromPersistence = new ArrayList<>();
for (SerializableHttpCookie cookie: targetCookies) {
// Check it there are expired cookies and remove them
if (cookie.hasExpired()) {
cookiesToRemoveFromPersistence.add(cookie);
} else {
httpCookies.add(cookie.getHttpCookie());
}
}
if (!cookiesToRemoveFromPersistence.isEmpty()) {
removeFromPersistence(uri, cookiesToRemoveFromPersistence);
}
return httpCookies;
}
/* http://tools.ietf.org/html/rfc6265#section-5.1.3
A string domain-matches a given domain string if at least one of the
following conditions hold:
o The domain string and the string are identical. (Note that both
the domain string and the string will have been canonicalized to
lower case at this point.)
o All of the following conditions hold:
* The domain string is a suffix of the string.
* The last character of the string that is not included in the
domain string is a %x2E (".") character.
* The string is a host name (i.e., not an IP address). */
private boolean checkDomainsMatch(String cookieHost, String requestHost) {
return requestHost.equals(cookieHost) || requestHost.endsWith("." + cookieHost);
}
/* http://tools.ietf.org/html/rfc6265#section-5.1.4
A request-path path-matches a given cookie-path if at least one of
the following conditions holds:
o The cookie-path and the request-path are identical.
o The cookie-path is a prefix of the request-path, and the last
character of the cookie-path is %x2F ("/").
o The cookie-path is a prefix of the request-path, and the first
character of the request-path that is not included in the cookie-
path is a %x2F ("/") character. */
private boolean checkPathsMatch(String cookiePath, String requestPath) {
return requestPath.equals(cookiePath) ||
(requestPath.startsWith(cookiePath) && cookiePath.charAt(cookiePath.length() - 1) == '/') ||
(requestPath.startsWith(cookiePath) && requestPath.substring(cookiePath.length()).charAt(0) == '/');
}
private void removeFromPersistence(URI uri, List<SerializableHttpCookie> cookiesToRemove) {
SharedPreferences.Editor editor = cookieStore.edit();
for (SerializableHttpCookie cookieToRemove : cookiesToRemove) {
String key = uri.toString() + SP_KEY_DELIMITER + cookieToRemove.getHttpCookie().getName();
editor.remove(key);
memoryStore.remove(key);
}
editor.apply();
}
@Override
public synchronized List<URI> getURIs() {
return new ArrayList<>(allCookies.keySet());
}
@Override
public synchronized boolean remove(URI uri, HttpCookie cookie) {
Set<SerializableHttpCookie> targetCookies = allCookies.get(uri);
SerializableHttpCookie normalizedCookie = new SerializableHttpCookie(cookie);
boolean cookieRemoved = false;
if (targetCookies != null) {
cookieRemoved = targetCookies.remove(normalizedCookie);
}
if (cookieRemoved) {
removeFromPersistence(uri, normalizedCookie);
}
return cookieRemoved;
}
private void removeFromPersistence(URI uri, SerializableHttpCookie cookieToRemove) {
SharedPreferences.Editor editor = cookieStore.edit();
String key = uri.toString() + SP_KEY_DELIMITER + cookieToRemove.getHttpCookie().getName();
editor.remove(key);
editor.apply();
memoryStore.remove(key);
}
@Override
public synchronized boolean removeAll() {
allCookies.clear();
removeAllFromPersistence();
return true;
}
private void removeAllFromPersistence() {
cookieStore.edit().clear().apply();
memoryStore.clear();
}
}
/*
* Copyright (c) 2011 James Smith <james@loopj.com>
* Copyright (c) 2015 Fran Montiel
* Copyright (c) 2016 Alexander Slesarev
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import android.util.Log;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.net.HttpCookie;
/**
* Based on the code from this stackoverflow answer http://stackoverflow.com/a/25462286/980387 by janoliver
* Modifications in the structure of the class and addition of serialization of httpOnly attribute
*/
class SerializableHttpCookie implements Serializable {
private static final String TAG = SerializableHttpCookie.class.getSimpleName();
private static final long serialVersionUID = 6374381323722046732L;
private transient HttpCookie cookie;
// Workaround httpOnly: The httpOnly attribute is not accessible so when we
// serialize and deserialize the cookie it not preserve the same value. We
// need to access it using reflection
private Field fieldHttpOnly;
private long whenCreated;
SerializableHttpCookie(HttpCookie cookie) {
this.cookie = cookie;
whenCreated = System.currentTimeMillis();
}
boolean hasExpired() {
long maxAge = cookie.getMaxAge();
if (maxAge == -1L) {
return false;
}
long deltaSecond = (System.currentTimeMillis() - whenCreated) / 1000;
return deltaSecond > maxAge;
}
HttpCookie getHttpCookie() {
return cookie;
}
public boolean equals(Object obj) {
if (obj instanceof HttpCookie) {
return this.cookie.equals(obj);
} else if (obj instanceof SerializableHttpCookie) {
return this.cookie.equals(((SerializableHttpCookie) obj).cookie);
}
return false;
}
public int hashCode() {
return cookie.hashCode();
}
// Workaround httpOnly (getter)
private boolean getHttpOnly() {
try {
initFieldHttpOnly();
return (boolean) fieldHttpOnly.get(cookie);
} catch (Exception e) {
// NoSuchFieldException || IllegalAccessException ||
// IllegalArgumentException
Log.w(TAG, e);
}
return false;
}
// Workaround httpOnly (setter)
private void setHttpOnly(boolean httpOnly) {
try {
initFieldHttpOnly();
fieldHttpOnly.set(cookie, httpOnly);
} catch (Exception e) {
// NoSuchFieldException || IllegalAccessException ||
// IllegalArgumentException
Log.w(TAG, e);
}
}
private void initFieldHttpOnly() throws NoSuchFieldException {
fieldHttpOnly = cookie.getClass().getDeclaredField("httpOnly");
fieldHttpOnly.setAccessible(true);
}
String encode() {
ByteArrayOutputStream os = new ByteArrayOutputStream();
try {
ObjectOutputStream outputStream = new ObjectOutputStream(os);
outputStream.writeObject(this);
} catch (IOException e) {
Log.d(TAG, "IOException in encodeCookie", e);
return null;
}
return byteArrayToHexString(os.toByteArray());
}
static SerializableHttpCookie decode(String encodedCookie) {
byte[] bytes = hexStringToByteArray(encodedCookie);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
try {
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
return (SerializableHttpCookie)objectInputStream.readObject();
} catch (IOException e) {
Log.d(TAG, "IOException in decodeCookie", e);
} catch (ClassNotFoundException e) {
Log.d(TAG, "ClassNotFoundException in decodeCookie", e);
}
return null;
}
private void writeObject(ObjectOutputStream out) throws IOException {
out.writeObject(cookie.getName());
out.writeObject(cookie.getValue());
out.writeObject(cookie.getComment());
out.writeObject(cookie.getCommentURL());
out.writeObject(cookie.getDomain());
out.writeLong(cookie.getMaxAge());
out.writeObject(cookie.getPath());
out.writeObject(cookie.getPortlist());
out.writeInt(cookie.getVersion());
out.writeBoolean(cookie.getSecure());
out.writeBoolean(cookie.getDiscard());
out.writeBoolean(getHttpOnly());
out.writeLong(whenCreated);
}
private void readObject(ObjectInputStream in) throws IOException,
ClassNotFoundException {
String name = (String) in.readObject();
String value = (String) in.readObject();
cookie = new HttpCookie(name, value);
cookie.setComment((String) in.readObject());
cookie.setCommentURL((String) in.readObject());
cookie.setDomain((String) in.readObject());
cookie.setMaxAge(in.readLong());
cookie.setPath((String) in.readObject());
cookie.setPortlist((String) in.readObject());
cookie.setVersion(in.readInt());
cookie.setSecure(in.readBoolean());
cookie.setDiscard(in.readBoolean());
setHttpOnly(in.readBoolean());
whenCreated = in.readLong();
}
/**
* Using some super basic byte array &lt;-&gt; hex conversions so we don't
* have to rely on any large Base64 libraries. Can be overridden if you
* like!
*
* @param bytes byte array to be converted
* @return string containing hex values
*/
private String byteArrayToHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder(bytes.length * 2);
for (byte element : bytes) {
int v = element & 0xff;
if (v < 16) {
sb.append('0');
}
sb.append(Integer.toHexString(v));
}
return sb.toString();
}
/**
* Converts hex values from strings to byte array
*
* @param hexString string of hex-encoded values
* @return decoded byte array
*/
private static byte[] hexStringToByteArray(String hexString) {
int len = hexString.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4) + Character
.digit(hexString.charAt(i + 1), 16));
}
return data;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment