beginning work of adding limitation and validation framework to http parameters, playing with old oauth code

This commit is contained in:
Jörg Prante 2019-01-17 15:42:07 +01:00
parent 412b6eaeb5
commit 3c00b77d98
41 changed files with 2385 additions and 46 deletions

View file

@ -2,6 +2,7 @@
plugins { plugins {
id "org.sonarqube" version "2.6.1" id "org.sonarqube" version "2.6.1"
id "io.codearte.nexus-staging" version "0.11.0" id "io.codearte.nexus-staging" version "0.11.0"
id "com.github.spotbugs" version "1.6.9"
id "org.xbib.gradle.plugin.asciidoctor" version "1.5.6.0.1" id "org.xbib.gradle.plugin.asciidoctor" version "1.5.6.0.1"
} }
@ -25,7 +26,7 @@ subprojects {
apply plugin: 'maven' apply plugin: 'maven'
apply plugin: 'signing' apply plugin: 'signing'
apply plugin: 'checkstyle' apply plugin: 'checkstyle'
apply plugin: 'findbugs' apply plugin: 'com.github.spotbugs'
apply plugin: 'pmd' apply plugin: 'pmd'
apply plugin: 'org.xbib.gradle.plugin.asciidoctor' apply plugin: 'org.xbib.gradle.plugin.asciidoctor'
@ -91,7 +92,7 @@ subprojects {
javadoc { javadoc {
options.docletpath = configurations.asciidoclet.files.asType(List) options.docletpath = configurations.asciidoclet.files.asType(List)
options.doclet = 'org.asciidoctor.Asciidoclet' options.doclet = 'org.asciidoctor.Asciidoclet'
options.overview = "src/docs/asciidoclet/overview.adoc" //options.overview = "src/docs/asciidoclet/overview.adoc"
options.addStringOption "-base-dir", "${projectDir}" options.addStringOption "-base-dir", "${projectDir}"
options.addStringOption "-attribute", options.addStringOption "-attribute",
"name=${project.name},version=${project.version},title-link=https://github.com/xbib/${project.name}" "name=${project.name},version=${project.version},title-link=https://github.com/xbib/${project.name}"
@ -120,7 +121,52 @@ subprojects {
} }
apply from: "${rootProject.projectDir}/gradle/publish.gradle" apply from: "${rootProject.projectDir}/gradle/publish.gradle"
apply from: "${rootProject.projectDir}/gradle/qa.gradle"
tasks.withType(Checkstyle) {
ignoreFailures = true
reports {
xml.enabled = true
html.enabled = true
}
}
tasks.withType(Pmd) {
ignoreFailures = true
reports {
xml.enabled = true
html.enabled = true
}
}
checkstyle {
configFile = rootProject.file('config/checkstyle/checkstyle.xml')
ignoreFailures = true
showViolations = true
}
sonarqube {
properties {
property "sonar.projectName", "${project.group} ${project.name}"
property "sonar.sourceEncoding", "UTF-8"
property "sonar.tests", "src/test/java"
property "sonar.scm.provider", "git"
property "sonar.junit.reportsPath", "build/test-results/test/"
}
}
spotbugs {
effort = "max"
reportLevel = "low"
//includeFilter = file("findbugs-exclude.xml")
}
tasks.withType(com.github.spotbugs.SpotBugsTask) {
ignoreFailures = true
reports {
xml.enabled = false
html.enabled = true
}
}
} }
nexusStaging { nexusStaging {

View file

@ -1,8 +1,10 @@
group = org.xbib group = org.xbib
name = net name = net
version = 1.1.3 version = 1.2.0
jackson.version = 2.8.11 jackson.version = 2.8.11
junit.version = 4.12 junit.version = 4.12
wagon.version = 3.0.0 wagon.version = 3.0.0
asciidoclet.version = 1.5.4 asciidoclet.version = 1.5.4
org.gradle.warning.mode=all

View file

@ -1,37 +0,0 @@
tasks.withType(Checkstyle) {
ignoreFailures = true
reports {
xml.enabled = true
html.enabled = true
}
}
tasks.withType(FindBugs) {
ignoreFailures = true
reports {
xml.enabled = false
html.enabled = true
}
}
tasks.withType(Pmd) {
ignoreFailures = true
reports {
xml.enabled = true
html.enabled = true
}
}
checkstyle {
configFile = rootProject.file('config/checkstyle/checkstyle.xml')
ignoreFailures = true
showViolations = true
}
sonarqube {
properties {
property "sonar.projectName", "${project.group} ${project.name}"
property "sonar.sourceEncoding", "UTF-8"
property "sonar.tests", "src/test/java"
property "sonar.scm.provider", "git"
property "sonar.junit.reportsPath", "build/test-results/test/"
}
}

Binary file not shown.

View file

@ -1,6 +1,6 @@
#Fri Sep 14 16:19:33 CEST 2018 #Thu Jan 17 15:23:55 CET 2019
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.9-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-5.1-all.zip

2
gradlew vendored
View file

@ -28,7 +28,7 @@ APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"` APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS="" DEFAULT_JVM_OPTS='"-Xmx64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum" MAX_FD="maximum"

2
gradlew.bat vendored
View file

@ -14,7 +14,7 @@ set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME% set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS= set DEFAULT_JVM_OPTS="-Xmx64m"
@rem Find java.exe @rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome if defined JAVA_HOME goto findJavaFromJavaHome

3
net-http/build.gradle Normal file
View file

@ -0,0 +1,3 @@
dependencies {
compile project(':net-url')
}

View file

@ -0,0 +1,298 @@
package org.xbib.net.http;
import org.xbib.net.PercentDecoder;
import org.xbib.net.PercentEncoder;
import org.xbib.net.PercentEncoders;
import org.xbib.net.http.util.LimitedSortedStringSet;
import org.xbib.net.http.util.LimitedStringMap;
import java.nio.charset.MalformedInputException;
import java.nio.charset.StandardCharsets;
import java.nio.charset.UnmappableCharacterException;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
/**
* A limited multi-map of HTTP request parameters. Each key references a
* limited set of parameters collected from the request during message
* signing. Parameter values are sorted as per
* <a href="http://oauth.net/core/1.0a/#anchor13">OAuth specification</a></a>.
* Every key/value pair will be percent-encoded upon insertion.
* This class has special semantics tailored to
* being useful for message signing; it's not a general purpose collection class
* to handle request parameters.
*/
public class HttpParameters implements Map<String, SortedSet<String>> {
private final LimitedStringMap wrappedMap;
private final PercentEncoder percentEncoder;
private final PercentDecoder percentDecoder;
public HttpParameters() {
this.wrappedMap = new LimitedStringMap();
this.percentEncoder = PercentEncoders.getQueryEncoder(StandardCharsets.UTF_8);
this.percentDecoder = new PercentDecoder();
}
@Override
public SortedSet<String> put(String key, SortedSet<String> value) {
return wrappedMap.put(key, value);
}
@Override
public SortedSet<String> get(Object key) {
return wrappedMap.get(key);
}
@Override
public void putAll(Map<? extends String, ? extends SortedSet<String>> m) {
wrappedMap.putAll(m);
}
@Override
public boolean containsKey(Object key) {
return wrappedMap.containsKey(key);
}
@Override
public boolean containsValue(Object value) {
if (value instanceof String) {
for (Set<String> values : wrappedMap.values()) {
if (values.contains(value)) {
return true;
}
}
}
return false;
}
@Override
public int size() {
int count = 0;
for (String key : wrappedMap.keySet()) {
count += wrappedMap.get(key).size();
}
return count;
}
@Override
public boolean isEmpty() {
return wrappedMap.isEmpty();
}
@Override
public void clear() {
wrappedMap.clear();
}
@Override
public SortedSet<String> remove(Object key) {
return wrappedMap.remove(key);
}
@Override
public Set<String> keySet() {
return wrappedMap.keySet();
}
@Override
public Collection<SortedSet<String>> values() {
return wrappedMap.values();
}
@Override
public Set<Entry<String, SortedSet<String>>> entrySet() {
return wrappedMap.entrySet();
}
public SortedSet<String> put(String key, SortedSet<String> values, boolean percentEncode)
throws MalformedInputException, UnmappableCharacterException {
if (percentEncode) {
remove(key);
for (String v : values) {
put(key, v, true);
}
return get(key);
} else {
return wrappedMap.put(key, values);
}
}
/**
* Convenience method to add a single value for the parameter specified by 'key'.
*
* @param key the parameter name
* @param value the parameter value
* @return the value
*/
public String put(String key, String value)
throws MalformedInputException, UnmappableCharacterException {
return put(key, value, false);
}
/**
* Convenience method to add a single value for the parameter specified by
* 'key'.
*
* @param key the parameter name
* @param value the parameter value
* @param percentEncode whether key and value should be percent encoded before being
* inserted into the map
* @return the value
*/
public String put(String key, String value, boolean percentEncode)
throws MalformedInputException, UnmappableCharacterException {
String k = percentEncode ? percentEncoder.encode(key) : key;
SortedSet<String> values = wrappedMap.get(k);
if (values == null) {
values = new LimitedSortedStringSet();
wrappedMap.put(k, values);
}
String v = null;
if (value != null) {
v = percentEncode ? percentEncoder.encode(value) : value;
values.add(v);
}
return v;
}
/**
* Convenience method to allow for storing null values. {@link #put} doesn't
* allow null values, because that would be ambiguous.
*
* @param key the parameter name
* @param nullString can be anything, but probably... null?
* @return null
*/
public String putNull(String key, String nullString)
throws MalformedInputException, UnmappableCharacterException {
return put(key, nullString);
}
public void putAll(Map<? extends String, ? extends SortedSet<String>> m, boolean percentEncode)
throws MalformedInputException, UnmappableCharacterException {
if (percentEncode) {
for (String key : m.keySet()) {
put(key, m.get(key), true);
}
} else {
wrappedMap.putAll(m);
}
}
public void putAll(String[] keyValuePairs, boolean percentEncode)
throws MalformedInputException, UnmappableCharacterException {
for (int i = 0; i < keyValuePairs.length - 1; i += 2) {
this.put(keyValuePairs[i], keyValuePairs[i + 1], percentEncode);
}
}
/**
* Convenience method to merge a Map<String, List<String>>.
*
* @param m the map
*/
public void putMap(Map<String, List<String>> m) {
for (String key : m.keySet()) {
SortedSet<String> vals = get(key);
if (vals == null) {
vals = new LimitedSortedStringSet();
put(key, vals);
}
vals.addAll(m.get(key));
}
}
public String getFirst(String key)
throws MalformedInputException, UnmappableCharacterException {
return getFirst(key, false);
}
/**
* Returns the first value from the set of all values for the given
* parameter name. If the key passed to this method contains special
* characters, you must first percent encode it, otherwise the lookup will fail
* (that's because upon storing values in this map, keys get
* percent-encoded).
*
* @param key the parameter name (must be percent encoded if it contains unsafe
* characters!)
* @param percentDecode whether the value being retrieved should be percent decoded
* @return the first value found for this parameter
*/
public String getFirst(String key, boolean percentDecode)
throws MalformedInputException, UnmappableCharacterException {
SortedSet<String> values = wrappedMap.get(key);
if (values == null || values.isEmpty()) {
return null;
}
String value = values.first();
return percentDecode ? percentDecoder.decode(value) : value;
}
/**
* Concatenates all values for the given key to a list of key/value pairs
* suitable for use in a URL query string.
*
* @param key the parameter name
* @return the query string
*/
public String getAsQueryString(String key)
throws MalformedInputException, UnmappableCharacterException {
return getAsQueryString(key, true);
}
/**
* Concatenates all values for the given key to a list of key/value pairs
* suitable for use in a URL query string.
*
* @param key the parameter name
* @param percentEncode whether key should be percent encoded before being
* used with the map
* @return the query string
*/
public String getAsQueryString(String key, boolean percentEncode)
throws MalformedInputException, UnmappableCharacterException {
String k = percentEncode ? percentEncoder.encode(key) : key;
SortedSet<String> values = wrappedMap.get(k);
if (values == null) {
return k + "=";
}
Iterator<String> it = values.iterator();
StringBuilder sb = new StringBuilder();
while (it.hasNext()) {
sb.append(k).append("=").append(it.next());
if (it.hasNext()) {
sb.append("&");
}
}
return sb.toString();
}
public String getAsHeaderElement(String key)
throws MalformedInputException, UnmappableCharacterException {
String value = getFirst(key);
if (value == null) {
return null;
}
return key + "=\"" + value + "\"";
}
public HttpParameters getOAuthParameters() {
HttpParameters oauthParams = new HttpParameters();
for (Entry<String, SortedSet<String>> param : this.entrySet()) {
String key = param.getKey();
if (key.startsWith("oauth_") || key.startsWith("x_oauth_")) {
oauthParams.put(key, param.getValue());
}
}
return oauthParams;
}
}

View file

@ -0,0 +1,30 @@
package org.xbib.net.http;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
/**
* A representation of an HTTP request. Contains methods to access all
* those parts of an HTTP request.
*/
public interface HttpRequest {
String getMethod();
String getRequestUrl();
void setRequestUrl(String url);
void setHeader(String name, String value);
String getHeader(String name);
Map<String, String> getAllHeaders();
InputStream getMessagePayload() throws IOException;
String getContentType();
Object unwrap();
}

View file

@ -0,0 +1,15 @@
package org.xbib.net.http;
import java.io.IOException;
import java.io.InputStream;
public interface HttpResponse {
int getStatusCode() throws IOException;
String getReasonPhrase() throws Exception;
InputStream getContent() throws IOException;
Object unwrap();
}

View file

@ -0,0 +1,49 @@
package org.xbib.net.http;
import java.io.InputStream;
import java.util.Collections;
import java.util.Map;
public class UrlStringRequestAdapter implements HttpRequest {
private String url;
public UrlStringRequestAdapter(String url) {
this.url = url;
}
public String getMethod() {
return "GET";
}
public String getRequestUrl() {
return url;
}
public void setRequestUrl(String url) {
this.url = url;
}
public void setHeader(String name, String value) {
}
public String getHeader(String name) {
return null;
}
public Map<String, String> getAllHeaders() {
return Collections.emptyMap();
}
public InputStream getMessagePayload() {
return null;
}
public String getContentType() {
return null;
}
public Object unwrap() {
return url;
}
}

View file

@ -0,0 +1,28 @@
package org.xbib.net.http.util;
import java.util.SortedSet;
import java.util.TreeSet;
public class LimitedSortedStringSet extends TreeSet<String> implements SortedSet<String> {
private final int sizeLimit;
private final int elementSizeLimit;
public LimitedSortedStringSet() {
this(1024, 65536);
}
public LimitedSortedStringSet(int sizeLimit, int elementSizeLimit) {
this.sizeLimit = sizeLimit;
this.elementSizeLimit = elementSizeLimit;
}
@Override
public boolean add(String string) {
if (size() < sizeLimit && string.length() <= elementSizeLimit ) {
return super.add(string);
}
return false;
}
}

View file

@ -0,0 +1,25 @@
package org.xbib.net.http.util;
import java.util.SortedSet;
import java.util.TreeMap;
public class LimitedStringMap extends TreeMap<String, SortedSet<String>> {
private final int limit;
public LimitedStringMap() {
this(1024);
}
public LimitedStringMap(int limit) {
this.limit = limit;
}
@Override
public SortedSet<String> put(String key, SortedSet<String> value) {
if (size() < limit) {
return super.put(key, value);
}
return null;
}
}

3
net-oauth/build.gradle Normal file
View file

@ -0,0 +1,3 @@
dependencies {
compile project(':net-http')
}

View file

@ -0,0 +1,237 @@
package org.xbib.net.oauth;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.MalformedInputException;
import java.nio.charset.UnmappableCharacterException;
import java.security.SecureRandom;
import java.util.Random;
import org.xbib.net.http.HttpParameters;
import org.xbib.net.http.HttpRequest;
import org.xbib.net.http.UrlStringRequestAdapter;
import org.xbib.net.oauth.sign.AuthorizationHeaderSigningStrategy;
import org.xbib.net.oauth.sign.HmacSha1MessageSigner;
import org.xbib.net.oauth.sign.OAuthMessageSigner;
import org.xbib.net.oauth.sign.QueryStringSigningStrategy;
import org.xbib.net.oauth.sign.SigningStrategy;
/**
* ABC for consumer implementations. If you're developing a custom consumer you
* will probably inherit from this class to save you a lot of work.
*
*/
public abstract class AbstractOAuthConsumer implements OAuthConsumer {
private String consumerKey, consumerSecret;
private String token;
private OAuthMessageSigner messageSigner;
private SigningStrategy signingStrategy;
// these are params that may be passed to the consumer directly (i.e.
// without going through the request object)
private HttpParameters additionalParameters;
// these are the params which will be passed to the message signer
private HttpParameters requestParameters;
private boolean sendEmptyTokens;
private final Random random = new SecureRandom();
public AbstractOAuthConsumer(String consumerKey, String consumerSecret) {
this.consumerKey = consumerKey;
this.consumerSecret = consumerSecret;
setMessageSigner(new HmacSha1MessageSigner());
setSigningStrategy(new AuthorizationHeaderSigningStrategy());
}
public void setMessageSigner(OAuthMessageSigner messageSigner) {
this.messageSigner = messageSigner;
messageSigner.setConsumerSecret(consumerSecret);
}
public void setSigningStrategy(SigningStrategy signingStrategy) {
this.signingStrategy = signingStrategy;
}
public void setAdditionalParameters(HttpParameters additionalParameters) {
this.additionalParameters = additionalParameters;
}
public synchronized HttpRequest sign(HttpRequest request) throws OAuthMessageSignerException,
OAuthExpectationFailedException, OAuthCommunicationException {
if (consumerKey == null) {
throw new OAuthExpectationFailedException("consumer key not set");
}
if (consumerSecret == null) {
throw new OAuthExpectationFailedException("consumer secret not set");
}
requestParameters = new HttpParameters();
try {
if (additionalParameters != null) {
requestParameters.putAll(additionalParameters, false);
}
collectHeaderParameters(request, requestParameters);
collectQueryParameters(request, requestParameters);
collectBodyParameters(request, requestParameters);
// add any OAuth params that haven't already been set
completeOAuthParameters(requestParameters);
requestParameters.remove(OAuth.OAUTH_SIGNATURE);
} catch (IOException e) {
throw new OAuthCommunicationException(e);
}
String signature = messageSigner.sign(request, requestParameters);
try {
signingStrategy.writeSignature(signature, request, requestParameters);
} catch (MalformedInputException | UnmappableCharacterException e) {
throw new OAuthMessageSignerException(e);
}
return request;
}
public synchronized HttpRequest sign(Object request) throws OAuthMessageSignerException,
OAuthExpectationFailedException, OAuthCommunicationException {
return sign(wrap(request));
}
public synchronized String sign(String url) throws OAuthMessageSignerException,
OAuthExpectationFailedException, OAuthCommunicationException {
HttpRequest request = new UrlStringRequestAdapter(url);
// switch to URL signing
SigningStrategy oldStrategy = this.signingStrategy;
this.signingStrategy = new QueryStringSigningStrategy();
sign(request);
// revert to old strategy
this.signingStrategy = oldStrategy;
return request.getRequestUrl();
}
/**
* Adapts the given request object to a Signpost {@link HttpRequest}. How
* this is done depends on the consumer implementation.
*
* @param request
* the native HTTP request instance
* @return the adapted request
*/
protected abstract HttpRequest wrap(Object request);
public void setTokenWithSecret(String token, String tokenSecret) {
this.token = token;
messageSigner.setTokenSecret(tokenSecret);
}
public String getToken() {
return token;
}
public String getTokenSecret() {
return messageSigner.getTokenSecret();
}
public String getConsumerKey() {
return this.consumerKey;
}
public String getConsumerSecret() {
return this.consumerSecret;
}
/**
* <p>
* Helper method that adds any OAuth parameters to the given request
* parameters which are missing from the current request but required for
* signing. A good example is the oauth_nonce parameter, which is typically
* not provided by the client in advance.
* </p>
* <p>
* It's probably not a very good idea to override this method. If you want
* to generate different nonces or timestamps, override
* {@link #generateNonce()} or {@link #generateTimestamp()} instead.
* </p>
*
* @param out
* the request parameter which should be completed
*/
protected void completeOAuthParameters(HttpParameters out)
throws MalformedInputException, UnmappableCharacterException {
if (!out.containsKey(OAuth.OAUTH_CONSUMER_KEY)) {
out.put(OAuth.OAUTH_CONSUMER_KEY, consumerKey, true);
}
if (!out.containsKey(OAuth.OAUTH_SIGNATURE_METHOD)) {
out.put(OAuth.OAUTH_SIGNATURE_METHOD, messageSigner.getSignatureMethod(), true);
}
if (!out.containsKey(OAuth.OAUTH_TIMESTAMP)) {
out.put(OAuth.OAUTH_TIMESTAMP, generateTimestamp(), true);
}
if (!out.containsKey(OAuth.OAUTH_NONCE)) {
out.put(OAuth.OAUTH_NONCE, generateNonce(), true);
}
if (!out.containsKey(OAuth.OAUTH_VERSION)) {
out.put(OAuth.OAUTH_VERSION, OAuth.VERSION_1_0, true);
}
if (!out.containsKey(OAuth.OAUTH_TOKEN)) {
if (token != null && !token.equals("") || sendEmptyTokens) {
out.put(OAuth.OAUTH_TOKEN, token, true);
}
}
}
public HttpParameters getRequestParameters() {
return requestParameters;
}
public void setSendEmptyTokens(boolean enable) {
this.sendEmptyTokens = enable;
}
/**
* Collects OAuth Authorization header parameters as per OAuth Core 1.0 spec
* section 9.1.1
*/
protected void collectHeaderParameters(HttpRequest request, HttpParameters out)
throws MalformedInputException, UnmappableCharacterException {
HttpParameters headerParams = OAuth.oauthHeaderToParamsMap(request.getHeader(OAuth.HTTP_AUTHORIZATION_HEADER));
out.putAll(headerParams, false);
}
/**
* Collects x-www-form-urlencoded body parameters as per OAuth Core 1.0 spec
* section 9.1.1
*/
protected void collectBodyParameters(HttpRequest request, HttpParameters out)
throws IOException {
// collect x-www-form-urlencoded body params
String contentType = request.getContentType();
if (contentType != null && contentType.startsWith(OAuth.FORM_ENCODED)) {
InputStream payload = request.getMessagePayload();
out.putAll(OAuth.decodeForm(payload), true);
}
}
/**
* Collects HTTP GET query string parameters as per OAuth Core 1.0 spec
* section 9.1.1
*/
protected void collectQueryParameters(HttpRequest request, HttpParameters out)
throws MalformedInputException, UnmappableCharacterException {
String url = request.getRequestUrl();
int q = url.indexOf('?');
if (q >= 0) {
// Combine the URL query string with the other parameters:
out.putAll(OAuth.decodeForm(url.substring(q + 1)), true);
}
}
protected String generateTimestamp() {
return Long.toString(System.currentTimeMillis() / 1000L);
}
protected String generateNonce() {
return Long.toString(random.nextLong());
}
}

View file

@ -0,0 +1,299 @@
package org.xbib.net.oauth;
import org.xbib.net.http.HttpParameters;
import org.xbib.net.http.HttpRequest;
import org.xbib.net.http.HttpResponse;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.MalformedInputException;
import java.nio.charset.UnmappableCharacterException;
import java.util.HashMap;
import java.util.Map;
/**
* For all provider implementations. If you're writing a custom provider,
* you will probably inherit from this class, since it takes a lot of work from
* you.
*/
public abstract class AbstractOAuthProvider implements OAuthProvider {
private final String requestTokenEndpointUrl;
private final String accessTokenEndpointUrl;
private final String authorizationWebsiteUrl;
private HttpParameters responseParameters;
private Map<String, String> defaultHeaders;
private boolean isOAuth10a;
private OAuthProviderListener listener;
public AbstractOAuthProvider(String requestTokenEndpointUrl, String accessTokenEndpointUrl,
String authorizationWebsiteUrl) {
this.requestTokenEndpointUrl = requestTokenEndpointUrl;
this.accessTokenEndpointUrl = accessTokenEndpointUrl;
this.authorizationWebsiteUrl = authorizationWebsiteUrl;
this.responseParameters = new HttpParameters();
this.defaultHeaders = new HashMap<String, String>();
}
public synchronized String retrieveRequestToken(OAuthConsumer consumer, String callbackUrl,
String... customOAuthParams) throws OAuthMessageSignerException,
OAuthNotAuthorizedException, OAuthExpectationFailedException,
OAuthCommunicationException {
// invalidate current credentials, if any
consumer.setTokenWithSecret(null, null);
// 1.0a expects the callback to be sent while getting the request token.
// 1.0 service providers would simply ignore this parameter.
HttpParameters params = new HttpParameters();
try {
params.putAll(customOAuthParams, true);
params.put(OAuth.OAUTH_CALLBACK, callbackUrl, true);
retrieveToken(consumer, requestTokenEndpointUrl, params);
String callbackConfirmed = responseParameters.getFirst(OAuth.OAUTH_CALLBACK_CONFIRMED);
responseParameters.remove(OAuth.OAUTH_CALLBACK_CONFIRMED);
isOAuth10a = Boolean.TRUE.toString().equals(callbackConfirmed);
// 1.0 service providers expect the callback as part of the auth URL,
// Do not send when 1.0a.
if (isOAuth10a) {
return OAuth.addQueryParameters(authorizationWebsiteUrl, OAuth.OAUTH_TOKEN,
consumer.getToken());
} else {
return OAuth.addQueryParameters(authorizationWebsiteUrl, OAuth.OAUTH_TOKEN,
consumer.getToken(), OAuth.OAUTH_CALLBACK, callbackUrl);
}
} catch (MalformedInputException | UnmappableCharacterException e) {
throw new OAuthMessageSignerException(e);
}
}
public synchronized void retrieveAccessToken(OAuthConsumer consumer, String oauthVerifier,
String... customOAuthParams) throws OAuthMessageSignerException,
OAuthNotAuthorizedException, OAuthExpectationFailedException,
OAuthCommunicationException {
if (consumer.getToken() == null || consumer.getTokenSecret() == null) {
throw new OAuthExpectationFailedException(
"Authorized request token or token secret not set. "
+ "Did you retrieve an authorized request token before?");
}
HttpParameters params = new HttpParameters();
try {
params.putAll(customOAuthParams, true);
if (isOAuth10a && oauthVerifier != null) {
params.put(OAuth.OAUTH_VERIFIER, oauthVerifier, true);
}
} catch (MalformedInputException | UnmappableCharacterException e) {
throw new OAuthMessageSignerException(e);
}
retrieveToken(consumer, accessTokenEndpointUrl, params);
}
/**
* Implemented by subclasses. The responsibility of this method is to
* contact the service provider at the given endpoint URL and fetch a
* request or access token. What kind of token is retrieved solely depends
* on the URL being used.
* Correct implementations of this method must guarantee the following
* post-conditions:
* <ul>
* <li>the {@link OAuthConsumer} passed to this method must have a valid
* {@link OAuth#OAUTH_TOKEN} and {@link OAuth#OAUTH_TOKEN_SECRET} set by
* calling {@link OAuthConsumer#setTokenWithSecret(String, String)}</li>
* <li>{@link #getResponseParameters()} must return the set of query
* parameters served by the service provider in the token response, with all
* OAuth specific parameters being removed</li>
* </ul>
*
* @param consumer the {@link OAuthConsumer} that should be used to sign the request
* @param endpointUrl the URL at which the service provider serves the OAuth token that
* is to be fetched
* @param customOAuthParams you can pass custom OAuth parameters here (such as oauth_callback
* or oauth_verifier) which will go directly into the signer, i.e.
* you don't have to put them into the request first.
* @throws OAuthMessageSignerException if signing the token request fails
* @throws OAuthCommunicationException if a network communication error occurs
* @throws OAuthNotAuthorizedException if the server replies 401 - Unauthorized
* @throws OAuthExpectationFailedException if an expectation has failed, e.g. because the server didn't
* reply in the expected format
*/
protected void retrieveToken(OAuthConsumer consumer, String endpointUrl,
HttpParameters customOAuthParams) throws OAuthMessageSignerException,
OAuthCommunicationException, OAuthNotAuthorizedException,
OAuthExpectationFailedException {
Map<String, String> defaultHeaders = getRequestHeaders();
if (consumer.getConsumerKey() == null || consumer.getConsumerSecret() == null) {
throw new OAuthExpectationFailedException("Consumer key or secret not set");
}
HttpRequest request = null;
HttpResponse response = null;
try {
request = createRequest(endpointUrl);
for (String header : defaultHeaders.keySet()) {
request.setHeader(header, defaultHeaders.get(header));
}
if (customOAuthParams != null && !customOAuthParams.isEmpty()) {
consumer.setAdditionalParameters(customOAuthParams);
}
if (this.listener != null) {
this.listener.prepareRequest(request);
}
consumer.sign(request);
if (this.listener != null) {
this.listener.prepareSubmission(request);
}
response = sendRequest(request);
int statusCode = response.getStatusCode();
boolean requestHandled = false;
if (this.listener != null) {
requestHandled = this.listener.onResponseReceived(request, response);
}
if (requestHandled) {
return;
}
if (statusCode >= 300) {
handleUnexpectedResponse(statusCode, response);
}
HttpParameters responseParams = OAuth.decodeForm(response.getContent());
String token = responseParams.getFirst(OAuth.OAUTH_TOKEN);
String secret = responseParams.getFirst(OAuth.OAUTH_TOKEN_SECRET);
responseParams.remove(OAuth.OAUTH_TOKEN);
responseParams.remove(OAuth.OAUTH_TOKEN_SECRET);
setResponseParameters(responseParams);
if (token == null || secret == null) {
throw new OAuthExpectationFailedException(
"Request token or token secret not set in server reply. "
+ "The service provider you use is probably buggy.");
}
consumer.setTokenWithSecret(token, secret);
} catch (OAuthNotAuthorizedException | OAuthExpectationFailedException e) {
throw e;
} catch (Exception e) {
throw new OAuthCommunicationException(e);
} finally {
try {
closeConnection(request, response);
} catch (Exception e) {
throw new OAuthCommunicationException(e);
}
}
}
protected void handleUnexpectedResponse(int statusCode, HttpResponse response) throws Exception {
if (response == null) {
return;
}
BufferedReader reader = new BufferedReader(new InputStreamReader(response.getContent()));
StringBuilder responseBody = new StringBuilder();
String line = reader.readLine();
while (line != null) {
responseBody.append(line);
line = reader.readLine();
}
if (statusCode == 401) {
throw new OAuthNotAuthorizedException(responseBody.toString());
}
throw new OAuthCommunicationException("Service provider responded in error: "
+ statusCode + " (" + response.getReasonPhrase() + ")", responseBody.toString());
}
/**
* Overrride this method if you want to customize the logic for building a
* request object for the given endpoint URL.
*
* @param endpointUrl
* the URL to which the request will go
* @return the request object
* @throws Exception
* if something breaks
*/
protected abstract HttpRequest createRequest(String endpointUrl) throws Exception;
/**
* Override this method if you want to customize the logic for how the given
* request is sent to the server.
*
* @param request
* the request to send
* @return the response to the request
* @throws Exception
* if something breaks
*/
protected abstract HttpResponse sendRequest(HttpRequest request) throws Exception;
/**
* Called when the connection is being finalized after receiving the
* response. Use this to do any cleanup / resource freeing.
*
* @param request
* the request that has been sent
* @param response
* the response that has been received
* @throws Exception
* if something breaks
*/
protected void closeConnection(HttpRequest request, HttpResponse response) throws Exception {
// NOP
}
public HttpParameters getResponseParameters() {
return responseParameters;
}
/**
* Returns a single query parameter as served by the service provider in a
* token reply. You must call {@link #setResponseParameters} with the set of
* parameters before using this method.
*
* @param key
* the parameter name
* @return the parameter value
*/
protected String getResponseParameter(String key)
throws MalformedInputException, UnmappableCharacterException {
return responseParameters.getFirst(key);
}
public void setResponseParameters(HttpParameters parameters) {
this.responseParameters = parameters;
}
public void setOAuth10a(boolean isOAuth10aProvider) {
this.isOAuth10a = isOAuth10aProvider;
}
public boolean isOAuth10a() {
return isOAuth10a;
}
public String getRequestTokenEndpointUrl() {
return this.requestTokenEndpointUrl;
}
public String getAccessTokenEndpointUrl() {
return this.accessTokenEndpointUrl;
}
public String getAuthorizationWebsiteUrl() {
return this.authorizationWebsiteUrl;
}
public void setRequestHeader(String header, String value) {
defaultHeaders.put(header, value);
}
public Map<String, String> getRequestHeaders() {
return defaultHeaders;
}
public void setListener(OAuthProviderListener listener) {
this.listener = listener;
}
public void removeListener(OAuthProviderListener listener) {
this.listener = null;
}
}

View file

@ -0,0 +1,26 @@
package org.xbib.net.oauth;
import org.xbib.net.http.HttpRequest;
import java.net.HttpURLConnection;
/**
* The default implementation for an OAuth consumer. Only supports signing
* {@link HttpURLConnection} type requests.
*/
public class DefaultOAuthConsumer extends AbstractOAuthConsumer {
public DefaultOAuthConsumer(String consumerKey, String consumerSecret) {
super(consumerKey, consumerSecret);
}
@Override
protected HttpRequest wrap(Object request) {
if (!(request instanceof HttpURLConnection)) {
throw new IllegalArgumentException(
"The default consumer expects requests of type java.net.HttpURLConnection");
}
return new HttpURLConnectionRequestAdapter((HttpURLConnection) request);
}
}

View file

@ -0,0 +1,42 @@
package org.xbib.net.oauth;
import org.xbib.net.http.HttpRequest;
import org.xbib.net.http.HttpResponse;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
/**
* This default implementation uses {@link HttpURLConnection} type GET
* requests to receive tokens from a service provider.
*/
public class DefaultOAuthProvider extends AbstractOAuthProvider {
public DefaultOAuthProvider(String requestTokenEndpointUrl, String accessTokenEndpointUrl,
String authorizationWebsiteUrl) {
super(requestTokenEndpointUrl, accessTokenEndpointUrl, authorizationWebsiteUrl);
}
protected HttpRequest createRequest(String endpointUrl) throws IOException {
HttpURLConnection connection = (HttpURLConnection) new URL(endpointUrl).openConnection();
connection.setRequestMethod("POST");
connection.setAllowUserInteraction(false);
connection.setRequestProperty("Content-Length", "0");
return new HttpURLConnectionRequestAdapter(connection);
}
protected HttpResponse sendRequest(HttpRequest request) throws IOException {
HttpURLConnection connection = (HttpURLConnection) request.unwrap();
connection.connect();
return new HttpURLConnectionResponseAdapter(connection);
}
@Override
protected void closeConnection(HttpRequest request, HttpResponse response) {
HttpURLConnection connection = (HttpURLConnection) request.unwrap();
if (connection != null) {
connection.disconnect();
}
}
}

View file

@ -0,0 +1,61 @@
package org.xbib.net.oauth;
import org.xbib.net.http.HttpRequest;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class HttpURLConnectionRequestAdapter implements HttpRequest {
protected HttpURLConnection connection;
public HttpURLConnectionRequestAdapter(HttpURLConnection connection) {
this.connection = connection;
}
public String getMethod() {
return connection.getRequestMethod();
}
public String getRequestUrl() {
return connection.getURL().toExternalForm();
}
public void setRequestUrl(String url) {
}
public void setHeader(String name, String value) {
connection.setRequestProperty(name, value);
}
public String getHeader(String name) {
return connection.getRequestProperty(name);
}
public Map<String, String> getAllHeaders() {
Map<String, List<String>> origHeaders = connection.getRequestProperties();
Map<String, String> headers = new HashMap<String, String>(origHeaders.size());
for (String name : origHeaders.keySet()) {
List<String> values = origHeaders.get(name);
if (!values.isEmpty()) {
headers.put(name, values.get(0));
}
}
return headers;
}
public InputStream getMessagePayload() {
return null;
}
public String getContentType() {
return connection.getRequestProperty("Content-Type");
}
public HttpURLConnection unwrap() {
return connection;
}
}

View file

@ -0,0 +1,36 @@
package org.xbib.net.oauth;
import org.xbib.net.http.HttpResponse;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
public class HttpURLConnectionResponseAdapter implements HttpResponse {
private HttpURLConnection connection;
public HttpURLConnectionResponseAdapter(HttpURLConnection connection) {
this.connection = connection;
}
public InputStream getContent() {
try {
return connection.getInputStream();
} catch (IOException e) {
return connection.getErrorStream();
}
}
public int getStatusCode() throws IOException {
return connection.getResponseCode();
}
public String getReasonPhrase() throws Exception {
return connection.getResponseMessage();
}
public Object unwrap() {
return connection;
}
}

View file

@ -0,0 +1,285 @@
package org.xbib.net.oauth;
import org.xbib.net.PercentDecoder;
import org.xbib.net.PercentEncoder;
import org.xbib.net.PercentEncoders;
import org.xbib.net.http.HttpParameters;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.nio.charset.MalformedInputException;
import java.nio.charset.StandardCharsets;
import java.nio.charset.UnmappableCharacterException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
public class OAuth {
public static final String VERSION_1_0 = "1.0";
public static final String ENCODING = "UTF-8";
public static final String FORM_ENCODED = "application/x-www-form-urlencoded";
public static final String HTTP_AUTHORIZATION_HEADER = "Authorization";
public static final String OAUTH_CONSUMER_KEY = "oauth_consumer_key";
public static final String OAUTH_TOKEN = "oauth_token";
public static final String OAUTH_TOKEN_SECRET = "oauth_token_secret";
public static final String OAUTH_SIGNATURE_METHOD = "oauth_signature_method";
public static final String OAUTH_SIGNATURE = "oauth_signature";
public static final String OAUTH_TIMESTAMP = "oauth_timestamp";
public static final String OAUTH_NONCE = "oauth_nonce";
public static final String OAUTH_VERSION = "oauth_version";
public static final String OAUTH_CALLBACK = "oauth_callback";
public static final String OAUTH_CALLBACK_CONFIRMED = "oauth_callback_confirmed";
public static final String OAUTH_VERIFIER = "oauth_verifier";
/**
* Pass this value as the callback "url" upon retrieving a request token if
* your application cannot receive callbacks (e.g. because it's a desktop
* app). This will tell the service provider that verification happens
* out-of-band, which basically means that it will generate a PIN code (the
* OAuth verifier) and display that to your user. You must obtain this code
* from your user and pass it to
* {@link OAuthProvider#retrieveAccessToken(OAuthConsumer, String, String...)} in order
* to complete the token handshake.
*/
public static final String OUT_OF_BAND = "oob";
public static final PercentEncoder percentEncoder = PercentEncoders.getQueryEncoder(StandardCharsets.UTF_8);
public static final PercentDecoder percentDecoder = new PercentDecoder();
/**
* Construct a x-www-form-urlencoded document containing the given sequence
* of name/value pairs. Use OAuth percent encoding (not exactly the encoding
* mandated by x-www-form-urlencoded).
*/
public static <T extends Map.Entry<String, String>> void formEncode(Collection<T> parameters,
OutputStream into) throws IOException {
if (parameters != null) {
boolean first = true;
for (Map.Entry<String, String> entry : parameters) {
if (first) {
first = false;
} else {
into.write('&');
}
into.write(percentEncoder.encode(safeToString(entry.getKey())).getBytes());
into.write('=');
into.write(percentEncoder.encode(safeToString(entry.getValue())).getBytes());
}
}
}
/**
* Construct a x-www-form-urlencoded document containing the given sequence
* of name/value pairs. Use OAuth percent encoding (not exactly the encoding
* mandated by x-www-form-urlencoded).
*/
public static <T extends Map.Entry<String, String>> String formEncode(Collection<T> parameters)
throws IOException {
ByteArrayOutputStream b = new ByteArrayOutputStream();
formEncode(parameters, b);
return new String(b.toByteArray());
}
/**
* Parse a form-urlencoded document.
*/
public static HttpParameters decodeForm(String form)
throws MalformedInputException, UnmappableCharacterException {
HttpParameters params = new HttpParameters();
if (isEmpty(form)) {
return params;
}
for (String nvp : form.split("\\&")) {
int equals = nvp.indexOf('=');
String name;
String value;
if (equals < 0) {
name = percentDecoder.decode(nvp);
value = null;
} else {
name = percentDecoder.decode(nvp.substring(0, equals));
value = percentDecoder.decode(nvp.substring(equals + 1));
}
params.put(name, value);
}
return params;
}
public static HttpParameters decodeForm(InputStream content)
throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(
content));
StringBuilder sb = new StringBuilder();
String line = reader.readLine();
while (line != null) {
sb.append(line);
line = reader.readLine();
}
return decodeForm(sb.toString());
}
/**
* Construct a Map containing a copy of the given parameters. If several
* parameters have the same name, the Map will contain the first value,
* only.
*/
public static <T extends Map.Entry<String, String>> Map<String, String> toMap(Collection<T> from) {
HashMap<String, String> map = new HashMap<String, String>();
if (from != null) {
for (Map.Entry<String, String> entry : from) {
String key = entry.getKey();
if (!map.containsKey(key)) {
map.put(key, entry.getValue());
}
}
}
return map;
}
public static final String safeToString(Object from) {
return (from == null) ? null : from.toString();
}
public static boolean isEmpty(String str) {
return (str == null) || (str.length() == 0);
}
/**
* Appends a list of key/value pairs to the given URL, e.g.:
*
* <pre>
* String url = OAuth.addQueryParameters(&quot;http://example.com?a=1&quot;, b, 2, c, 3);
* </pre>
*
* which yields:
*
* <pre>
* http://example.com?a=1&b=2&c=3
* </pre>
*
* All parameters will be encoded according to OAuth's percent encoding
* rules.
*
* @param url
* the URL
* @param kvPairs
* the list of key/value pairs
* @return string
*/
public static String addQueryParameters(String url, String... kvPairs)
throws MalformedInputException, UnmappableCharacterException {
String queryDelim = url.contains("?") ? "&" : "?";
StringBuilder sb = new StringBuilder(url + queryDelim);
for (int i = 0; i < kvPairs.length; i += 2) {
if (i > 0) {
sb.append("&");
}
sb.append(percentEncoder.encode(kvPairs[i])).append("=")
.append(percentEncoder.encode(kvPairs[i + 1]));
}
return sb.toString();
}
public static String addQueryParameters(String url, Map<String, String> params)
throws MalformedInputException, UnmappableCharacterException {
String[] kvPairs = new String[params.size() * 2];
int idx = 0;
for (String key : params.keySet()) {
kvPairs[idx] = key;
kvPairs[idx + 1] = params.get(key);
idx += 2;
}
return addQueryParameters(url, kvPairs);
}
public static String addQueryString(String url, String queryString) {
String queryDelim = url.contains("?") ? "&" : "?";
return url + queryDelim + queryString;
}
/**
* Builds an OAuth header from the given list of header fields. All
* parameters starting in 'oauth_*' will be percent encoded.
*
* <pre>
* String authHeader = OAuth.prepareOAuthHeader(&quot;realm&quot;, &quot;http://example.com&quot;, &quot;oauth_token&quot;, &quot;x%y&quot;);
* </pre>
*
* which yields:
*
* <pre>
* OAuth realm=&quot;http://example.com&quot;, oauth_token=&quot;x%25y&quot;
* </pre>
*
* @param kvPairs
* the list of key/value pairs
* @return a string eligible to be used as an OAuth HTTP Authorization
* header.
*/
public static String prepareOAuthHeader(String... kvPairs)
throws MalformedInputException, UnmappableCharacterException {
StringBuilder sb = new StringBuilder("OAuth ");
for (int i = 0; i < kvPairs.length; i += 2) {
if (i > 0) {
sb.append(", ");
}
boolean isOAuthElem = kvPairs[i].startsWith("oauth_")
|| kvPairs[i].startsWith("x_oauth_");
String value = isOAuthElem ? percentEncoder.encode(kvPairs[i + 1]) : kvPairs[i + 1];
sb.append(percentEncoder.encode(kvPairs[i])).append("=\"").append(value).append("\"");
}
return sb.toString();
}
public static HttpParameters oauthHeaderToParamsMap(String oauthHeader)
throws MalformedInputException, UnmappableCharacterException {
HttpParameters params = new HttpParameters();
if (oauthHeader == null || !oauthHeader.startsWith("OAuth ")) {
return params;
}
String[] elements = oauthHeader.substring("OAuth ".length()).split(",");
for (String keyValuePair : elements) {
String[] keyValue = keyValuePair.split("=");
params.put(keyValue[0].trim(), keyValue[1].replace("\"", "").trim());
}
return params;
}
/**
* Helper method to concatenate a parameter and its value to a pair that can
* be used in an HTTP header. This method percent encodes both parts before
* joining them.
*
* @param name
* the OAuth parameter name, e.g. oauth_token
* @param value
* the OAuth parameter value, e.g. 'hello oauth'
* @return a name/value pair, e.g. oauth_token="hello%20oauth"
*/
public static String toHeaderElement(String name, String value)
throws MalformedInputException, UnmappableCharacterException {
return percentEncoder.encode(name) + "=\"" + percentEncoder.encode(value) + "\"";
}
}

View file

@ -0,0 +1,21 @@
package org.xbib.net.oauth;
@SuppressWarnings("serial")
public class OAuthCommunicationException extends OAuthException {
private String responseBody;
public OAuthCommunicationException(Exception cause) {
super("Communication with the service provider failed: "
+ cause.getLocalizedMessage(), cause);
}
public OAuthCommunicationException(String message, String responseBody) {
super(message);
this.responseBody = responseBody;
}
public String getResponseBody() {
return responseBody;
}
}

View file

@ -0,0 +1,157 @@
package org.xbib.net.oauth;
import org.xbib.net.http.HttpParameters;
import org.xbib.net.http.HttpRequest;
import org.xbib.net.oauth.sign.OAuthMessageSigner;
import org.xbib.net.oauth.sign.SigningStrategy;
/**
* <p>
* Exposes a simple interface to sign HTTP requests using a given OAuth token
* and secret. Refer to {@link OAuthProvider} how to retrieve a valid token and
* token secret.
* </p>
* HTTP messages are signed as follows:
*
* <pre>
* // exchange the arguments with the actual token/secret pair
* OAuthConsumer consumer = new DefaultOAuthConsumer(&quot;1234&quot;, &quot;5678&quot;);
* URL url = new URL(&quot;http://example.com/protected.xml&quot;);
* HttpURLConnection request = (HttpURLConnection) url.openConnection();
* consumer.sign(request);
* request.connect();
* </pre>
*
*/
public interface OAuthConsumer {
/**
* Sets the message signer that should be used to generate the OAuth
* signature.
*
* @param messageSigner
* the signer
*/
void setMessageSigner(OAuthMessageSigner messageSigner);
/**
* Allows you to add parameters (typically OAuth parameters such as
* oauth_callback or oauth_verifier) which will go directly into the signer,
* i.e. you don't have to put them into the request first. The consumer's
* signing strategy will then take care of writing them to the
* correct part of the request before it is sent. This is useful if you want
* to pre-set custom OAuth parameters. Note that these parameters are
* expected to already be percent encoded -- they will be simply merged
* as-is. <b>BE CAREFUL WITH THIS METHOD! Your service provider may decide
* to ignore any non-standard OAuth params when computing the signature.</b>
*
* @param additionalParameters
* the parameters
*/
void setAdditionalParameters(HttpParameters additionalParameters);
/**
* Defines which strategy should be used to write a signature to an HTTP
* request.
*
* @param signingStrategy
* the strategy
*/
void setSigningStrategy(SigningStrategy signingStrategy);
/**
* <p>
* Causes the consumer to always include the oauth_token parameter to be
* sent, even if blank. If you're seeing 401s during calls to
* {@link OAuthProvider#retrieveRequestToken}, try setting this to true.
* </p>
*
* @param enable
* true or false
*/
void setSendEmptyTokens(boolean enable);
/**
* Signs the given HTTP request by writing an OAuth signature (and other
* required OAuth parameters) to it. Where these parameters are written
* depends on the current {@link SigningStrategy}.
*
* @param request
* the request to sign
* @return the request object passed as an argument
* @throws OAuthMessageSignerException
* @throws OAuthExpectationFailedException
* @throws OAuthCommunicationException
*/
HttpRequest sign(HttpRequest request) throws OAuthMessageSignerException,
OAuthExpectationFailedException, OAuthCommunicationException;
/**
* <p>
* Signs the given HTTP request by writing an OAuth signature (and other
* required OAuth parameters) to it. Where these parameters are written
* depends on the current {@link SigningStrategy}.
* </p>
* This method accepts HTTP library specific request objects; the consumer
* implementation must ensure that only those request types are passed which
* it supports.
*
* @param request
* the request to sign
* @return the request object passed as an argument
* @throws OAuthMessageSignerException
* @throws OAuthExpectationFailedException
* @throws OAuthCommunicationException
*/
HttpRequest sign(Object request) throws OAuthMessageSignerException,
OAuthExpectationFailedException, OAuthCommunicationException;
/**
* "Signs" the given URL by appending all OAuth parameters to it which are
* required for message signing. The assumed HTTP method is GET.
* Essentially, this is equivalent to signing an HTTP GET request, but it
* can be useful if your application requires clickable links to protected
* resources, i.e. when your application does not have access to the actual
* request that is being sent.
*
* @param url
* the input URL. May have query parameters.
* @return the input URL, with all necessary OAuth parameters attached as a
* query string. Existing query parameters are preserved.
* @throws OAuthMessageSignerException
* @throws OAuthExpectationFailedException
* @throws OAuthCommunicationException
*/
String sign(String url) throws OAuthMessageSignerException,
OAuthExpectationFailedException, OAuthCommunicationException;
/**
* Sets the OAuth token and token secret used for message signing.
*
* @param token
* the token
* @param tokenSecret
* the token secret
*/
void setTokenWithSecret(String token, String tokenSecret);
String getToken();
String getTokenSecret();
String getConsumerKey();
String getConsumerSecret();
/**
* Returns all parameters collected from the HTTP request during message
* signing (this means the return value may be NULL before a call to
* {@link #sign}), plus all required OAuth parameters that were added
* because the request didn't contain them beforehand. In other words, this
* is the exact set of parameters that were used for creating the message
* signature.
*
* @return the request parameters used for message signing
*/
HttpParameters getRequestParameters();
}

View file

@ -0,0 +1,17 @@
package org.xbib.net.oauth;
@SuppressWarnings("serial")
public abstract class OAuthException extends Exception {
public OAuthException(String message) {
super(message);
}
public OAuthException(Throwable cause) {
super(cause);
}
public OAuthException(String message, Throwable cause) {
super(message, cause);
}
}

View file

@ -0,0 +1,9 @@
package org.xbib.net.oauth;
@SuppressWarnings("serial")
public class OAuthExpectationFailedException extends OAuthException {
public OAuthExpectationFailedException(String message) {
super(message);
}
}

View file

@ -0,0 +1,14 @@
package org.xbib.net.oauth;
@SuppressWarnings("serial")
public class OAuthMessageSignerException extends OAuthException {
public OAuthMessageSignerException(String message) {
super(message);
}
public OAuthMessageSignerException(Exception cause) {
super(cause);
}
}

View file

@ -0,0 +1,24 @@
package org.xbib.net.oauth;
@SuppressWarnings("serial")
public class OAuthNotAuthorizedException extends OAuthException {
private static final String ERROR = "Authorization failed (server replied with a 401). "
+ "This can happen if the consumer key was not correct or "
+ "the signatures did not match.";
private String responseBody;
public OAuthNotAuthorizedException() {
super(ERROR);
}
public OAuthNotAuthorizedException(String responseBody) {
super(ERROR);
this.responseBody = responseBody;
}
public String getResponseBody() {
return responseBody;
}
}

View file

@ -0,0 +1,206 @@
package org.xbib.net.oauth;
import org.xbib.net.http.HttpParameters;
/**
* <p>
* Supplies an interface that can be used to retrieve request and access tokens
* from an OAuth 1.0(a) service provider. A provider object requires an
* {@link OAuthConsumer} to sign the token request message; after a token has
* been retrieved, the consumer is automatically updated with the token and the
* corresponding secret.
* </p>
* <p>
* To initiate the token exchange, create a new provider instance and configure
* it with the URLs the service provider exposes for requesting tokens and
* resource authorization, e.g.:
* </p>
*
* <pre>
* OAuthProvider provider = new DefaultOAuthProvider(&quot;http://twitter.com/oauth/request_token&quot;,
* &quot;http://twitter.com/oauth/access_token&quot;, &quot;http://twitter.com/oauth/authorize&quot;);
* </pre>
* <p>
* Depending on the HTTP library you use, you may need a different provider
* type, refer to the website documentation for how to do that.
* </p>
* <p>
* To receive a request token which the user must authorize, you invoke it using
* a consumer instance and a callback URL:
* </p>
* <p>
*
* <pre>
* String url = provider.retrieveRequestToken(consumer, &quot;http://www.example.com/callback&quot;);
* </pre>
*
* </p>
* <p>
* That url must be opened in a Web browser, where the user can grant access to
* the resources in question. If that succeeds, the service provider will
* redirect to the callback URL and append the blessed request token.
* </p>
* <p>
* That token must now be exchanged for an access token, as such:
* </p>
* <p>
*
* <pre>
* provider.retrieveAccessToken(consumer, nullOrVerifierCode);
* </pre>
*
* </p>
* <p>
* where nullOrVerifierCode is either null if your provided a callback URL in
* the previous step, or the pin code issued by the service provider to the user
* if the request was out-of-band (cf. {@link OAuth#OUT_OF_BAND}.
* </p>
* <p>
* The consumer used during token handshakes is now ready for signing.
* </p>
*
* @see OAuthProviderListener
*/
public interface OAuthProvider {
/**
* Queries the service provider for a request token.
* <p>
* <b>Pre-conditions:</b> the given {@link OAuthConsumer} must have a valid
* consumer key and consumer secret already set.
* </p>
* <p>
* <b>Post-conditions:</b> the given {@link OAuthConsumer} will have an
* unauthorized request token and token secret set.
* </p>
*
* @param consumer
* the {@link OAuthConsumer} that should be used to sign the request
* @param callbackUrl
* Pass an actual URL if your app can receive callbacks and you want
* to get informed about the result of the authorization process.
* Pass OUT_OF_BAND if the service provider implements
* OAuth 1.0a and your app cannot receive callbacks. Pass null if the
* service provider implements OAuth 1.0 and your app cannot receive
* callbacks. Please note that some services (among them Twitter)
* will fail authorization if you pass a callback URL but register
* your application as a desktop app (which would only be able to
* handle OOB requests).
* @param customOAuthParams
* you can pass custom OAuth parameters here which will go directly
* into the signer, i.e. you don't have to put them into the request
* first. This is useful for pre-setting OAuth params for signing.
* Pass them sequentially in key/value order.
* @return The URL to which the user must be sent in order to authorize the
* consumer. It includes the unauthorized request token (and in the
* case of OAuth 1.0, the callback URL -- 1.0a clients send along
* with the token request).
* @throws OAuthMessageSignerException
* if signing the request failed
* @throws OAuthNotAuthorizedException
* if the service provider rejected the consumer
* @throws OAuthExpectationFailedException
* if required parameters were not correctly set by the consumer or
* service provider
* @throws OAuthCommunicationException
* if server communication failed
*/
String retrieveRequestToken(OAuthConsumer consumer, String callbackUrl,
String... customOAuthParams) throws OAuthMessageSignerException,
OAuthNotAuthorizedException, OAuthExpectationFailedException,
OAuthCommunicationException;
/**
* Queries the service provider for an access token.
* <p>
* <b>Pre-conditions:</b> the given {@link OAuthConsumer} must have a valid
* consumer key, consumer secret, authorized request token and token secret
* already set.
* </p>
* <p>
* <b>Post-conditions:</b> the given {@link OAuthConsumer} will have an
* access token and token secret set.
* </p>
*
* @param consumer
* the {@link OAuthConsumer} that should be used to sign the request
* @param oauthVerifier
* <b>NOTE: Only applies to service providers implementing OAuth
* 1.0a. Set to null if the service provider is still using OAuth
* 1.0.</b> The verification code issued by the service provider
* after the the user has granted the consumer authorization. If the
* callback method provided in the previous step was
* OUT_OF_BAND, then you must ask the user for this
* value. If your app has received a callback, the verfication code
* was passed as part of that request instead.
* @param customOAuthParams
* you can pass custom OAuth parameters here which will go directly
* into the signer, i.e. you don't have to put them into the request
* first. This is useful for pre-setting OAuth params for signing.
* Pass them sequentially in key/value order.
* @throws OAuthMessageSignerException
* if signing the request failed
* @throws OAuthNotAuthorizedException
* if the service provider rejected the consumer
* @throws OAuthExpectationFailedException
* if required parameters were not correctly set by the consumer or
* service provider
* @throws OAuthCommunicationException
* if server communication failed
*/
void retrieveAccessToken(OAuthConsumer consumer, String oauthVerifier,
String... customOAuthParams) throws OAuthMessageSignerException,
OAuthNotAuthorizedException, OAuthExpectationFailedException,
OAuthCommunicationException;
/**
* Any additional non-OAuth parameters returned in the response body of a
* token request can be obtained through this method. These parameters will
* be preserved until the next token request is issued. The return value is
* never null.
*/
HttpParameters getResponseParameters();
/**
* Subclasses must use this setter to preserve any non-OAuth query
* parameters contained in the server response. It's the caller's
* responsibility that any OAuth parameters be removed beforehand.
*
* @param parameters
* the map of query parameters served by the service provider in the
* token response
*/
void setResponseParameters(HttpParameters parameters);
/**
* @param isOAuth10aProvider
* set to true if the service provider supports OAuth 1.0a. Note that
* you need only call this method if you reconstruct a provider
* object in between calls to retrieveRequestToken() and
* retrieveAccessToken() (i.e. if the object state isn't preserved).
* If instead those two methods are called on the same provider
* instance, this flag will be deducted automatically based on the
* server response during retrieveRequestToken(), so you can simply
* ignore this method.
*/
void setOAuth10a(boolean isOAuth10aProvider);
/**
* @return true if the service provider supports OAuth 1.0a. Note that the
* value returned here is only meaningful after you have already
* performed the token handshake, otherwise there is no way to
* determine what version of the OAuth protocol the service provider
* implements.
*/
boolean isOAuth10a();
String getRequestTokenEndpointUrl();
String getAccessTokenEndpointUrl();
String getAuthorizationWebsiteUrl();
void setListener(OAuthProviderListener listener);
void removeListener(OAuthProviderListener listener);
}

View file

@ -0,0 +1,47 @@
package org.xbib.net.oauth;
import org.xbib.net.http.HttpRequest;
import org.xbib.net.http.HttpResponse;
/**
* Provides hooks into the token request handling procedure executed by
* {@link OAuthProvider}.
*
*/
public interface OAuthProviderListener {
/**
* Called after the request has been created and default headers added, but
* before the request has been signed.
*
* @param request
* the request to be sent
* @throws Exception
*/
void prepareRequest(HttpRequest request) throws Exception;
/**
* Called after the request has been signed, but before it's being sent.
*
* @param request
* the request to be sent
* @throws Exception
*/
void prepareSubmission(HttpRequest request) throws Exception;
/**
* Called when the server response has been received. You can implement this
* to manually handle the response data.
*
* @param request
* the request that was sent
* @param response
* the response that was received
* @return returning true means you have handled the response, and the
* provider will return immediately. Return false to let the event
* propagate and let the provider execute its default response
* handling.
* @throws Exception
*/
boolean onResponseReceived(HttpRequest request, HttpResponse response) throws Exception;
}

View file

@ -0,0 +1,42 @@
package org.xbib.net.oauth.sign;
import org.xbib.net.http.HttpParameters;
import org.xbib.net.http.HttpRequest;
import org.xbib.net.oauth.OAuth;
import java.nio.charset.MalformedInputException;
import java.nio.charset.UnmappableCharacterException;
import java.util.Iterator;
/**
* Writes to the HTTP Authorization header field.
*/
public class AuthorizationHeaderSigningStrategy implements SigningStrategy {
@Override
public String writeSignature(String signature, HttpRequest request,
HttpParameters requestParameters) throws MalformedInputException, UnmappableCharacterException {
StringBuilder sb = new StringBuilder();
sb.append("OAuth ");
// add the realm parameter, if any
if (requestParameters.containsKey("realm")) {
sb.append(requestParameters.getAsHeaderElement("realm"));
sb.append(", ");
}
// add all (x_)oauth parameters
HttpParameters oauthParams = requestParameters.getOAuthParameters();
oauthParams.put(OAuth.OAUTH_SIGNATURE, signature, true);
Iterator<String> iterator = oauthParams.keySet().iterator();
while (iterator.hasNext()) {
String key = iterator.next();
sb.append(oauthParams.getAsHeaderElement(key));
if (iterator.hasNext()) {
sb.append(", ");
}
}
String header = sb.toString();
request.setHeader(OAuth.HTTP_AUTHORIZATION_HEADER, header);
return header;
}
}

View file

@ -0,0 +1,45 @@
package org.xbib.net.oauth.sign;
import org.xbib.net.http.HttpParameters;
import org.xbib.net.http.HttpRequest;
import org.xbib.net.oauth.OAuth;
import org.xbib.net.oauth.OAuthMessageSignerException;
import java.io.UnsupportedEncodingException;
import java.nio.charset.MalformedInputException;
import java.nio.charset.UnmappableCharacterException;
import java.security.GeneralSecurityException;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
@SuppressWarnings("serial")
public class HmacSha1MessageSigner extends OAuthMessageSigner {
private static final String MAC_NAME = "HmacSHA1";
@Override
public String getSignatureMethod() {
return "HMAC-SHA1";
}
@Override
public String sign(HttpRequest request, HttpParameters requestParams)
throws OAuthMessageSignerException {
try {
String keyString = OAuth.percentEncoder.encode(getConsumerSecret()) + '&'
+ OAuth.percentEncoder.encode(getTokenSecret());
byte[] keyBytes = keyString.getBytes(OAuth.ENCODING);
SecretKey key = new SecretKeySpec(keyBytes, MAC_NAME);
Mac mac = Mac.getInstance(MAC_NAME);
mac.init(key);
String sbs = new SignatureBaseString(request, requestParams).generate();
byte[] text = sbs.getBytes(OAuth.ENCODING);
return base64Encode(mac.doFinal(text)).trim();
} catch (GeneralSecurityException | UnsupportedEncodingException |
MalformedInputException | UnmappableCharacterException e) {
throw new OAuthMessageSignerException(e);
}
}
}

View file

@ -0,0 +1,45 @@
package org.xbib.net.oauth.sign;
import org.xbib.net.http.HttpParameters;
import org.xbib.net.http.HttpRequest;
import org.xbib.net.oauth.OAuth;
import org.xbib.net.oauth.OAuthMessageSignerException;
import java.io.UnsupportedEncodingException;
import java.nio.charset.MalformedInputException;
import java.nio.charset.UnmappableCharacterException;
import java.security.GeneralSecurityException;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
@SuppressWarnings("serial")
public class HmacSha256MessageSigner extends OAuthMessageSigner {
private static final String MAC_NAME = "HmacSHA256";
@Override
public String getSignatureMethod() {
return "HMAC-SHA256";
}
@Override
public String sign(HttpRequest request, HttpParameters requestParams)
throws OAuthMessageSignerException {
try {
String keyString = OAuth.percentEncoder.encode(getConsumerSecret()) + '&'
+ OAuth.percentEncoder.encode(getTokenSecret());
byte[] keyBytes = keyString.getBytes(OAuth.ENCODING);
SecretKey key = new SecretKeySpec(keyBytes, MAC_NAME);
Mac mac = Mac.getInstance(MAC_NAME);
mac.init(key);
String sbs = new SignatureBaseString(request, requestParams).generate();
byte[] text = sbs.getBytes(OAuth.ENCODING);
return base64Encode(mac.doFinal(text)).trim();
} catch (GeneralSecurityException | UnsupportedEncodingException |
MalformedInputException| UnmappableCharacterException e) {
throw new OAuthMessageSignerException(e);
}
}
}

View file

@ -0,0 +1,54 @@
package org.xbib.net.oauth.sign;
import org.xbib.net.http.HttpParameters;
import org.xbib.net.http.HttpRequest;
import org.xbib.net.oauth.OAuthMessageSignerException;
import java.util.Base64;
public abstract class OAuthMessageSigner {
private Base64.Encoder base64Encoder;
private Base64.Decoder base64Decoder;
private String consumerSecret;
private String tokenSecret;
public OAuthMessageSigner() {
this.base64Encoder = Base64.getEncoder();
this.base64Decoder = Base64.getDecoder();
}
public abstract String sign(HttpRequest request, HttpParameters requestParameters)
throws OAuthMessageSignerException;
public abstract String getSignatureMethod();
public String getConsumerSecret() {
return consumerSecret;
}
public String getTokenSecret() {
return tokenSecret;
}
public void setConsumerSecret(String consumerSecret) {
this.consumerSecret = consumerSecret;
}
public void setTokenSecret(String tokenSecret) {
this.tokenSecret = tokenSecret;
}
protected byte[] decodeBase64(String s) {
return base64Decoder.decode(s.getBytes());
}
protected String base64Encode(byte[] b) {
return new String(base64Encoder.encode(b));
}
}

View file

@ -0,0 +1,29 @@
package org.xbib.net.oauth.sign;
import org.xbib.net.http.HttpParameters;
import org.xbib.net.http.HttpRequest;
import org.xbib.net.oauth.OAuth;
import org.xbib.net.oauth.OAuthMessageSignerException;
import java.nio.charset.MalformedInputException;
import java.nio.charset.UnmappableCharacterException;
@SuppressWarnings("serial")
public class PlainTextMessageSigner extends OAuthMessageSigner {
@Override
public String getSignatureMethod() {
return "PLAINTEXT";
}
@Override
public String sign(HttpRequest request, HttpParameters requestParams)
throws OAuthMessageSignerException {
try {
return OAuth.percentEncoder.encode(getConsumerSecret()) + '&'
+ OAuth.percentEncoder.encode(getTokenSecret());
} catch (MalformedInputException | UnmappableCharacterException e) {
throw new OAuthMessageSignerException(e);
}
}
}

View file

@ -0,0 +1,41 @@
package org.xbib.net.oauth.sign;
import org.xbib.net.http.HttpParameters;
import org.xbib.net.http.HttpRequest;
import org.xbib.net.oauth.OAuth;
import java.nio.charset.MalformedInputException;
import java.nio.charset.UnmappableCharacterException;
import java.util.Iterator;
/**
* Writes to a URL query string. <strong>Note that this currently ONLY works
* when signing a URL directly, not with HTTP request objects.</strong> That's
* because most HTTP request implementations do not allow the client to change
* the URL once the request has been instantiated, so there is no way to append
* parameters to it.
*/
public class QueryStringSigningStrategy implements SigningStrategy {
@Override
public String writeSignature(String signature, HttpRequest request,
HttpParameters requestParameters)
throws MalformedInputException, UnmappableCharacterException {
// add all (x_)oauth parameters
HttpParameters oauthParams = requestParameters.getOAuthParameters();
oauthParams.put(OAuth.OAUTH_SIGNATURE, signature, true);
Iterator<String> iterator = oauthParams.keySet().iterator();
// add the first query parameter (we always have at least the signature)
String firstKey = iterator.next();
StringBuilder sb = new StringBuilder(OAuth.addQueryString(request.getRequestUrl(),
oauthParams.getAsQueryString(firstKey)));
while (iterator.hasNext()) {
sb.append("&");
String key = iterator.next();
sb.append(oauthParams.getAsQueryString(key));
}
String signedUrl = sb.toString();
request.setRequestUrl(signedUrl);
return signedUrl;
}
}

View file

@ -0,0 +1,96 @@
package org.xbib.net.oauth.sign;
import org.xbib.net.http.HttpParameters;
import org.xbib.net.http.HttpRequest;
import org.xbib.net.oauth.OAuth;
import org.xbib.net.oauth.OAuthMessageSignerException;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Iterator;
public class SignatureBaseString {
private final HttpRequest request;
private final HttpParameters requestParameters;
/**
* Constructs a new instance that will operate on the given request
* object and parameter set.
*
* @param request the HTTP request
* @param requestParameters the set of request parameters from the Authorization header, query
* string and form body
*/
public SignatureBaseString(HttpRequest request, HttpParameters requestParameters) {
this.request = request;
this.requestParameters = requestParameters;
}
/**
* Builds the signature base string from the data this instance was
* configured with.
*
* @return the signature base string
* @throws OAuthMessageSignerException
*/
public String generate() throws OAuthMessageSignerException {
try {
String normalizedUrl = normalizeRequestUrl();
String normalizedParams = normalizeRequestParameters();
return request.getMethod() + '&' + OAuth.percentEncoder.encode(normalizedUrl) + '&'
+ OAuth.percentEncoder.encode(normalizedParams);
} catch (URISyntaxException | IOException e) {
throw new OAuthMessageSignerException(e);
}
}
public String normalizeRequestUrl() throws URISyntaxException {
URI uri = new URI(request.getRequestUrl());
String scheme = uri.getScheme().toLowerCase();
String authority = uri.getAuthority().toLowerCase();
boolean dropPort = (scheme.equals("http") && uri.getPort() == 80)
|| (scheme.equals("https") && uri.getPort() == 443);
if (dropPort) {
// find the last : in the authority
int index = authority.lastIndexOf(":");
if (index >= 0) {
authority = authority.substring(0, index);
}
}
String path = uri.getRawPath();
if (path == null || path.length() <= 0) {
path = "/"; // conforms to RFC 2616 section 3.2.2
}
// we know that there is no query and no fragment here.
return scheme + "://" + authority + path;
}
/**
* Normalizes the set of request parameters this instance was configured
* with, as per OAuth spec section 9.1.1.
*
* @return the normalized params string
* @throws IOException
*/
public String normalizeRequestParameters() throws IOException {
if (requestParameters == null) {
return "";
}
StringBuilder sb = new StringBuilder();
Iterator<String> iter = requestParameters.keySet().iterator();
for (int i = 0; iter.hasNext(); i++) {
String param = iter.next();
if (OAuth.OAUTH_SIGNATURE.equals(param) || "realm".equals(param)) {
continue;
}
if (i > 0) {
sb.append("&");
}
sb.append(requestParameters.getAsQueryString(param, false));
}
return sb.toString();
}
}

View file

@ -0,0 +1,37 @@
package org.xbib.net.oauth.sign;
import org.xbib.net.http.HttpParameters;
import org.xbib.net.http.HttpRequest;
import java.nio.charset.MalformedInputException;
import java.nio.charset.UnmappableCharacterException;
/**
* <p>
* Defines how an OAuth signature string is written to a request.
* </p>
* <p>
* Unlike {@link OAuthMessageSigner}, which is concerned with <i>how</i> to
* generate a signature, this class is concered with <i>where</i> to write it
* (e.g. HTTP header or query string).
* </p>
*/
public interface SigningStrategy {
/**
* Writes an OAuth signature and all remaining required parameters to an
* HTTP message.
*
* @param signature
* the signature to write
* @param request
* the request to sign
* @param requestParameters
* the request parameters
* @return whatever has been written to the request, e.g. an Authorization
* header field
*/
String writeSignature(String signature, HttpRequest request, HttpParameters requestParameters)
throws MalformedInputException, UnmappableCharacterException;
}

View file

@ -5,7 +5,7 @@ import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
* Query parameters. * Query parameter list, of limited size. Default is 1024 pairs.
*/ */
public class QueryParameters extends ArrayList<QueryParameters.Pair<String, String>> { public class QueryParameters extends ArrayList<QueryParameters.Pair<String, String>> {

View file

@ -864,6 +864,12 @@ public class URL implements Comparable<URL> {
return this; return this;
} }
public Builder resetQueryParams() {
queryParams.clear();
query = null;
return this;
}
/** /**
* Add a query parameter. Query parameters will be encoded in the order added. * Add a query parameter. Query parameters will be encoded in the order added.
* *

View file

@ -1,3 +1,4 @@
rootProject.name = name rootProject.name = name
include 'net-url' include 'net-url'
include 'net-http'