Skip to content

Instantly share code, notes, and snippets.

@franmontiel
Last active April 1, 2024 05:40
Show Gist options
  • Save franmontiel/ed12a2295566b7076161 to your computer and use it in GitHub Desktop.
Save franmontiel/ed12a2295566b7076161 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
*
* 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.
*/
public 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 SharedPreferences sharedPreferences;
// In memory
private Map<URI, Set<HttpCookie>> allCookies;
public PersistentCookieStore(Context context) {
sharedPreferences = context.getSharedPreferences(SP_COOKIE_STORE,
Context.MODE_PRIVATE);
loadAllFromPersistence();
}
private void loadAllFromPersistence() {
allCookies = new HashMap<URI, Set<HttpCookie>>();
Map<String, ?> allPairs = sharedPreferences.getAll();
for (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();
HttpCookie cookie = new SerializableHttpCookie()
.decode(encodedCookie);
Set<HttpCookie> targetCookies = allCookies.get(uri);
if (targetCookies == null) {
targetCookies = new HashSet<HttpCookie>();
allCookies.put(uri, targetCookies);
}
// Repeated cookies cannot exist in persistence
// targetCookies.remove(cookie)
targetCookies.add(cookie);
} catch (URISyntaxException e) {
Log.w(TAG, e);
}
}
}
@Override
public synchronized void add(URI uri, HttpCookie cookie) {
uri = cookieUri(uri, cookie);
Set<HttpCookie> targetCookies = allCookies.get(uri);
if (targetCookies == null) {
targetCookies = new HashSet<HttpCookie>();
allCookies.put(uri, targetCookies);
}
targetCookies.remove(cookie);
targetCookies.add(cookie);
saveToPersistence(uri, cookie);
}
/**
* 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
* @param cookie
* @return
*/
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, HttpCookie cookie) {
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString(uri.toString() + SP_KEY_DELIMITER + cookie.getName(),
new SerializableHttpCookie().encode(cookie));
editor.apply();
}
@Override
public synchronized List<HttpCookie> get(URI uri) {
return getValidCookies(uri);
}
@Override
public synchronized List<HttpCookie> getCookies() {
List<HttpCookie> allValidCookies = new ArrayList<HttpCookie>();
for (URI storedUri : allCookies.keySet()) {
allValidCookies.addAll(getValidCookies(storedUri));
}
return allValidCookies;
}
private List<HttpCookie> getValidCookies(URI uri) {
List<HttpCookie> targetCookies = new ArrayList<HttpCookie>();
// 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));
}
}
}
// Check it there are expired cookies and remove them
if (!targetCookies.isEmpty()) {
List<HttpCookie> cookiesToRemoveFromPersistence = new ArrayList<HttpCookie>();
for (Iterator<HttpCookie> it = targetCookies.iterator(); it
.hasNext(); ) {
HttpCookie currentCookie = it.next();
if (currentCookie.hasExpired()) {
cookiesToRemoveFromPersistence.add(currentCookie);
it.remove();
}
}
if (!cookiesToRemoveFromPersistence.isEmpty()) {
removeFromPersistence(uri, cookiesToRemoveFromPersistence);
}
}
return targetCookies;
}
/* 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<HttpCookie> cookiesToRemove) {
SharedPreferences.Editor editor = sharedPreferences.edit();
for (HttpCookie cookieToRemove : cookiesToRemove) {
editor.remove(uri.toString() + SP_KEY_DELIMITER
+ cookieToRemove.getName());
}
editor.apply();
}
@Override
public synchronized List<URI> getURIs() {
return new ArrayList<URI>(allCookies.keySet());
}
@Override
public synchronized boolean remove(URI uri, HttpCookie cookie) {
Set<HttpCookie> targetCookies = allCookies.get(uri);
boolean cookieRemoved = targetCookies != null && targetCookies
.remove(cookie);
if (cookieRemoved) {
removeFromPersistence(uri, cookie);
}
return cookieRemoved;
}
private void removeFromPersistence(URI uri, HttpCookie cookieToRemove) {
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.remove(uri.toString() + SP_KEY_DELIMITER
+ cookieToRemove.getName());
editor.apply();
}
@Override
public synchronized boolean removeAll() {
allCookies.clear();
removeAllFromPersistence();
return true;
}
private void removeAllFromPersistence() {
sharedPreferences.edit().clear().apply();
}
}
/*
* Copyright (c) 2011 James Smith <james@loopj.com>
* Copyright (c) 2015 Fran Montiel
*
* 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.
*/
/**
* 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
*/
public 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;
public SerializableHttpCookie() {
}
public String encode(HttpCookie cookie) {
this.cookie = cookie;
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());
}
public HttpCookie decode(String encodedCookie) {
byte[] bytes = hexStringToByteArray(encodedCookie);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(
bytes);
HttpCookie cookie = null;
try {
ObjectInputStream objectInputStream = new ObjectInputStream(
byteArrayInputStream);
cookie = ((SerializableHttpCookie) objectInputStream.readObject()).cookie;
} catch (IOException e) {
Log.d(TAG, "IOException in decodeCookie", e);
} catch (ClassNotFoundException e) {
Log.d(TAG, "ClassNotFoundException in decodeCookie", e);
}
return cookie;
}
// 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);
}
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());
}
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());
}
/**
* 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 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;
}
}
@nuald
Copy link

nuald commented Dec 7, 2016

I've fixed few issues:

Please feel free to grab the changes from my fork: https://gist.github.com/nuald/ad776c9f7f52d3f6865142bda58c6d3f

@hoomanv
Copy link

hoomanv commented Dec 7, 2016

If by HttpCookie you mean java.net.HttpCookie then there is a huge mistake in this code.
The java.net.HttpCookie has a private final field called "whenCreated" that is set at consturction time and is used to calculate hasExpired().
Your code is not serializing that value hence after reloading cookies from the persistence all of them get new extended lifetime.

EDIT: I just saw the comment above by nuald and seems someone else has already detected this bug

EDIT2: I was looking for a way to implement persistent cookie store in standard java api not android. The bug that I explained refers to that of the standard java api.

@torv
Copy link

torv commented Dec 11, 2016

@mrmaffen build fail with yours, i change to below:

-keepnames class * implements java.io.Serializable

-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
!static !transient ;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}

@agent10
Copy link

agent10 commented Dec 19, 2016

FYI, isHttpOnly() and setHttpOnly() methods were added to 24 API version.

@agent10
Copy link

agent10 commented Dec 19, 2016

I found strange situation in getValidCookies() method.
storedUri may match with uri but allCookies.get(storedUri) return null value.
It leads to crash in:

HttpCookie currentCookie = it.next();
if (currentCookie.hasExpired()) {

I have no Proguard option and I set CookieStore only once..

@yeshengwu
Copy link

@agent10
java.lang.NullPointerException: Attempt to invoke virtual method 'boolean java.net.HttpCookie.hasExpired()' on a null object reference
device:NEXUS 4
client code:
in application onCreate:
CookieManager manager = new CookieManager(
new PersistentCookieStore(this),
CookiePolicy.ACCEPT_ALL);
CookieHandler.setDefault(manager);

if I comment SerializableHttpCookie.java npe bug will not appear;
line 127 writeObject method out.writeBoolean(getHttpOnly());
line 144 readObject method setHttpOnly(in.readBoolean());

@ggeetha
Copy link

ggeetha commented Feb 24, 2017

Hi,

While uploading a file using okHttp, facing the following issue. Pls help me to sort it out.
Mentioned the issues:

SerializableHttpCookie: java.lang.NoSuchFieldException: httpOnly
at java.lang.Class.getDeclaredField(Class.java:546)
at in.xxxx.xxxx.SerializableHttpCookie.initFieldHttpOnly(SerializableHttpCookie.java:98)
at in.xxxx.xxxx.SerializableHttpCookie.setHttpOnly(SerializableHttpCookie.java:88)
at in.xxxx.xxxx.SerializableHttpCookie.readObject(SerializableHttpCookie.java:131)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:525)
at java.io.ObjectInputStream.readObjectForClass(ObjectInputStream.java:1357)
at java.io.ObjectInputStream.readHierarchy(ObjectInputStream.java:1269)
at java.io.ObjectInputStream.readNewObject(ObjectInputStream.java:1858)
at java.io.ObjectInputStream.readNonPrimitiveContent(ObjectInputStream.java:787)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:2006)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:1963)
at in.xxxx.xxxx.SerializableHttpCookie.decode(SerializableHttpCookie.java:62)
at in.xxxx.xxxx.PersistentCookieStore.loadAllFromPersistence(PersistentCookieStore.java:53)
at in.xxxx.xxxx.PersistentCookieStore.(PersistentCookieStore.java:39)

System.err: java.lang.NullPointerException
System.err: at in.xxxx.xxxx.PersistentCookieStore.(PersistentCookieStore.java:37)
System.err: at in.xxxx.xxxx.RequestManager.upload(RequestManager.java:204)

@CapnSpellcheck
Copy link

CapnSpellcheck commented Jul 24, 2017

@mrmaffen @torv I also get a build error with these Proguard rules, on the static transient line:
Warning: Exception while processing task java.io.IOException: proguard.ParseException: Expecting java type before ';' in line 32 of file '/Users/julian/AndroidStudioProjects/Twinkle/app/proguard-rules.pro'

It seems like both of you have this line.
Update: I think it's because the real line keeps getting filtered: try quoting it:
!static !transient <fields>;

@TristanWiley
Copy link

Been trying to fix the httpOnly problem on API level 18. Thoughts?

08-11 15:36:11.243 3613-3644/me.shreyasr.chatse W/SerializableHttpCookie: java.lang.NullPointerException
                                                                              at me.shreyasr.chatse.network.cookie.SerializableHttpCookie.getHttpOnly(SerializableHttpCookie.java:100)
                                                                              at me.shreyasr.chatse.network.cookie.SerializableHttpCookie.writeObject(SerializableHttpCookie.java:142)
                                                                              at java.lang.reflect.Method.invokeNative(Native Method)
                                                                              at java.lang.reflect.Method.invoke(Method.java:525)
                                                                              at java.io.ObjectOutputStream.writeHierarchy(ObjectOutputStream.java:1055)
                                                                              at java.io.ObjectOutputStream.writeNewObject(ObjectOutputStream.java:1406)
                                                                              at java.io.ObjectOutputStream.writeObjectInternal(ObjectOutputStream.java:1673)
                                                                              at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:1519)
                                                                              at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:1483)
                                                                              at me.shreyasr.chatse.network.cookie.SerializableHttpCookie.encode(SerializableHttpCookie.java:69)
                                                                              at me.shreyasr.chatse.network.cookie.PersistentCookieStore.saveToPersistence(PersistentCookieStore.java:139)
                                                                              at me.shreyasr.chatse.network.cookie.PersistentCookieStore.add(PersistentCookieStore.java:132)
                                                                              at java.net.CookieManager.put(CookieManager.java:188)
                                                                              at com.squareup.okhttp.internal.http.HttpEngine.receiveHeaders(HttpEngine.java:1054)
                                                                              at com.squareup.okhttp.internal.http.HttpEngine.readResponse(HttpEngine.java:796)
                                                                              at com.squareup.okhttp.Call.getResponse(Call.java:274)
                                                                              at com.squareup.okhttp.Call$ApplicationInterceptorChain.proceed(Call.java:230)
                                                                              at com.squareup.okhttp.Call.getResponseWithInterceptorChain(Call.java:201)
                                                                              at com.squareup.okhttp.Call.execute(Call.java:81)
                                                                              at me.shreyasr.chatse.chat.service.IncomingEventService.loadRoom$app_debug(IncomingEventService.kt:65)
                                                                              at me.shreyasr.chatse.chat.service.IncomingEventServiceBinder.loadRoom(IncomingEventServiceBinder.kt:27)
                                                                              at me.shreyasr.chatse.chat.ChatActivity$rejoinFavoriteRooms$1.invoke(ChatActivity.kt:481)
                                                                              at me.shreyasr.chatse.chat.ChatActivity$rejoinFavoriteRooms$1.invoke(ChatActivity.kt:56)
                                                                              at org.jetbrains.anko.AsyncKt$doAsync$1.invoke(Async.kt:140)
                                                                              at org.jetbrains.anko.AsyncKt$doAsync$1.invoke(Async.kt)
                                                                              at org.jetbrains.anko.AsyncKt$sam$Callable$761a5578.call(Async.kt)
                                                                              at java.util.concurrent.FutureTask.run(FutureTask.java:234)
                                                                              at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:153)
                                                                              at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:267)
                                                                              at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1080)
                                                                              at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:573)
                                                                              at java.lang.Thread.run(Thread.java:841)

@mos1551
Copy link

mos1551 commented May 2, 2020

Hi
This Class In Debug Mode Is true
But When Release App Not Work This Class
This Class Has Error :
HttpCookie.hasExpired() is Null Of object Refrence ????

@saloniamatteo
Copy link

Can anyone tell me how to use this? When I tried to use it, the cookies would not persist application restarts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment