市民、認証は義務です

こんばんは。これは、[D言語 Advent Calendar 2012](http://qiita.com/advent-calendar/2012/d)の20日目の記事です。D言語でOAuthなAPI呼び出しをしたい、という人が世の中には居るそうです。という話をします。

##OAuth認証

OAuth認証とは、平和と慈愛に満ちたコンピュータ様の与えて下さる庇護と恩恵であり、それを幸福と感じることは善良な市民たるあなたの義務です。
今どきのWeb APIなんかがこぞって採用していますが、残念なことにD言語にはOAuthライブラリが無いらしいです。仕方ないね。

####トークン

とりあえず、keyとsecretのペアをトークンと呼ぶことにします。などと意味不明な供述をしており、正確な意味はわかりません。

|name|description|
|:---|:---|
|**consumer token** (consumer key, consumer secret)|アプリケーション固有のトークン|
|**request token** (request key, request secret)|*access token* を取得するために必要、使い捨て|
|**access token** (access key, access secret)|ユーザ固有のトークン、ユーザIDとパスワードの代わり|

最終的に必要なのは、*consumer token* と *access token* の2つです。

####手順

ややこしいです。死にます。

1. アプリケーション登録をして *consumer token* をもらう
2. *consumer token* を使って *request token* をもらう
3. 認証用URIに *request key* を付加したURIを開き、PIN番号をもらう
4. *consumer token* と *request token* とPIN番号を使い、*access token* をもらう
5. *consumer token* と *access token* を使ってAPIとキャッキャウフフする

####パラメータ

ややこしいです。死にます。

|key|value|
|:---|:---|
|**oauth_consumer_key**|アプリケーションの登録をしたときに貰える*consumer key* |
|**oautn_nonce**|適当な文字列、例では面倒なのでUnix時間にしてる|
|**oauth_signature**|署名、詳しくは後述|
|**oauth_signature_method**|使用するハッシュ関数の名前、*HMAC-SHA1*|
|**oauth_timestamp**|現在のUnix時間、タイムアウトの判定とかに使われる|
|**oauth_token** *1|*request token* とか *access token* とか|
|**oauth_verifier** *2|名状しがたき値、PIN番号|
|**oauth_version**|バージョン、現在 *1.0* と *2.0* がある|

これに加えて、APIごとに要求されるパラメータというものがあります。例えば、ツイーヨするときは、`status`というキーに呟きたい文字列を渡します。後は、各サービスのリファレンスでも食べてください。

>\*1 手順4で *request token* 、それ以降は *access token* が入る
>\*2 手順4でしか使わない

##認証付き呼び出し

説明するのが億劫なのでコードを載せます。察してください。
今回は、通信にcurlを使用します。また、以下の通りのインポートと構造体の定義をしているものとします。

```d
import std.algorithm, std.array, std.base64, std.conv, std.datetime, std.digest.sha, std.string, std.typecons, std.uri;
import std.net.curl, std.stdio;
 
alias Tuple!(string, "key", string, "secret") Token;
```

####署名の生成

トークンとHTTPメソッド名とURIとパラメータから生成します。

```d
// パラメータをキーでソートし、キーと値をそれぞれ=と&で繋ぐ
auto query = param.keys.sort.map!(k => k ~ "=" ~ param[k]).join("&");

// consumer secretとtoken secretをそれぞれURLエンコードし、&で繋ぐ
auto key = [consumer.secret, token.secret].map!encodeComponent.join("&");

// メソッド名とURIと上記のqueryをそれぞれURLエンコードし、&で繋ぐ
auto base = [method, uri, query].map!encodeComponent.join("&");

// HMAC-SHA1ハッシュ関数にkeyとbaseを渡してダイジェスト値を求め、Base64エンコードし、URLエンコードする
auto signature = encodeComponent(cast(immutable)Base64.encode(hmac_sha1(key, base)));
```

`hmac_sha1`関数は、`std.digest.sha.sha1Of`関数を利用して自分で作成したものです。実装は後で出します。

####OAuthな呼び出し

署名などをHTTPヘッダのAuthorizationフィールドに乗せて、HTTPメソッドを繰り出します。

```d
// 適当にAPIの呼び出しに使うパラメータ
auto param = ["mami": "candeloro", "saya": "oktavia", "mado": "kriemhild gretchen"];

// OAuthなパラメータをつっこむ
auto parameter = ["oauth_consumer_key": consumer.key,
	"oauth_nonce": Clock.currTime.toUnixTime.to!string,
	"oauth_signature_method": "HMAC-SHA1",
	"oauth_timestamp": Clock.currTime.toUnixTime.to!string,
	"oauth_version": "1.0"];

// パラメータ同士を合わせる
// ついでに日本語来ると困るのでURLエンコードしとく
foreach (k, v; param)
	parameter[k] = v.encodeComponent;

// token keyが存在しているならパラメータに含める
if (!(token.key is null))
	parameter["oauth_token"] = token.key;

// 署名を生成し、パラメータに含める
auto signature = ……;
parameter["oauth_signature"] = signature;

// OAuthなパラメータを抜き出し、それぞれ=と,で繋ぐ
auto authorize_keys = parameter.keys.filter!q{a.countUntil("oauth_")==0};
auto authorize = "OAuth " ~ authorize_keys.map!(x => x ~ "=" ~ parameter[x]).join(",");

// それ以外のパラメータは、それぞれ=と&で繋ぐ
auto option_keys = parameter.keys.filter!q{a.countUntil("oauth_")!=0};
auto option = option_keys.map!(x => x ~ "=" ~ parameter[x]).join("&");

auto http = HTTP();
// Authorizationフィールドにパラメータを渡す
http.addRequestHeader("Authorization", authorize);
// URIの後ろに残りのパラメータを付与してGET
auto result = cast(immutable)get((0 < option.length)? uri ~ "?" ~ option: uri, http);
```

##実装例

ツイーヨするコード。

```d
// dmd 2.062import std.algorithm, std.array, std.base64, std.conv, std.datetime, std.digest.sha, std.string, std.typecons, std.uri;import std.net.curl, std.stdio;alias Tuple!(string, "key", string, "secret") Token;private string signature(in Token consumer, in Token token, in string method, in string uri,
		in string[string] param) {	auto query = param.keys.sort.map!(k => k ~ "=" ~ param[k]).join("&");	auto key = [consumer.secret, token.secret].map!encodeComponent.join("&");	auto base = [method, uri, query].map!encodeComponent.join("&");	return encodeComponent(cast(immutable)Base64.encode(hmac_sha1(key, base)));}private string signedCall(in Token consumer, in Token token, in string method, in string uri,
		in string[string] param, string delegate(HTTP, in string, in string) call) {	auto parameter = ["oauth_consumer_key": consumer.key,		"oauth_nonce": Clock.currTime.toUnixTime.to!string,		"oauth_signature_method": "HMAC-SHA1",		"oauth_timestamp": Clock.currTime.toUnixTime.to!string,		"oauth_version": "1.0"];	foreach (k, v; param)		parameter[k] = v.encodeComponent;	if (!(token.key is null))		parameter["oauth_token"] = token.key;	auto signature = signature(consumer, token, method, uri, parameter);	parameter["oauth_signature"] = signature;	auto authorize_keys = parameter.keys.filter!q{a.countUntil("oauth_")==0};	auto authorize = "OAuth " ~ authorize_keys.map!(x => x ~ "=" ~ parameter[x]).join(",");	auto option_keys = parameter.keys.filter!q{a.countUntil("oauth_")!=0};	auto option = option_keys.map!(x => x ~ "=" ~ parameter[x]).join("&");	auto http = HTTP();	http.addRequestHeader("Authorization", authorize);	return call(http, uri, option);}string signedGet(in Token consumer, in Token token, in string uri, in string[string] param = null) {	return signedCall(consumer, token, "GET", uri, param, (http, uri, option) {		return cast(immutable)get((0 < option.length)? uri ~ "?" ~ option: uri, http);	});}string signedPost(in Token consumer, in Token token, in string uri, in string[string] param = null) {	return signedCall(consumer, token, "POST", uri, param, (http, uri, option) {		return cast(immutable)post(uri, option, http);	});}private ubyte[] hmac_sha1(in string key, in string message) {	auto padding(in ubyte[] k) {		auto h = (64 < k.length)? sha1Of(k): k;		return h ~ new ubyte[64 - h.length];	}	const k = padding(cast(ubyte[])key);	return sha1Of((k.map!q{cast(ubyte)(a^0x5c)}.array)			~ sha1Of((k.map!q{cast(ubyte)(a^0x36)}.array) ~ cast(ubyte[])message)).dup;}private Token toToken(string s) {	string[string] result;	foreach (x; s.split("&").map!q{a.split("=")})		result[x[0]] = x[1];	return Token(result["oauth_token"], result["oauth_token_secret"]);}void main() {	immutable consumer		= Token("replace your consumer key", "replace your consumer secret");		auto request_token = signedGet(consumer, Token(),        "https://api.twitter.com/oauth/request_token").toToken;	auto authorize_uri = "https://api.twitter.com/oauth/authorize" ~ "?oauth_token=" ~ request_token.key;	authorize_uri.writeln;	auto verifier = readln.chomp;	auto access_token = signedGet(consumer, request_token,        "https://api.twitter.com/oauth/access_token", ["oauth_verifier": verifier]).toToken;    signedPost(consumer, access_token,
        "https://api.twitter.com/1.1/statuses/update.json", ["status": "市民、認証は義務です"]);
}```

##終わりに

朦朧とした意識の中書いた文章とコードなのでところどころ頭が溶けている感じもしますが、
どうしてこんなことになったのか、私にはわかりません。
これをあなたが読んだなら、その時、私は寝ているでしょう。
記事があるか、ないかの違いはあるでしょうが。
これを読んだあなた。どうかライブラリを書いてください。それだけが、私の望みです。

次の21日目は、[@Fuhduki](http://qiita.com/users/Fuhduki)さんです。蛙。