前書き
「ライブラリで何してんのか分からないから気持ち悪いんじゃヴォエエエ」 って人は読んでみるといいと思うよ(震え声)
HTTP通信について全く知らない人は、先に以下のリンク先を読んでおいてください。
※ HTTP通信における改行コードは全て \r\n
です。
GET /1.1/statuses/show.json?id=464526017030545409 HTTP/1.1
Host: api.twitter.com
Authorization: OAuth oauth_consumer_key="***", oauth_signature_method="HMAC-SHA1", oauth_timestamp="***", oauth_version="1.0a", oauth_nonce="***", oauth_signature="***", oauth_token="***"
Connection: close
POST /1.1/statuses/update.json HTTP/1.1
Host: api.twitter.com
Authorization: OAuth oauth_consumer_key="***", oauth_signature_method="HMAC-SHA1", oauth_timestamp="***", oauth_version="1.0a", oauth_nonce="***", oauth_signature="***", oauth_token="***"
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 11
status=test
POST /1.1/statuses/update_with_media.json HTTP/1.1
Host: api.twitter.com
Authorization: OAuth oauth_consumer_key="***", oauth_signature_method="HMAC-SHA1", oauth_timestamp="***", oauth_version="1.0a", oauth_nonce="***", oauth_signature="***", oauth_token="***"
Connection: close
Content-Type: multipart/form-data; boundary=-----hogehoge
Content-Length: 232
-------hogehoge
Content-Disposition: form-data; name="status"
test
-------hogehoge
Content-Disposition: form-data; name="media[]"; filename="test.png"
Content-Type: application/octet-stream
臼NG........
-------hogehoge--
OAuth認証の流れ
ライブラリを使ってTwitter連携機能を実装したことのある人なら分かりきっていると思いますが、念のため分かりやすい図を@ITさんの記事より引用させていただきます。現状メインで使われているのはやはりOAuth1.0および1.0aなので、ここではそれに限定して説明していきます。
oauth_signature
生成のために必要なパラメータ
ここでは便宜上
- 送信する情報の本体
→ 「ベース」 - 送信する情報の正当性を保証する署名に使われる部分
→ 「キー」
と呼ぶことにします。
oauth/request_token
の場合
ベース
要素名 | 説明 |
---|---|
oauth_consumer_key | コンシューマーキー |
oauth_signature_method | "HMAC-SHA1" |
oauth_timestamp | タイムスタンプ |
oauth_version | "1.0a" |
oauth_nonce |
bin2hex(openssl_random_pseudo_bytes(16)) で生成した乱数ハッシュなど |
oauth_callback | 認証後にリダイレクトして戻ってくるURL |
-
oauth_callback
は省略可能です。省略した場合はコンシューマーキー登録時のものが使用されます。 -
oauth_callback
に"oob"
を指定した場合、リダイレクトする代わりにPIN入力になります。
キー
オフセット | 説明 |
---|---|
0 | コンシューマーシークレット |
1 | "" (空文字列) |
oauth/access_token
の場合
ベース
要素名 | 説明 |
---|---|
oauth_consumer_key | コンシューマーキー |
oauth_signature_method | "HMAC-SHA1" |
oauth_timestamp | タイムスタンプ |
oauth_version | "1.0a" |
oauth_nonce |
bin2hex(openssl_random_pseudo_bytes(16)) で生成した乱数ハッシュなど |
oauth_verifier | ベリファイア(PIN) |
oauth_token | リクエストトークン |
キー
オフセット | 説明 |
---|---|
0 | コンシューマーシークレット |
1 | リクエストトークンシークレット |
その他のリクエストの場合(通常)
ベース
※ 追加パラメータは oauth_signature
生成より後には除外されます。
要素名 | 説明 |
---|---|
oauth_consumer_key | コンシューマーキー |
oauth_signature_method | "HMAC-SHA1" |
oauth_timestamp | タイムスタンプ |
oauth_version | "1.0a" |
oauth_nonce |
bin2hex(openssl_random_pseudo_bytes(16)) で生成した乱数ハッシュなど |
oauth_token | アクセストークン |
パラメータ1の名称 | パラメータ1の値 |
パラメータ2の名称 | パラメータ2の値 |
パラメータ3の名称 | パラメータ3の値 |
... | ... |
キー
オフセット | 説明 |
---|---|
0 | コンシューマーシークレット |
1 | アクセストークンシークレット |
その他のリクエストの場合(マルチパート)
ベース
要素名 | 説明 |
---|---|
oauth_consumer_key | コンシューマーキー |
oauth_signature_method | "HMAC-SHA1" |
oauth_timestamp | タイムスタンプ |
oauth_version | "1.0a" |
oauth_nonce |
bin2hex(openssl_random_pseudo_bytes(16)) で生成した乱数ハッシュなど |
oauth_token | アクセストークン |
キー
オフセット | 説明 |
---|---|
0 | コンシューマーシークレット |
1 | アクセストークンシークレット |
Authorization
ヘッダー生成の手順
上記の 「その他のリクエストの場合(通常)」 で POST statuses/update を利用する場合の具体例を示します。
1. ベースとキーの準備
// 最後まで使われる基本パラメータ
$oauth_params = [
'oauth_consumer_key' => 'コンシューマーキー',
'oauth_signature_method' => 'HMAC-SHA1',
'oauth_timestamp' => time(),
'oauth_version' => '1.0a',
'oauth_nonce' => bin2hex(openssl_random_pseudo_bytes(16)),
'oauth_token' => 'アクセストークン',
];
// oauth_signature生成まで使われる追加パラメータ
$additional_params = [
'status' => 'ツイート内容',
'in_reply_to_status_id' => 'リプライ先ステータスID',
];
// ベース
$base = $oauth_params + $additional_params;
// キー
$key = ['コンシューマーシークレット', 'アクセストークンシークレット'];
2. ベースを要素名自然順でソートする
通常のソートだと
a1, a10, a2, a3, a4, a5, a6, a7, a8, a9
となってしまうところを
a1, a2, a3, a4, a5, a6, a7, a8, a9, a10
とするのが自然順ソートです。以下のように記述します。
uksort($base, 'strnatcmp');
実際にこのような差異がつくパラメータ名はTwitterAPIには採用されていないので、
ksort($base);
と書いても特に問題ないかもしれません。
3. ベースをRFC3986に従ってクエリストリングにする
$base = http_build_query($base, '', '&', PHP_QUERY_RFC3986);
4. 所定の手順に従ってベースを再構成する
※ GETリクエストの場合、この「URL」にクエリストリングは含めません。
// リクエストメソッド、URL、ベースからなる配列を作る
$base = ['POST', 'https://api.twitter.com/1.1/statuses/update.json', $base];
// RFC3986に従ったURLエンコードを適用
$base = array_map('rawurlencode', $base);
// 「&」で結合する
$base = implode('&', $base);
5. 所定の手順に従ってキーを再構成する
// RFC3986に従ったURLエンコードを適用
$key = array_map('rawurlencode', $key);
// 「&」で結合する
$key = implode('&', $key);
6. oauth_signature
を生成して基本パラメータに追加する
HMACとかSHA-1とか聞かれても答えられないので暗号アルゴリズムについて自力で模索するぐらい意識の高い人以外は おまじない として軽くスルーしてください。Twitterがこの方法で署名を作れとしているので、単にそれに従っているだけです。
$oauth_params['oauth_signature'] = base64_encode(hash_hmac('sha1', $base, $key, true));
7. ヘッダーの形に仕上げて完成
foreach ($oauth_params as $name => $value) {
$items[] = sprintf('%s="%s"', urlencode($name), urlencode($value));
}
$header = 'Authorization: OAuth ' . implode(', ', $items);
ここまでの手順を最短でまとめると以下のようになります。
uksort($base, 'strnatcmp');
$oauth_params['oauth_signature'] = base64_encode(hash_hmac(
'sha1',
implode('&', array_map('rawurlencode', [
'POST',
'https://api.twitter.com/1.1/statuses/update.json',
http_build_query($base, '', '&', PHP_QUERY_RFC3986)
])),
implode('&', array_map('rawurlencode', $key)),
true
));
foreach ($oauth_params as $name => $value) {
$items[] = sprintf('%s="%s"', urlencode($name), urlencode($value));
}
$header = 'Authorization: OAuth ' . implode(', ', $items);
uksort($base, 'strnatcmp');
$oauth_params['oauth_signature'] = base64_encode(hash_hmac(
'sha1',
implode('&', array_map('rawurlencode', array(
'POST',
'https://api.twitter.com/1.1/statuses/update.json',
str_replace(
array('+', '%7E'),
array('%20', '~'),
http_build_query($base, '', '&')
)
))),
implode('&', array_map('rawurlencode', $key)),
true
));
foreach ($oauth_params as $name => $value) {
$items[] = sprintf('%s="%s"', urlencode($name), urlencode($value));
}
$header = 'Authorization: OAuth ' . implode(', ', $items);
リクエストの準備
fsockopen 関数などを使ってリクエストヘッダーをガリガリ書くよりも cURL関数群 を利用したほうがはるかに容易なので、後者の方法で実装することにします。
GETリクエストの場合
生成済みの Authorization
ヘッダー $header を利用して GET users/show にリクエストする場合の具体例を示します。
$url = 'https://api.twitter.com/1.1/users/show.json';
$params = ['screen_name' => 'mpyw'];
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url . '?' . http_build_query($params, '', '&'),
CURLOPT_HTTPHEADER => [$header],
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => 'gzip',
]);
POSTリクエストの場合(通常)
生成済みの Authorization
ヘッダー $header を利用して POST statuses/update にリクエストする場合の具体例を示します。
$url = 'https://api.twitter.com/1.1/statuses/update.json';
$params = ['status' => 'test'];
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($params, '', '&'),
CURLOPT_HTTPHEADER => [$header],
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => 'gzip',
]);
POSTリクエストの場合(マルチパート)
生成済みの Authorization
ヘッダー $header を利用して POST statuses/update_with_media にリクエストする場合の具体例を示します。 http_build_query 関数を通さず、直接ポストフィールドに連想配列を渡すとマルチパートリクエストになります。
$url = 'https://api.twitter.com/1.1/statuses/update_with_media.json';
$params = [
'status' => 'test',
'media[]' => new CURLFile('/home/mpyw/test.png'),
];
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $params,
CURLOPT_SAFE_UPLOAD => true,
CURLOPT_HTTPHEADER => [$header],
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => 'gzip',
]);
PHP5.4以前では CURLFile クラスや CURLOPT_SAFE_UPLOAD
フラグが使えないため、少々面倒な方法を採ることになります。詳しくは以下を参照してください。
リクエストの実行・レスポンスのパース・例外処理
例外処理の前提
何らかのエラーが発生していた場合、以下のような形式で例外をスローすることにします。
throw new RuntimeException('エラーメッセージ', 'HTTPステータスコード');
奇形フォーマットへの対応
Twitterには歴史的な理由でさまざまなフォーマットが存在するので、その全てに対応しなければなりません。その際の Content-Type
もアテにならないので、実際にそれぞれの形式でデコードしてみる必要があります。
リクエスト成功時のレスポンス
{
"id" : 123456,
"id_str" : "123456"
}
[
{
"id" : 123456,
"id_str" : "123456"
},
{
"id" : 123456,
"id_str" : "123456"
}
]
oauth_token=****&oauth_token_secret=****
リクエスト失敗時のレスポンス
{
"errors" : [
{
"code" : 123,
"message" : "ERROR"
}
]
}
{
"errors" : "sharing is not permissible for this status (Share validations failed)"
}
{
"request" : "\/1.1\/statuses\/user_timeline.json?screen_name=******",
"error" : "Not authorized."
}
Failed to validate oauth signature and token
<?xml version="1.0" encoding="UTF-8"?>
<hash>
<error>Invalid / expired Token</error>
<request>/oauth/access_token</request>
</hash>
Exceeded connection limit for user
<html>\n<head>\n<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>\n<title>Error 401 Unauthorized</title>
</head>
<body>
<h2>HTTP ERROR: 401</h2>
<p>Problem accessing '/1.1/user.json'. Reason:
<pre> Unauthorized</pre>
</body>
</html>
(空文字列)
XMLやHTMLを突然返してきたり、 \n
という文字がそのまま返されてたりしてさんざんですね。
関数の実装
リクエストの準備を行ったcURLリソース $ch を渡し、リクエストを実行してレスポンスをデコードしてオブジェクトまたは配列で返す関数を実装してみます。なお、実際にストリーミングAPIを利用するときにこの関数は使わないので、これらに関する例外処理は行いません。
/**
* 通常のcURLリクエストを実行します。
*
* @param resource $ch cURLリソース
* @return stdClass|array デコード済みのメッセージ
* @throw RuntimeException
*/
function curl_exec_decode($ch)
{
// リクエストを実行
$response = curl_exec($ch);
// cURLのメタデータを取得
$info = curl_getinfo($ch);
// cURLの実行自体にエラーが発生しているか
if (curl_errno($ch)) {
throw new RuntimeException(curl_error($ch), $info['http_code']);
}
// レスポンスがJSON形式またはXML形式であるか
if (
null !== $obj = json_decode($response) or
false !== $obj = json_decode(json_encode(@simplexml_load_string($response)))
) {
// レスポンスが文字列形式のプロパティerrorを持っているか
if (isset($obj->error)) {
throw new RuntimeException($obj->error, $info['http_code']);
}
// レスポンスがプロパティerrorsを持っているか
if (isset($obj->errors)) {
if (is_string($obj->errors)) {
// 文字列形式の場合
throw new RuntimeException($obj->errors, $info['http_code']);
} else {
// 配列形式の場合
throw new RuntimeException($obj->errors[0]->message, $info['http_code']);
}
}
return $obj;
}
// クエリストリングとしてパース
parse_str($response, $obj);
$obj = (object)$obj;
// レスポンスが妥当な形式のクエリストリングであるか
if (isset($obj->oauth_token, $obj->oauth_token_secret)) {
return $obj;
}
// レスポンスはテキスト形式のエラーであるか
if (strip_tags($response) === $response) {
throw new RuntimeException(trim($response), $info['http_code']);
}
// 形式不明のレスポンスであった
throw new RuntimeException('Malformed response detected.', $info['http_code']);
}
ストリーミングAPIへの対応
ストリーミングAPIは無限にレスポンスを返し続けるので、 curl_exec の実行時間が無限大になります。通常の方法では対応できません。
生成済みの Authorization
ヘッダー $header を利用して GET statuses/sample にリクエストする場合の具体例を示します。
$url = 'https://stream.twitter.com/1.1/statuses/sample.json';
// コールバック関数を作成する(内部でstatic変数を利用しているため、必ずクロージャでなければならない)
$callback = function ($ch, $str) {
// メッセージが蓄積されるバッファ
static $buffer = '';
// 新規メッセージを追記する
$buffer .= $str;
// デコード可能な状態であるか(末尾が改行コードであるか)
if ($buffer[strlen($buffer) - 1] === "\n") {
if (null !== $obj = json_decode($buffer)) {
$info = curl_getinfo($ch);
if (preg_match("@Reason:\n<pre>([^<]++)</pre>@", $buffer, $matches)) {
// レスポンスがHTML形式のエラーである場合
throw new RuntimeException(trim($matches[1]), $info['http_code']);
}
if (strip_tags($buffer) === $buffer) {
// レスポンスがテキスト形式のエラーである場合
throw new RuntimeException(trim($buffer), $info['http_code']);
}
// 形式不明のレスポンスであった
throw new RuntimeException('Malformed response detected.', $info['http_code']);
}
if (isset($obj->disconnect)) {
// レスポンスがJSON形式だが切断されたことを意味する場合
$info = curl_getinfo($ch);
throw new RuntimeException($obj->disconnect->reason, $info['http_code']);
};
// メッセージを使って目的の処理を行う(ここではダンプしている)
var_dump($buffer);
// バッファを空にする
$buffer = '';
}
// 追記された長さを返す
return strlen($str);
};
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_HTTPHEADER => [$header],
CURLOPT_ENCODING => 'gzip',
CURLOPT_TIMEOUT => 0, // 無限に実行し続ける
CURLOPT_WRITEFUNCTION => $callback, // 各メッセージを処理する関数として設定する
]);
curl_exec($ch); // エラーが発生するまでここで停滞する
// ここに到達したことはcURL自体のエラーを意味する
$info = curl_getinfo($ch);
throw new RuntimeException(curl_error($ch), $info['http_code']);
活用例
ライブラリの作成例
例外処理をガチガチに固めてあるので、これを使えば原因不明のエラーに悩まされることが無くなります。