beginning work of adding limitation and validation framework to http parameters, playing with old oauth code
This commit is contained in:
parent
412b6eaeb5
commit
3c00b77d98
41 changed files with 2385 additions and 46 deletions
52
build.gradle
52
build.gradle
|
@ -2,6 +2,7 @@
|
|||
plugins {
|
||||
id "org.sonarqube" version "2.6.1"
|
||||
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"
|
||||
}
|
||||
|
||||
|
@ -25,7 +26,7 @@ subprojects {
|
|||
apply plugin: 'maven'
|
||||
apply plugin: 'signing'
|
||||
apply plugin: 'checkstyle'
|
||||
apply plugin: 'findbugs'
|
||||
apply plugin: 'com.github.spotbugs'
|
||||
apply plugin: 'pmd'
|
||||
apply plugin: 'org.xbib.gradle.plugin.asciidoctor'
|
||||
|
||||
|
@ -91,7 +92,7 @@ subprojects {
|
|||
javadoc {
|
||||
options.docletpath = configurations.asciidoclet.files.asType(List)
|
||||
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 "-attribute",
|
||||
"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/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 {
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
group = org.xbib
|
||||
name = net
|
||||
version = 1.1.3
|
||||
version = 1.2.0
|
||||
|
||||
jackson.version = 2.8.11
|
||||
junit.version = 4.12
|
||||
wagon.version = 3.0.0
|
||||
asciidoclet.version = 1.5.4
|
||||
|
||||
org.gradle.warning.mode=all
|
||||
|
|
|
@ -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/"
|
||||
}
|
||||
}
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -1,6 +1,6 @@
|
|||
#Fri Sep 14 16:19:33 CEST 2018
|
||||
#Thu Jan 17 15:23:55 CET 2019
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
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
2
gradlew
vendored
|
@ -28,7 +28,7 @@ APP_NAME="Gradle"
|
|||
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.
|
||||
DEFAULT_JVM_OPTS=""
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
|
2
gradlew.bat
vendored
2
gradlew.bat
vendored
|
@ -14,7 +14,7 @@ set APP_BASE_NAME=%~n0
|
|||
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.
|
||||
set DEFAULT_JVM_OPTS=
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
|
3
net-http/build.gradle
Normal file
3
net-http/build.gradle
Normal file
|
@ -0,0 +1,3 @@
|
|||
dependencies {
|
||||
compile project(':net-url')
|
||||
}
|
298
net-http/src/main/java/org/xbib/net/http/HttpParameters.java
Normal file
298
net-http/src/main/java/org/xbib/net/http/HttpParameters.java
Normal 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;
|
||||
}
|
||||
}
|
30
net-http/src/main/java/org/xbib/net/http/HttpRequest.java
Normal file
30
net-http/src/main/java/org/xbib/net/http/HttpRequest.java
Normal 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();
|
||||
}
|
15
net-http/src/main/java/org/xbib/net/http/HttpResponse.java
Normal file
15
net-http/src/main/java/org/xbib/net/http/HttpResponse.java
Normal 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();
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
3
net-oauth/build.gradle
Normal file
|
@ -0,0 +1,3 @@
|
|||
dependencies {
|
||||
compile project(':net-http')
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
285
net-oauth/src/main/java/org/xbib/net/oauth/OAuth.java
Normal file
285
net-oauth/src/main/java/org/xbib/net/oauth/OAuth.java
Normal 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("http://example.com?a=1", 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("realm", "http://example.com", "oauth_token", "x%y");
|
||||
* </pre>
|
||||
*
|
||||
* which yields:
|
||||
*
|
||||
* <pre>
|
||||
* OAuth realm="http://example.com", oauth_token="x%25y"
|
||||
* </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) + "\"";
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
157
net-oauth/src/main/java/org/xbib/net/oauth/OAuthConsumer.java
Normal file
157
net-oauth/src/main/java/org/xbib/net/oauth/OAuthConsumer.java
Normal 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("1234", "5678");
|
||||
* URL url = new URL("http://example.com/protected.xml");
|
||||
* 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();
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package org.xbib.net.oauth;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
public class OAuthExpectationFailedException extends OAuthException {
|
||||
|
||||
public OAuthExpectationFailedException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
206
net-oauth/src/main/java/org/xbib/net/oauth/OAuthProvider.java
Normal file
206
net-oauth/src/main/java/org/xbib/net/oauth/OAuthProvider.java
Normal 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("http://twitter.com/oauth/request_token",
|
||||
* "http://twitter.com/oauth/access_token", "http://twitter.com/oauth/authorize");
|
||||
* </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, "http://www.example.com/callback");
|
||||
* </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);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -5,7 +5,7 @@ import java.util.List;
|
|||
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>> {
|
||||
|
||||
|
|
|
@ -864,6 +864,12 @@ public class URL implements Comparable<URL> {
|
|||
return this;
|
||||
}
|
||||
|
||||
public Builder resetQueryParams() {
|
||||
queryParams.clear();
|
||||
query = null;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a query parameter. Query parameters will be encoded in the order added.
|
||||
*
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
rootProject.name = name
|
||||
|
||||
include 'net-url'
|
||||
include 'net-http'
|
||||
|
|
Loading…
Reference in a new issue