1 /**
2  * ae.net.oauth.common
3  *
4  * I have no idea what I'm doing.
5  * Please don't use this module.
6  *
7  * License:
8  *   This Source Code Form is subject to the terms of
9  *   the Mozilla Public License, v. 2.0. If a copy of
10  *   the MPL was not distributed with this file, You
11  *   can obtain one at http://mozilla.org/MPL/2.0/.
12  *
13  * Authors:
14  *   Vladimir Panteleev <ae@cy.md>
15  */
16 
17 module ae.net.oauth.common;
18 
19 import std.algorithm.sorting;
20 import std.base64;
21 import std.conv;
22 import std.datetime;
23 import std.digest.hmac;
24 import std.digest.sha;
25 
26 import ae.net.ietf.url;
27 import ae.utils.text;
28 
29 debug(OAUTH) import std.stdio : stderr;
30 
31 /// OAuth configuration.
32 struct OAuthConfig
33 {
34 	string consumerKey;    ///
35 	string consumerSecret; ///
36 }
37 
38 /**
39    Implements an OAuth client session.
40 
41    Example:
42    ---
43    OAuthSession session;
44    session.config.consumerKey    = "(... obtain from service ...)";
45    session.config.consumerSecret = "(... obtain from service ...)";
46    session.token                 = "(... obtain from service ...)";
47    session.tokenSecret           = "(... obtain from service ...)";
48 
49    ...
50 
51    UrlParameters parameters;
52    parameters["payload"] = "(... some data here ...)";
53    auto request = new HttpRequest;
54    auto queryString = parameters.byPair.map!(p => session.encode(p.key) ~ "=" ~ session.encode(p.value)).join("&");
55    auto baseURL = "https://api.example.com/endpoint";
56    auto fullURL = baseURL ~ "?" ~ queryString;
57    request.resource = fullURL;
58    request.method = "POST";
59    request.headers["Authorization"] = session.prepareRequest(baseURL, "POST", parameters).oauthHeader;
60    httpRequest(request, null);
61    ---
62 */
63 struct OAuthSession
64 {
65 	OAuthConfig config; ///
66 
67 	///
68 	string token, tokenSecret;
69 
70 	/// Signs a request and returns the relevant parameters for the "Authorization" header.
71 	UrlParameters prepareRequest(string requestUrl, string method, UrlParameters[] parameters...)
72 	{
73 		UrlParameters oauthParams;
74 		oauthParams["oauth_consumer_key"] = config.consumerKey;
75 		oauthParams["oauth_token"] = token;
76 		oauthParams["oauth_timestamp"] = Clock.currTime().toUnixTime().text();
77 		oauthParams["oauth_nonce"] = randomString();
78 		oauthParams["oauth_version"] = "1.0";
79 		oauthParams["oauth_signature_method"] = "HMAC-SHA1";
80 		oauthParams["oauth_signature"] = signRequest(method, requestUrl, parameters ~ oauthParams);
81 		return oauthParams;
82 	}
83 
84 	/// Calculates the signature for a request.
85 	string signRequest(string method, string requestUrl, UrlParameters[] parameters...)
86 	{
87 		string paramStr;
88 		bool[string] keys;
89 
90 		foreach (set; parameters)
91 			foreach (key, value; set)
92 				keys[key] = true;
93 
94 		foreach (key; keys.keys.sort())
95 		{
96 			string[] values;
97 			foreach (set; parameters)
98 				foreach (value; set.valuesOf(key).sort())
99 					values ~= value;
100 
101 			foreach (value; values.sort())
102 			{
103 				if (paramStr.length)
104 					paramStr ~= '&';
105 				paramStr ~= encode(key) ~ "=" ~ encode(value);
106 			}
107 		}
108 
109 		auto str = encode(method) ~ "&" ~ encode(requestUrl) ~ "&" ~ encode(paramStr);
110 		debug(OAUTH) stderr.writeln("Signature base string: ", str);
111 
112 		auto key = encode(config.consumerSecret) ~ "&" ~ encode(tokenSecret);
113 		debug(OAUTH) stderr.writeln("Signing key: ", key);
114 		auto digest = hmac!SHA1(cast(ubyte[])str, cast(ubyte[])key);
115 		return Base64.encode(digest);
116 	}
117 
118 	unittest
119 	{
120 		// Example from https://dev.twitter.com/oauth/overview/creating-signatures
121 
122 		OAuthSession session;
123 		session.config.consumerSecret = "kAcSOqF21Fu85e7zjz7ZN2U4ZRhfV3WpwPAoE3Z7kBw";
124 		session.tokenSecret = "LswwdoUaIvS8ltyTt5jkRh4J50vUPVVHtR2YPi5kE";
125 
126 		UrlParameters getVars, postVars, oauthVars;
127 		getVars["include_entities"] = "true";
128 		postVars["status"] = "Hello Ladies + Gentlemen, a signed OAuth request!";
129 
130 		oauthVars["oauth_consumer_key"] = "xvz1evFS4wEEPTGEFPHBog";
131 		oauthVars["oauth_nonce"] = "kYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg";
132 		oauthVars["oauth_signature_method"] = "HMAC-SHA1";
133 		oauthVars["oauth_timestamp"] = "1318622958";
134 		oauthVars["oauth_token"] = "370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb";
135 		oauthVars["oauth_version"] = "1.0";
136 
137 		auto signature = session.signRequest("POST", "https://api.twitter.com/1/statuses/update.json", getVars, postVars, oauthVars);
138 		assert(signature == "tnnArxj06cWHq44gCs1OSKk/jLY=");
139 	}
140 
141 	/// Alias to `oauthEncode`.
142 	alias encode = oauthEncode;
143 }
144 
145 /// Converts OAuth parameters into a string suitable for the "Authorization" header.
146 string oauthHeader(UrlParameters oauthParams)
147 {
148 	string s;
149 	foreach (key, value; oauthParams)
150 		s ~= (s.length ? ", " : "") ~ key ~ `="` ~ oauthEncode(value) ~ `"`;
151 	return "OAuth " ~ s;
152 }
153 
154 static import std.ascii;
155 /// Performs URL encoding as required by OAuth.
156 static alias oauthEncode = encodeUrlPart!(c => std.ascii.isAlphaNum(c) || c=='-' || c=='.' || c=='_' || c=='~');