diff --git a/oauth.d b/oauth.d deleted file mode 120000 index e7a538c..0000000 --- a/oauth.d +++ /dev/null @@ -1 +0,0 @@ -../../work/oauth.d \ No newline at end of file diff --git a/oauth.d b/oauth.d new file mode 100644 index 0000000..83e6026 --- /dev/null +++ b/oauth.d @@ -0,0 +1,698 @@ +module arsd.oauth; + +import arsd.curl; +import arsd.cgi; // for decodeVariables +import std.array; +static import std.uri; +static import std.algorithm; +import std.conv; +import std.string; +import std.random; +import std.base64; +import std.exception; +import std.datetime; + + + +/////////////////////////////////////// + +class FacebookApiException : Exception { + public this(string response, string token = null, string scopeRequired = null) { + this.token = token; + this.scopeRequired = scopeRequired; + super(response ~ "\nToken: " ~ token ~ "\nScope: " ~ scopeRequired); + } + + string token; + string scopeRequired; +} + + +import arsd.curl; +import arsd.sha; +import std.md5; + +import std.file; + + +Variant[string] postToFacebookWall(string[] info, string id, string message, string picture = null, string link = null) { + string url = "https://graph.facebook.com/" ~ id ~ "/feed"; + + + string data = "access_token=" ~ std.uri.encodeComponent(info[1]); + data ~= "&message=" ~ std.uri.encodeComponent(message); + + if(picture !is null && picture.length) + data ~= "&picture=" ~ std.uri.encodeComponent(picture); + if(link !is null && link.length) + data ~= "&link=" ~ std.uri.encodeComponent(link); + + auto response = curl(url, data); + + auto res = jsonToVariant(response); +/+ +{"error":{"type":"OAuthException","message":"An active access token must be used to query information about the current user."}} ++/ + // assert(0, response); + + auto var = res.get!(Variant[string]); + + + if("error" in var) { + auto error = var["error"].get!(Variant[string]); + + throw new FacebookApiException(error["message"].get!string, info[1], + "scope" in error ? error["scope"].get!string : "publish_stream"); + } + + return var; + +} + +// note ids=a,b,c works too. it returns an associative array of the ids requested. +Variant[string] fbGraph(string[] info, string id, bool useCache = false, long maxCacheHours = 2) { + string response; + + string cacheFile; + + char c = '?'; + + if(id.indexOf("?") != -1) + c = '&'; + + string url; + + if(id[0] != '/') + id = "/" ~ id; + + if(info !is null) + url = "https://graph.facebook.com" ~ id + ~ c ~ "access_token=" ~ info[1] ~ "&format=json"; + else + url = "http://graph.facebook.com" ~ id + ~ c ~ "format=json"; + + + + if(useCache) + cacheFile = "/tmp/fbGraphCache-" ~ hashToString(SHA1(url)); + + if(useCache) { + if(std.file.exists(cacheFile)) { + if((Clock.currTime() - std.file.timeLastModified(cacheFile)) < dur!"hours"(maxCacheHours)) { + response = std.file.readText(cacheFile); + goto haveResponse; + } + } + } + + try { + response = curl(url); + } catch(CurlException e) { + throw new FacebookApiException(e.msg); + } + + if(useCache) { + std.file.write(cacheFile, response); + } + + haveResponse: + assert(response.length); + + if(response == "false") { + //throw new Exception("This page is private. Please make it public in Facebook."); + // we'll make dummy data so this still returns + + Variant[string] ret; + + Variant v1 = id[1..$]; + ret["id"] = v1; + ret["name"] = v1 = "Private"; + ret["description"] = v1 = "This is a private facebook page. Please make it public in Facebook if you want to promote it."; + ret["link"] = v1 = "http://facebook.com?profile.php?id=" ~ id[1..$]; + ret["is_false"] = true; + return ret; + } + + auto res = jsonToVariant(response); +/+ +{"error":{"type":"OAuthException","message":"An active access token must be used to query information about the current user."}} ++/ + // assert(0, response); + + auto var = res.get!(Variant[string]); + + if("error" in var) { + auto error = var["error"].get!(Variant[string]); + + if("message" in error) + throw new FacebookApiException(error["message"].get!string, info.length > 1 ? info[1] : null, + "scope" in error ? error["scope"].get!string : null); + else + throw new FacebookApiException("couldn't get FB info"); + } + + return var; +} + + + +string[string][] getBasicDataFromVariant(Variant[string] v) { + auto items = v["data"].get!(Variant[]); + return getBasicDataFromVariant(items); +} + +string[string][] getBasicDataFromVariant(Variant[] items) { + string[string][] ret; + + foreach(item; items) { + auto i = item.get!(Variant[string]); + + string[string] l; + + foreach(k, v; i) { + l[k] = to!string(v); + } + + ret ~= l; + } + + return ret; +} + + +///////////////////////////////////// + + + + + + + + + + + +/* ******************************* */ + +/* OAUTH 1.0 */ + +/* ******************************* */ + + +struct OAuthParams { + string apiKey; + string apiSecret; + string baseUrl; + string requestTokenPath; + string accessTokenPath; + string authorizePath; +} + +OAuthParams twitter(string apiKey, string apiSecret) { + OAuthParams params; + + params.apiKey = apiKey; + params.apiSecret = apiSecret; + + //params.baseUrl = "https://api.twitter.com"; + params.baseUrl = "http://twitter.com"; + params.requestTokenPath = "/oauth/request_token"; + params.authorizePath = "/oauth/authorize"; + params.accessTokenPath = "/oauth/access_token"; + + return params; +} + +OAuthParams linkedIn(string apiKey, string apiSecret) { + OAuthParams params; + + params.apiKey = apiKey; + params.apiSecret = apiSecret; + + params.baseUrl = "https://api.linkedin.com"; + params.requestTokenPath = "/uas/oauth/requestToken"; + params.accessTokenPath = "/uas/oauth/accessToken"; + params.authorizePath = "/uas/oauth/authorize"; + + return params; +} + +OAuthParams aWeber(string apiKey, string apiSecret) { + OAuthParams params; + + params.apiKey = apiKey; + params.apiSecret = apiSecret; + + params.baseUrl = "https://auth.aweber.com"; + params.requestTokenPath = "/1.0/oauth/request_token"; + params.accessTokenPath = "/1.0/oauth/access_token"; + params.authorizePath = "/1.0/oauth/authorize"; + + // API Base: https://api.aweber.com/1.0/ + + return params; +} + + +string tweet(OAuthParams params, string oauthToken, string tokenSecret, string message) { + assert(oauthToken.length); + assert(tokenSecret.length); + + auto args = [ + "oauth_token" : oauthToken, + "token_secret" : tokenSecret, + ]; + + auto data = encodeVariables(["status" : message]); + + auto ret = curlOAuth(params, "http://api.twitter.com" ~ "/1/statuses/update.json", args, "POST", data); + + auto val = jsonToVariant(ret).get!(Variant[string]); + return val["id_str"].get!string; +} + +import std.file; +/** + Redirects the user to the authorize page on the provider's website. +*/ +void authorizeStepOne(Cgi cgi, OAuthParams params, string oauthCallback = null) { + if(oauthCallback is null) { + oauthCallback = cgi.getCurrentCompleteUri(); + if(oauthCallback.indexOf("?") == -1) + oauthCallback ~= "?oauth_step=two"; + else + oauthCallback ~= "&oauth_step=two"; + } + + string[string] args; + if(oauthCallback.length) + args["oauth_callback"] = oauthCallback; + + auto ret = curlOAuth(params, params.baseUrl ~ params.requestTokenPath, + args, "POST", "", ""); + auto vals = decodeVariables(ret); + + if("oauth_problem" in vals) + throw new Exception("OAuth problem: " ~ vals["oauth_problem"][0]); + + if(vals.keys.length < 2) + throw new Exception(ret); + + ///vals["fuck_you"] = [params.baseUrl ~ params.requestTokenPath]; + + auto oauth_token = vals["oauth_token"][0]; + auto oauth_secret = vals["oauth_token_secret"][0]; + + // need to save the secret for later + std.file.write("/tmp/oauth-token-secret-" ~ oauth_token, + oauth_secret); + + cgi.setResponseLocation(params.baseUrl ~ params.authorizePath ~ "?oauth_token=" ~ oauth_token); +} + +/** + Gets the final token, given the stuff from step one. This should be called + from the callback in step one. + + Returns [token, secret] +*/ +string[] authorizeStepTwo(const(Cgi) cgi, OAuthParams params) { + if("oauth_problem" in cgi.get) + throw new Exception("OAuth problem: " ~ cgi.get["oauth_problem"]); + + string token = cgi.get["oauth_token"]; + string verifier = cgi.get["oauth_verifier"]; + + // reload from file written above. FIXME: clean up old shit too + string secret = std.file.readText("/tmp/oauth-token-secret-" ~ token); + // don't need it anymore... + std.file.remove("/tmp/oauth-token-secret-" ~ token); + + + auto ret = curlOAuth(params, params.baseUrl ~ params.accessTokenPath, + ["oauth_token" : token, + "oauth_verifier" : verifier, + "token_secret" : secret], "POST", "", ""); + + auto vars = decodeVariables(ret); + + return [vars["oauth_token"][0], vars["oauth_token_secret"][0]]; +} + + + +/** + Note in oauthValues: + It creates the nonce, signature_method, version, consumer_key, and timestamp + ones inside this function - you don't have to do it. + + Just put in the values specific to your call. + + oauthValues["token_secret"] if present, is concated into the signing string. Don't + put it in for the early steps! +*/ + +import core.stdc.stdlib; + +string curlOAuth(OAuthParams auth, string url, string[string] oauthValues, string method = null,string data = null, string contentType = "application/x-www-form-urlencoded") { + + //string oauth_callback; // from user + + oauthValues["oauth_consumer_key"] = auth.apiKey; + oauthValues["oauth_nonce"] = makeNonce(); + oauthValues["oauth_signature_method"] = "HMAC-SHA1"; + + oauthValues["oauth_timestamp"] = to!string(Clock.currTime().toUTC().toUnixTime()); + oauthValues["oauth_version"] = "1.0"; + + auto questionMark = std.string.indexOf(url, "?"); + + string signWith = std.uri.encodeComponent(auth.apiSecret) ~ "&"; + if("token_secret" in oauthValues) { + signWith ~= std.uri.encodeComponent(oauthValues["token_secret"]); + oauthValues.remove("token_secret"); + } + + if(method is null) + method = data is null ? "GET" : "POST"; + + auto baseString = getSignatureBaseString( + method, + questionMark == -1 ? url : url[0..questionMark], + questionMark == -1 ? "" : url[questionMark+1 .. $], + oauthValues, + contentType == "application/x-www-form-urlencoded" ? data : null + ); + + string oauth_signature = /*std.uri.encodeComponent*/(cast(string) + Base64.encode(mhashSign(baseString, signWith, MHASH_SHA1))); + + oauthValues["oauth_signature"] = oauth_signature; + + string oauthHeader; + bool outputted = false; + Pair[] pairs; + foreach(k, v; oauthValues) { + pairs ~= Pair(k, v); + } + + foreach(pair; std.algorithm.sort(pairs)) { + if(outputted) + oauthHeader ~= ", "; + else + outputted = true; + + oauthHeader ~= pair.output(true); + } + + return curlAuth(url, data, null, null, contentType, method, ["Authorization: OAuth " ~ oauthHeader]); +} + +bool isOAuthRequest(Cgi cgi) { + if(cgi.authorization.length < 5 || cgi.authorization[0..5] != "OAuth") + return false; + return true; +} + +string getApiKeyFromRequest(Cgi cgi) { + enforce(isOAuthRequest(cgi)); + auto variables = split(cgi.authorization[6..$], ","); + + foreach(var; variables) + if(var.startsWith("oauth_consumer_key")) + return var["oauth_consumer_key".length + 3 .. $ - 1]; // trimming quotes too + throw new Exception("api key not present"); +} + +bool isSignatureValid(Cgi cgi, string apiSecret, string tokenSecret) { + enforce(isOAuthRequest(cgi)); + auto variables = split(cgi.authorization[6..$], ","); + + string[string] oauthValues; + foreach(var; variables) { + auto it = var.split("="); + oauthValues[it[0]] = it[1][1 .. $ - 1]; // trimming quotes + } + + auto url = cgi.getCurrentCompleteUri(); + + auto questionMark = std.string.indexOf(url, "?"); + + string signWith = std.uri.encodeComponent(apiSecret) ~ "&"; + if(tokenSecret.length) + signWith ~= std.uri.encodeComponent(tokenSecret); + + auto method = to!string(cgi.requestMethod); + + if("oauth_signature" !in oauthValues) + return false; + + auto providedSignature = oauthValues["oauth_signature"]; + + oauthValues.remove("oauth_signature"); + + string oauth_signature = std.uri.encodeComponent(cast(string) + Base64.encode(mhashSign( + getSignatureBaseString( + method, + questionMark == -1 ? url : url[0..questionMark], + questionMark == -1 ? "" : url[questionMark+1 .. $], + oauthValues, + cgi.postArray // FIXME: if this was a file upload, this isn't actually right + ), signWith, MHASH_SHA1))); + + return oauth_signature == providedSignature; + +} + +string makeNonce() { + auto val = to!string(uniform(uint.min, uint.max)) ~ to!string(Clock.currTime().stdTime); + + return val; +} + +struct Pair { + string name; + string value; + + string output(bool useQuotes = false) { + if(useQuotes) + return std.uri.encodeComponent(name) ~ "=\"" ~ std.uri.encodeComponent(value) ~ "\""; + else + return std.uri.encodeComponent(name) ~ "=" ~ std.uri.encodeComponent(value); + } + + int opCmp(Pair rhs) { + // FIXME: is name supposed to be encoded? + int val = std.string.cmp(name, rhs.name); + + if(val == 0) + val = std.string.cmp(value, rhs.value); + + return val; + } +} +string getSignatureBaseString( + string method, + string protocolHostAndPath, + string queryStringContents, + string[string] authorizationHeaderContents, + in string[][string] postArray) +{ + string baseString; + + baseString ~= method; + baseString ~= "&"; + baseString ~= std.uri.encodeComponent(protocolHostAndPath); + baseString ~= "&"; + + auto getArray = decodeVariables(queryStringContents); + + Pair[] pairs; + + foreach(k, vals; getArray) + foreach(v; vals) + pairs ~= Pair(k, v); + foreach(k, vals; postArray) + foreach(v; vals) + pairs ~= Pair(k, v); + foreach(k, v; authorizationHeaderContents) + pairs ~= Pair(k, v); + + bool outputted = false; + + string params; + foreach(pair; std.algorithm.sort(pairs)) { + if(outputted) + params ~= "&"; + else + outputted = true; + params ~= pair.output(); + } + + baseString ~= std.uri.encodeComponent(params); + + return baseString; +} + + +string getSignatureBaseString( + string method, + string protocolHostAndPath, + string queryStringContents, + string[string] authorizationHeaderContents, + string postBodyIfWwwEncoded) +{ + return getSignatureBaseString( + method, + protocolHostAndPath, + queryStringContents, + authorizationHeaderContents, + decodeVariables(postBodyIfWwwEncoded)); +} + +/***************************************/ + +// OAuth 2.0 as used by Facebook // + +/***************************************/ + +immutable(ubyte)[] base64UrlDecode(string e) { + string encoded = e.idup; + while (encoded.length % 4) { + encoded ~= "="; // add padding + } + + // convert base64 URL to standard base 64 + encoded = encoded.replace("-", "+"); + encoded = encoded.replace("_", "/"); + + auto ugh = Base64.decode(encoded); + return assumeUnique(ugh); +} + +Variant parseSignedRequest(in string req, string apisecret) { + auto parts = req.split("."); + + immutable signature = parts[0]; + immutable jsonEncoded = parts[1]; + + auto expected = mhashSign(jsonEncoded, apisecret, MHASH_SHA256); + auto got = base64UrlDecode(signature); + + enforce(expected == got, "Signatures didn't match"); + + auto json = cast(string) base64UrlDecode(jsonEncoded); + + return jsonToVariant(json); +} + + +/****************************************/ + +// Generic helper functions for web work + +/****************************************/ + +import std.variant; +import std.json; + +Variant jsonToVariant(string json) { + auto decoded = parseJSON(json); + return jsonValueToVariant(decoded); +} + +Variant jsonValueToVariant(JSONValue v) { + Variant ret; + + final switch(v.type) { + case JSON_TYPE.STRING: + ret = v.str; + break; + // FIXME FIXME FIXME + version(live) {} else { + case JSON_TYPE.UINTEGER: + ret = v.uinteger; + break; + } + case JSON_TYPE.INTEGER: + ret = v.integer; + break; + case JSON_TYPE.FLOAT: + ret = v.floating; + break; + case JSON_TYPE.OBJECT: + Variant[string] obj; + foreach(k, val; v.object) { + obj[k] = jsonValueToVariant(val); + } + + ret = obj; + break; + case JSON_TYPE.ARRAY: + Variant[] arr; + foreach(i; v.array) { + arr ~= jsonValueToVariant(i); + } + + ret = arr; + break; + case JSON_TYPE.TRUE: + ret = true; + break; + case JSON_TYPE.FALSE: + ret = false; + break; + case JSON_TYPE.NULL: + ret = null; + break; + } + + return ret; +} + +/***************************************/ + +// Interface to C lib for signing + +/***************************************/ + +extern(C) { + alias int hashid; + MHASH mhash_hmac_init(hashid, in void*, int, int); + bool mhash(in void*, in void*, int); + int mhash_get_hash_pblock(hashid); + byte* mhash_hmac_end(MHASH); + int mhash_get_block_size(hashid); + + hashid MHASH_MD5 = 1; + hashid MHASH_SHA1 = 2; + hashid MHASH_SHA256 = 17; + alias void* MHASH; +} + +ubyte[] mhashSign(string data, string signWith, hashid algorithm) { + auto td = mhash_hmac_init(algorithm, signWith.ptr, cast(int) signWith.length, + mhash_get_hash_pblock(algorithm)); + + mhash(td, data.ptr, cast(int) data.length); + auto mac = mhash_hmac_end(td); + ubyte[] ret; + + for (int j = 0; j < mhash_get_block_size(algorithm); j++) { + ret ~= cast(ubyte) mac[j]; + } + +/* + string ret; + + for (int j = 0; j < mhash_get_block_size(algorithm); j++) { + ret ~= std.string.format("%.2x", mac[j]); + } +*/ + + return ret; +} + +pragma(lib, "mhash");