2 Copyright (c) 2007-2009, Yusuke Yamamoto
5 Redistribution and use in source and binary forms, with or without
6 modification, are permitted provided that the following conditions are met:
7 * Redistributions of source code must retain the above copyright
8 notice, this list of conditions and the following disclaimer.
9 * Redistributions in binary form must reproduce the above copyright
10 notice, this list of conditions and the following disclaimer in the
11 documentation and/or other materials provided with the distribution.
12 * Neither the name of the Yusuke Yamamoto nor the
13 names of its contributors may be used to endorse or promote products
14 derived from this software without specific prior written permission.
16 THIS SOFTWARE IS PROVIDED BY Yusuke Yamamoto ``AS IS'' AND ANY
17 EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 DISCLAIMED. IN NO EVENT SHALL Yusuke Yamamoto BE LIABLE FOR ANY
20 DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
21 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
22 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
23 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27 package weibo4android
.http
;
29 import weibo4android
.Configuration
;
31 import javax
.crypto
.Mac
;
32 import javax
.crypto
.spec
.SecretKeySpec
;
33 import java
.io
.UnsupportedEncodingException
;
34 import java
.net
.URLDecoder
;
35 import java
.net
.URLEncoder
;
36 import java
.security
.InvalidKeyException
;
37 import java
.security
.NoSuchAlgorithmException
;
38 import java
.util
.ArrayList
;
39 import java
.util
.Arrays
;
40 import java
.util
.Collections
;
41 import java
.util
.List
;
42 import java
.util
.Random
;
45 * @author Yusuke Yamamoto - yusuke at mac.com
46 * @see <a href="http://oauth.net/core/1.0/">OAuth Core 1.0</a>
48 public class OAuth
implements java
.io
.Serializable
{
49 private static final String HMAC_SHA1
= "HmacSHA1";
50 private static final PostParameter OAUTH_SIGNATURE_METHOD
= new PostParameter("oauth_signature_method", "HMAC-SHA1");
51 private final static boolean DEBUG
= Configuration
.getDebug();
52 static final long serialVersionUID
= -4368426677157998618L;
53 private String consumerKey
= "";
54 private String consumerSecret
;
56 public OAuth(String consumerKey
, String consumerSecret
) {
57 setConsumerKey(consumerKey
);
58 setConsumerSecret(consumerSecret
);
61 /*package*/ String
generateAuthorizationHeader(String method
, String url
, PostParameter
[] params
, String nonce
, String timestamp
, OAuthToken otoken
) {
63 params
= new PostParameter
[0];
65 List
<PostParameter
> oauthHeaderParams
= new ArrayList
<PostParameter
>(5);
66 oauthHeaderParams
.add(new PostParameter("oauth_consumer_key", consumerKey
));
67 oauthHeaderParams
.add(OAUTH_SIGNATURE_METHOD
);
68 oauthHeaderParams
.add(new PostParameter("oauth_timestamp", timestamp
));
69 oauthHeaderParams
.add(new PostParameter("oauth_nonce", nonce
));
71 oauthHeaderParams
.add(new PostParameter("oauth_version", "1.0"));
73 oauthHeaderParams
.add(new PostParameter("oauth_token", otoken
.getToken()));
75 List
<PostParameter
> signatureBaseParams
= new ArrayList
<PostParameter
>(oauthHeaderParams
.size() + params
.length
);
76 signatureBaseParams
.addAll(oauthHeaderParams
);
77 signatureBaseParams
.addAll(toParamList(params
));
78 parseGetParameters(url
, signatureBaseParams
);
80 StringBuffer base
= new StringBuffer(method
).append("&")
81 .append(encode(constructRequestURL(url
))).append("&");
82 base
.append(encode(normalizeRequestParameters(signatureBaseParams
)));
83 String oauthBaseString
= base
.toString();
84 log("OAuth base string:", oauthBaseString
);
85 String signature
= generateSignature(oauthBaseString
, otoken
);
86 log("OAuth signature:", signature
);
87 oauthHeaderParams
.add(new PostParameter("oauth_signature", signature
));
88 return "OAuth " + encodeParameters(oauthHeaderParams
, ",", true);
91 private void parseGetParameters(String url
, List
<PostParameter
> signatureBaseParams
) {
92 int queryStart
= url
.indexOf("?");
93 if (-1 != queryStart
) {
94 String
[] queryStrs
= url
.substring(queryStart
+ 1).split("&");
96 for (String query
: queryStrs
) {
97 String
[] split
= query
.split("=");
98 if (split
.length
== 2) {
99 signatureBaseParams
.add(
100 new PostParameter(URLDecoder
.decode(split
[0],
101 "UTF-8"), URLDecoder
.decode(split
[1],
104 signatureBaseParams
.add(
105 new PostParameter(URLDecoder
.decode(split
[0],
109 } catch (UnsupportedEncodingException ignore
) {
116 private static Random RAND
= new Random();
120 * @see <a href="http://oauth.net/core/1.0#rfc.section.5.4.1">OAuth Core - 5.4.1. Authorization Header</a>
122 /*package*/ String
generateAuthorizationHeader(String method
, String url
, PostParameter
[] params
, OAuthToken token
) {
123 long timestamp
= System
.currentTimeMillis() / 1000;
124 long nonce
= timestamp
+ RAND
.nextInt();
125 return generateAuthorizationHeader(method
, url
, params
, String
.valueOf(nonce
), String
.valueOf(timestamp
), token
);
130 * Computes RFC 2104-compliant HMAC signature.
132 * @param data the data to be signed
134 * @see <a href="http://oauth.net/core/1.0/#rfc.section.9.2.1">OAuth Core - 9.2.1. Generating Signature</a>
136 /*package*/ String
generateSignature(String data
, OAuthToken token
) {
137 byte[] byteHMAC
= null;
139 Mac mac
= Mac
.getInstance(HMAC_SHA1
);
142 String oauthSignature
= encode(consumerSecret
) + "&";
143 spec
= new SecretKeySpec(oauthSignature
.getBytes(), HMAC_SHA1
);
145 if (null == token
.getSecretKeySpec()) {
146 String oauthSignature
= encode(consumerSecret
) + "&" + encode(token
.getTokenSecret());
147 spec
= new SecretKeySpec(oauthSignature
.getBytes(), HMAC_SHA1
);
148 token
.setSecretKeySpec(spec
);
150 spec
= token
.getSecretKeySpec();
153 byteHMAC
= mac
.doFinal(data
.getBytes());
154 } catch (InvalidKeyException e
) {
156 } catch (NoSuchAlgorithmException ignore
) {
157 // should never happen
159 return new BASE64Encoder().encode(byteHMAC
);
162 /*package*/ String
generateSignature(String data
) {
163 return generateSignature(data
, null);
168 * The request parameters are collected, sorted and concatenated into a normalized string:<br>
169 * 鈥�Parameters in the OAuth HTTP Authorization header excluding the realm parameter.<br>
170 * 鈥�Parameters in the HTTP POST request body (with a content-type of application/x-www-form-urlencoded).<br>
171 * 鈥�HTTP GET parameters added to the URLs in the query part (as defined by [RFC3986] section 3).<br>
173 * The oauth_signature parameter MUST be excluded.<br>
174 * The parameters are normalized into a single string as follows:<br>
175 * 1. Parameters are sorted by name, using lexicographical byte value ordering. If two or more parameters share the same name, they are sorted by their value. For example:<br>
176 * 2. a=1, c=hi%20there, f=25, f=50, f=a, z=p, z=t<br>
178 * 4. Parameters are concatenated in their sorted order into a single string. For each parameter, the name is separated from the corresponding value by an 鈥�鈥�character (ASCII code 61), even if the value is empty. Each name-value pair is separated by an 鈥�鈥�character (ASCII code 38). For example:<br>
179 * 5. a=1&c=hi%20there&f=25&f=50&f=a&z=p&z=t<br>
182 * @param params parameters to be normalized and concatenated
183 * @return nomarized and concatenated parameters
184 * @see <a href="http://oauth.net/core/1.0#rfc.section.9.1.1">OAuth Core - 9.1.1. Normalize Request Parameters</a>
186 public static String
normalizeRequestParameters(PostParameter
[] params
) {
187 return normalizeRequestParameters(toParamList(params
));
190 public static String
normalizeRequestParameters(List
<PostParameter
> params
) {
191 Collections
.sort(params
);
192 return encodeParameters(params
);
195 public static String
normalizeAuthorizationHeaders(List
<PostParameter
> params
) {
196 Collections
.sort(params
);
197 return encodeParameters(params
);
200 public static List
<PostParameter
> toParamList(PostParameter
[] params
) {
201 List
<PostParameter
> paramList
= new ArrayList
<PostParameter
>(params
.length
);
202 paramList
.addAll(Arrays
.asList(params
));
207 * @param postParams parameters to be enocded and concatenated
208 * @return eoncoded string
209 * @see <a href="http://wiki.oauth.net/TestCases">OAuth / TestCases</a>
210 * @see <a href="http://groups.google.com/group/oauth/browse_thread/thread/a8398d0521f4ae3d/9d79b698ab217df2?hl=en&lnk=gst&q=space+encoding#9d79b698ab217df2">Space encoding - OAuth | Google Groups</a>
212 public static String
encodeParameters(List
<PostParameter
> postParams
) {
213 return encodeParameters(postParams
, "&", false);
216 public static String
encodeParameters(List
<PostParameter
> postParams
, String splitter
, boolean quot
) {
217 StringBuffer buf
= new StringBuffer();
218 for (PostParameter param
: postParams
) {
219 if (buf
.length() != 0) {
223 buf
.append(splitter
);
225 buf
.append(encode(param
.name
)).append("=");
230 encode(param
.value
));
232 if (buf
.length() != 0) {
237 return buf
.toString();
241 * @param value string to be encoded
242 * @return encoded string
243 * @see <a href="http://wiki.oauth.net/TestCases">OAuth / TestCases</a>
244 * @see <a href="http://groups.google.com/group/oauth/browse_thread/thread/a8398d0521f4ae3d/9d79b698ab217df2?hl=en&lnk=gst&q=space+encoding#9d79b698ab217df2">Space encoding - OAuth | Google Groups</a>
245 * @see <a href="http://tools.ietf.org/html/rfc3986#section-2.1">RFC 3986 - Uniform Resource Identifier (URI): Generic Syntax - 2.1. Percent-Encoding</a>
247 public static String
encode(String value
) {
248 String encoded
= null;
250 encoded
= URLEncoder
.encode(value
, "UTF-8");
251 } catch (UnsupportedEncodingException ignore
) {
253 StringBuffer buf
= new StringBuffer(encoded
.length());
255 for (int i
= 0; i
< encoded
.length(); i
++) {
256 focus
= encoded
.charAt(i
);
259 } else if (focus
== '+') {
261 } else if (focus
== '%' && (i
+ 1) < encoded
.length()
262 && encoded
.charAt(i
+ 1) == '7' && encoded
.charAt(i
+ 2) == 'E') {
269 return buf
.toString();
273 * The Signature Base String includes the request absolute URL, tying the signature to a specific endpoint. The URL used in the Signature Base String MUST include the scheme, authority, and path, and MUST exclude the query and fragment as defined by [RFC3986] section 3.<br>
274 * If the absolute request URL is not available to the Service Provider (it is always available to the Consumer), it can be constructed by combining the scheme being used, the HTTP Host header, and the relative HTTP request URL. If the Host header is not available, the Service Provider SHOULD use the host name communicated to the Consumer in the documentation or other means.<br>
275 * The Service Provider SHOULD document the form of URL used in the Signature Base String to avoid ambiguity due to URL normalization. Unless specified, URL scheme and authority MUST be lowercase and include the port number; http default port 80 and https default port 443 MUST be excluded.<br>
277 * For example, the request:<br>
278 * HTTP://Example.com:80/resource?id=123<br>
279 * Is included in the Signature Base String as:<br>
280 * http://example.com/resource
282 * @param url the url to be normalized
283 * @return the Signature Base String
284 * @see <a href="http://oauth.net/core/1.0#rfc.section.9.1.2">OAuth Core - 9.1.2. Construct Request URL</a>
287 public static String
constructRequestURL(String url
) {
288 int index
= url
.indexOf("?");
290 url
= url
.substring(0, index
);
292 int slashIndex
= url
.indexOf("/", 8);
293 String baseURL
= url
.substring(0, slashIndex
).toLowerCase();
294 int colonIndex
= baseURL
.indexOf(":", 8);
295 if (-1 != colonIndex
) {
296 // url contains port number
297 if (baseURL
.startsWith("http://") && baseURL
.endsWith(":80")) {
298 // http default port 80 MUST be excluded
299 baseURL
= baseURL
.substring(0, colonIndex
);
300 } else if (baseURL
.startsWith("https://") && baseURL
.endsWith(":443")) {
301 // http default port 443 MUST be excluded
302 baseURL
= baseURL
.substring(0, colonIndex
);
305 url
= baseURL
+ url
.substring(slashIndex
);
310 public void setConsumerKey(String consumerKey
) {
311 this.consumerKey
= null != consumerKey ? consumerKey
: "";
314 public void setConsumerSecret(String consumerSecret
) {
315 this.consumerSecret
= null != consumerSecret ? consumerSecret
: "";
318 private void log(String message
) {
320 System
.out
.println("[" + new java
.util
.Date() + "]" + message
);
324 private void log(String message
, String message2
) {
326 log(message
+ message2
);
331 public boolean equals(Object o
) {
332 if (this == o
) return true;
333 if (!(o
instanceof OAuth
)) return false;
335 OAuth oAuth
= (OAuth
) o
;
337 if (consumerKey
!= null ?
!consumerKey
.equals(oAuth
.consumerKey
) : oAuth
.consumerKey
!= null)
339 if (consumerSecret
!= null ?
!consumerSecret
.equals(oAuth
.consumerSecret
) : oAuth
.consumerSecret
!= null)
346 public int hashCode() {
347 int result
= consumerKey
!= null ? consumerKey
.hashCode() : 0;
348 result
= 31 * result
+ (consumerSecret
!= null ? consumerSecret
.hashCode() : 0);
353 public String
toString() {
355 "consumerKey='" + consumerKey
+ '\'' +
356 ", consumerSecret='" + consumerSecret
+ '\'' +