Skip to content
This repository was archived by the owner on Mar 11, 2022. It is now read-only.

Commit 8a4caea

Browse files
committed
Refactored session renewal interceptors
Move more functionality in the CookieBaseInterceptor. Throw more explicit exceptions for auth problems. Do not disable auth methods after exceptions. Prevent multiple simultaneous session renewal attempts.
1 parent f409553 commit 8a4caea

7 files changed

Lines changed: 260 additions & 338 deletions

File tree

CHANGES.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
# Unreleased
2-
- [UPGRADED] Optional OkHttp dependency to version 3.12.2.
32
- [FIXED] Create an array of strings for QueryBuilder.fields() when a single field is provided.
3+
- [IMPROVED] Return exceptions directly from IAM token request failures instead of logging and
4+
deferring the request to the service with no credentials. The exception type is the same, but
5+
the message and cause are more clear and a round trip is avoided.
6+
- [IMPROVED] Prevent multiple session renewal requests happening simultaneously because some auth
7+
types apply limits to the number of requests that can be made.
8+
- [UPGRADED] Optional OkHttp dependency to version 3.12.2.
49

510
# 2.17.0 (2019-05-23)
611
- [NEW] Added `com.cloudant.client.api.model.DbInfo#getDocDelCountLong()` to return

cloudant-client/src/main/java/com/cloudant/client/org/lightcouch/CouchDbClient.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -551,9 +551,10 @@ public HttpConnection execute(HttpConnection connection) {
551551
try {
552552
connection = connection.execute();
553553
} catch (HttpConnectionInterceptorException e) {
554-
CouchDbException exception = new CouchDbException(connection.getConnection()
555-
.getResponseMessage(), connection.getConnection().getResponseCode());
554+
CouchDbException exception;
556555
if (e.deserialize) {
556+
exception = new CouchDbException(connection.getConnection()
557+
.getResponseMessage(), connection.getConnection().getResponseCode());
557558
try {
558559
JsonObject errorResponse = new Gson().fromJson(e.error, JsonObject
559560
.class);
@@ -563,6 +564,7 @@ public HttpConnection execute(HttpConnection connection) {
563564
exception.error = e.error;
564565
}
565566
} else {
567+
exception = new CouchDbException(e.getMessage(), e);
566568
exception.error = e.error;
567569
exception.reason = e.reason;
568570
}

cloudant-http/src/main/java/com/cloudant/http/HttpConnection.java

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright © 2015, 2017 IBM Corp. All rights reserved.
1+
// Copyright © 2015, 2019 IBM Corp. All rights reserved.
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
44
// except in compliance with the License. You may obtain a copy of the License at
@@ -287,7 +287,11 @@ public HttpConnection execute() throws IOException {
287287
.interceptorStates);
288288

289289
for (HttpConnectionRequestInterceptor requestInterceptor : requestInterceptors) {
290-
currentContext = requestInterceptor.interceptRequest(currentContext);
290+
try {
291+
currentContext = requestInterceptor.interceptRequest(currentContext);
292+
} catch (HttpConnectionInterceptorException e) {
293+
throw convertAndThrowInterceptorException(e);
294+
}
291295
}
292296

293297
//set request properties after interceptors, in case the interceptors have added
@@ -340,15 +344,7 @@ public HttpConnection execute() throws IOException {
340344
try {
341345
currentContext = responseInterceptor.interceptResponse(currentContext);
342346
} catch (HttpConnectionInterceptorException e) {
343-
// Sadly the current interceptor API doesn't allow an IOException to be thrown
344-
// so to avoid swallowing them the interceptors need to wrap them in the runtime
345-
// HttpConnectionInterceptorException and we can then unwrap them here.
346-
Throwable cause = e.getCause();
347-
if (cause != null && cause instanceof IOException) {
348-
throw (IOException) cause;
349-
} else {
350-
throw e;
351-
}
347+
throw convertAndThrowInterceptorException(e);
352348
}
353349
}
354350

@@ -370,6 +366,18 @@ public HttpConnection execute() throws IOException {
370366
return this;
371367
}
372368

369+
private HttpConnectionInterceptorException convertAndThrowInterceptorException(HttpConnectionInterceptorException e) throws IOException {
370+
// Sadly the current interceptor API doesn't allow an IOException to be thrown
371+
// so to avoid swallowing them the interceptors need to wrap them in the runtime
372+
// HttpConnectionInterceptorException and we can then unwrap them here.
373+
Throwable cause = e.getCause();
374+
if (cause instanceof IOException) {
375+
throw (IOException) cause;
376+
} else {
377+
return e;
378+
}
379+
}
380+
373381
/**
374382
* <p>
375383
* Return response body data from server as a String.
Lines changed: 58 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright © 2015, 2017 IBM Corp. All rights reserved.
2+
* Copyright © 2015, 2019 IBM Corp. All rights reserved.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
55
* except in compliance with the License. You may obtain a copy of the License at
@@ -14,7 +14,6 @@
1414

1515
package com.cloudant.http.internal.interceptors;
1616

17-
import com.cloudant.http.HttpConnection;
1817
import com.cloudant.http.HttpConnectionInterceptorContext;
1918
import com.cloudant.http.internal.Utils;
2019

@@ -27,20 +26,12 @@
2726
import java.util.logging.Level;
2827

2928
/**
30-
* Adds cookie authentication support to http requests.
31-
*
32-
* It does this by adding the cookie header for CouchDB
33-
* using request interceptor pipeline in {@link HttpConnection}.
34-
*
35-
* If a response has a response code of 401, it will fetch a cookie from
36-
* the server using provided credentials and tell {@link HttpConnection} to reply
37-
* the request by setting {@link HttpConnectionInterceptorContext#replayRequest} property to true.
38-
*
39-
* If the request to get the cookie for use in future request fails with a 401 status code
40-
* (or any status that indicates client error) cookie authentication will not be attempted again.
29+
* Extends the CookieInterceptorBase to provide Apache CouchDB _session cookie support.
4130
*/
4231
public class CookieInterceptor extends CookieInterceptorBase {
4332

33+
private final byte[] auth;
34+
4435
/**
4536
* Constructs a cookie interceptor. Credentials should be supplied not URL encoded, this class
4637
* will perform the necessary URL encoding.
@@ -50,92 +41,72 @@ public class CookieInterceptor extends CookieInterceptorBase {
5041
* @param baseURL The base URL to use when constructing an `_session` request.
5142
*/
5243
public CookieInterceptor(String username, String password, String baseURL) {
53-
super("application/x-www-form-urlencoded", baseURL, "/_session");
44+
// Use form encoding for the user/pass submission
45+
super(baseURL, "/_session", "application/x-www-form-urlencoded");
5446
try {
55-
this.sessionRequestBody = String.format("name=%s&password=%s", URLEncoder.encode(username, "UTF-8"), URLEncoder.encode(password, "UTF-8"))
56-
.getBytes("UTF-8"); ;
47+
this.auth = String.format("name=%s&password=%s", URLEncoder.encode(username, "UTF-8")
48+
, URLEncoder.encode(password, "UTF-8"))
49+
.getBytes("UTF-8");
50+
;
5751
} catch (UnsupportedEncodingException e) {
5852
//all JVMs should support UTF-8, so this should not happen
5953
throw new RuntimeException(e);
6054
}
6155
}
6256

63-
57+
/**
58+
* Returns form encoded credentials to pass to _session.
59+
*
60+
* @param context interceptor context
61+
* @return form encoded credentials body payload
62+
*/
6463
@Override
65-
public HttpConnectionInterceptorContext interceptResponse(HttpConnectionInterceptorContext
66-
context) {
67-
68-
// Check if this interceptor is valid before attempting any kind of renewal
69-
if (shouldAttemptCookieRequest.get()) {
70-
71-
HttpURLConnection connection = context.connection.getConnection();
72-
73-
// If we got a 401 or 403 we might need to renew the cookie
74-
try {
75-
boolean renewCookie = false;
76-
int statusCode = connection.getResponseCode();
77-
78-
if (statusCode == HttpURLConnection.HTTP_FORBIDDEN || statusCode ==
79-
HttpURLConnection.HTTP_UNAUTHORIZED) {
80-
// Get the string value of the error stream
81-
InputStream errorStream = connection.getErrorStream();
82-
String errorString = null;
83-
if (errorStream != null) {
84-
errorString = Utils.collectAndCloseStream(connection
85-
.getErrorStream());
86-
logger.log(Level.FINE, String.format(Locale.ENGLISH, "Intercepted " +
87-
"response %d %s", statusCode, errorString));
88-
}
89-
switch (statusCode) {
90-
case HttpURLConnection.HTTP_FORBIDDEN: //403
91-
// Check if it was an expiry case
92-
// Check using a regex to avoid dependency on a JSON library.
93-
// Note (?siu) flags used for . to also match line breaks and for
94-
// unicode
95-
// case insensitivity.
96-
if (errorString != null && errorString.matches("(?siu)" +
97-
".*\\\"error\\\"\\s*:\\s*\\\"credentials_expired\\\".*")) {
98-
// Was expired - set boolean to renew cookie
99-
renewCookie = true;
100-
} else {
101-
// Wasn't a credentials expired, throw exception
102-
HttpConnectionInterceptorException toThrow = new
103-
HttpConnectionInterceptorException(errorString);
104-
// Set the flag for deserialization
105-
toThrow.deserialize = errorString != null;
106-
throw toThrow;
107-
}
108-
break;
109-
case HttpURLConnection.HTTP_UNAUTHORIZED: //401
110-
// We need to get a new cookie
111-
renewCookie = true;
112-
break;
113-
default:
114-
break;
115-
}
64+
protected byte[] getSessionRequestPayload(HttpConnectionInterceptorContext context) {
65+
return auth;
66+
}
11667

117-
if (renewCookie) {
118-
logger.finest("Cookie was invalid. Will attempt to get new cookie.");
119-
boolean success = requestCookie(context);
120-
if (success) {
121-
// New cookie obtained, replay the request
122-
context.replayRequest = true;
123-
} else {
124-
// Didn't successfully renew, maybe creds are invalid
125-
context.replayRequest = false; // Don't replay
126-
shouldAttemptCookieRequest.set(false); // Set the flag to stop trying
127-
}
128-
}
68+
/**
69+
* Adds an additional check for HTTP 403 status codes with "credentials expired" messages that
70+
* are returned by some Cloudant versions.
71+
*
72+
* @param connection the connection to interrogate
73+
* @param statusCode the HTTP response status code
74+
* @return
75+
*/
76+
@Override
77+
protected boolean shouldRenew(HttpURLConnection connection, int statusCode) {
78+
try {
79+
if (statusCode == HttpURLConnection.HTTP_FORBIDDEN) {
80+
// Get the string value of the error stream
81+
InputStream errorStream = connection.getErrorStream();
82+
String errorString = null;
83+
if (errorStream != null) {
84+
errorString = Utils.collectAndCloseStream(connection
85+
.getErrorStream());
86+
logger.log(Level.FINE, String.format(Locale.ENGLISH, "Intercepted " +
87+
"response %d %s", statusCode, errorString));
88+
}
89+
// Check if it was an expiry case
90+
// Check using a regex to avoid dependency on a JSON library.
91+
// Note (?siu) flags used for . to also match line breaks and for
92+
// unicode
93+
// case insensitivity.
94+
if (errorString != null && errorString.matches("(?siu)" +
95+
".*\\\"error\\\"\\s*:\\s*\\\"credentials_expired\\\".*")) {
96+
// Was expired - renew cookie
97+
return true;
12998
} else {
130-
// Store any cookies provided on the response
131-
storeCookiesFromResponse(connection);
99+
// Wasn't a credentials expired, throw exception
100+
HttpConnectionInterceptorException toThrow = new
101+
HttpConnectionInterceptorException(errorString, null);
102+
// Set the flag for deserialization
103+
toThrow.deserialize = errorString != null;
104+
throw toThrow;
132105
}
133-
} catch (IOException e) {
134-
logger.log(Level.SEVERE, "Error reading response code or body from request", e);
135106
}
107+
} catch (IOException e) {
108+
throw wrapIOException("Failed to read HTTP reponse code or body from", connection, e);
136109
}
137-
return context;
138-
110+
return false;
139111
}
140-
141112
}

0 commit comments

Comments
 (0)