【C++】libsslとlibcurlを使ってツイートする
タイトルの通りです.今回はC++でlibcurlとlibsslを使用してTwitter APIを叩いてツイートしてみたいと思います.TwitterAPIを扱う上でキモとなるOAuth認証に関する部分も自前で実装していきます。
OAuth認証
Twitterに投稿するにあたり,OAuth認証が必要になってきます.今回は古いバージョンのAPIを使用するため,OAuth1.0を実装します.ちなみにOAuth1.0に関してはここにめちゃくちゃ詳しく書いています.
必要なパラメータ
キー | 値 |
oauth_consumer_key | “your consumer key” |
oauth_nonce | ランダム文字列 |
oauth_signature_method | “HMAC-SHA1” |
oauth_timestamp | 署名作成時のUNIX時間 |
oauth_token | “your access token” |
oauth_version | 1.0 |
さて,oauth_consumer_key
とoauth_token
はTwitterのAPI利用申請時に取得することができます.oauth_signature_method
は今回はHMAC_SHA1
というアルゴリズムで決まっていますし,oauth_version
も1.0
で確定しています.
oauth_nonce
は実際には固定値でも問題はないのですが,安全のためにリクエスト毎に異なる文字列を生成したほうがよさそうです.oauth_timestamp
は署名作成時刻をUNIX時間で埋め込みます.
メッセージの組み立て
oauth_nonceの生成
乱数とテーブルを用いて適当な文字列を作成します.
#include <random> std::random_device engine; std::string nonceTable = "abcdefghijklmnopqrstuvwxyz0123456789"; std::uniform_int_distribution<std::size_t> dist(0, nonceTable.length() - 1); std::string nonce = ""; for (auto i = 0; i < 32; ++i) { nonce += nonceTable[dist(engine)]; }
oauth_timestampの生成
UNIX時間を取得するだけであれば特段難しいことはありません.
#include <ctime> std::string timestamp = std::to_string(time(nullptr));
さて,OAuth認証をするにあたり,まずは先ほど示したパラメータを辞書順にしてkey=value
の形でそれぞれ&
で連結します.これらに加えて,クエリパラメータも追加します.今回使用するツイート用のエンドポイントにはstatus
というパラメータもあるので追加します.
それを,encode(HTTP_METHOD)&encode(URL)&encode(parameter)
のような形式で連結します.ここでencode()
はURLエンコードされた値を意味します.またHTTP_METHOD
はGET
もしくはPOST
となりますが,今回使用するのはPOST
です.
#include <map> std::string status = "hello, world!!"; std::string consumerKey = "your consumer key"; std::string accessToken = "your access token"; // OAuthヘッダ生成用のパラメータ std::map<std::string, std::string> oauthParam{ {"oauth_consumer_key", consumerKey}, {"oauth_nonce", nonce}, {"oauth_signature_method", "HMAC-SHA1"}, {"oauth_timestamp", timestamp}, {"oauth_token", accessToken}, {"oauth_version", "1.0"} } // OAuthヘッダにstatusパラメータが不要のため署名作成用にコピーして // そっちにstatusパラメータを追加する std::map<std::string, std::string> signingParam = oauthParam; signingParam["status"] = status; std::string paramString = ""; for(const auto& [key, value] : signingParam){ paramString += (key + "=" + "value" + "&"); } // 末尾の`&`を取り除く paramString.pop_back(); signingBase = std::string("POST") + "&" + urlEncode(url) + urlEncode(paramString);
署名
署名にはopensslを利用するのが恐らく一番楽だと思います.署名に使用する鍵はAPI利用申請時に取得したConsumer Secret
とAccess Token Secret
を使用します.これらをURLエンコードして&
で結合します.(おそらくURLエンコードの必要はありませんが,定義上そうなっています.)
std::string consumerSecret = "your consumer secret"; std::string accessTokenSecret = "your access token secret"; std::string signingKey = urlEncode(consumerSecret) + "&" + urlEncode(accessTokenSecret);
そうしたら,opensslのライブラリを使用して署名を作成していきましょう.
extern "C" { #include <openssl/hmac.h> #include <openssl/sha.h> #include <openssl/buffer.h> } unsigned char result[255]; unsigned int length = 255; HMAC(EVP_sha1(), reinterpret_cast<const unsigned char*>(signingKey.c_str()), signingKey.length(), reinterpret_cast<const unsigned char*>(signingBase.c_str()), signingBase.length(), result, &length); auto sha1 = std::string(reinterpret_cast<char*>(result), length);
Base64エンコード
作成された署名はBase64エンコードでヘッダに追加する必要があります(より厳密にいうと,Base54エンコードしたものをさらにURLエンコードしますが).署名作成時にせっかくopensslを使用したので,Base64エンコードもopensslを使用して一気に行っていきたいと思います.
BIO* encoder = BIO_new(BIO_f_base64()); BIO* bmem = BIO_new(BIO_s_mem()); encoder = BIO_push(encoder, bmem); BIO_write(encoder, sha1.c_str(), sha1.length()); BIO_flush(encoder); BUF_MEM* bptr; BIO_get_mem_ptr(encoder, &bptr); char* k64 = (char*)std::malloc(bptr->length); std::memcpy(k64, bptr->data, bptr->length - 1); k64[bptr->length - 1] = 0; BIO_free_all(encoder); k64Sha1 = static_cast<std::string>(k64);
認証ヘッダの組み立て
認証ヘッダを組み立てるにあたって,まずはoauthParam
にSHA1署名をoauth_signature
というキーで登録する.
oauthParam["oauth_signature"] = k64Sha1;
そうしたら,key=encode(value
)の形で連結したものをカンマ区切りでつなげていき,authorization: OAuth
という句のあとにくっつける
std::string oauthHeader = "authorization: OAuth "; for(const auto& [key, value] : oauthParam){ paramString += (key + "=" + urlEncode(value) + ","); } // 末尾の`,`を取り除く paramString.pop_back();
投稿
参考にしたサイトにはoauthParam
もボディに併せて流し込むって書いてある(よね?)んだけど,実際はそれがなくても問題ないと思う.というか問題なかった.クエリパラメータだけ流し込めばいいです.ちなみに投稿にはlibcurlを使います.
#include <iostream> extern "C" { #include <curl/curl.h> } std::string body = "status=" + urlEncode(status); CURL* curl; CURLcode res; std::string rcv; curl = curl_easy_init(); url_ = url_; std::cout << "URL : " << url << std::endl; if (curl) { curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl, CURLOPT_POST, 1); curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str()); curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, body.length()); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curlCallback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, (std::string*)&rcv); curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); struct curl_slist* headers = NULL; // Authorizationをヘッダに追加 headers = curl_slist_append(headers, oauthHeader.c_str()); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); res = curl_easy_perform(curl); curl_easy_cleanup(curl); } if (res != CURLE_OK) { std::cout << "curl error : " << res << std::endl; exit(1); }
ちなみにlibcurlでPOSTする際にコールバックを登録すればレスポンスが見れます.(rcv
に格納してるやつ)
size_t postInterface::curlCallback(char* _ptr, size_t _size, size_t _nmemb, std::string* _stream) { int realsize = _size * _nmemb; _stream->append(_ptr, realsize); return realsize; }
その他
urlエンコードするためのサポート関数はこんな感じ.厳密にはパーセントエンコード?日本語にも対応しています.\n
とかの改行もうまく動きました.
#include <cctype> #include <iomanip> std::string urlEncode(const std::string& _str) { std::stringstream out; for (const auto c : _str) { if (std::isalpha(c) || std::isdigit(c) || (c == '.' || (c == '_') || (c == '-' || (c == '~')))) { out << c; } else { out << '%' << std::setw(2) << std::setfill('0') << std::hex << std::uppercase << (0xFF & static_cast<int>(c)); } } return out.str(); }
さいごに
ツイートするだけなら結構簡単.
実際に動いたコードはあるけどそれをあまり見ないで記憶で記事を書いてるから動かなくても許してください.(カス)
動いているソースはGitHubにあります
最近のコメント