📑

libsslとlibcurlを使ってツイートする

2022/12/01に公開

タイトルのままです。
C++でlibcurlとlibsslを使ってTwitter APIを叩いてみます。
今回はOAuth1.0も自前で実装していきます。

なおこの記事は2021年に執筆したものを焼き直しています。

OAuth認証

Twitter APIを使うにあたって、そのリクエストにはOAuth1.0に準拠した署名を含める必要があります。

関連パラメータ

キー
oauth_consumer_key Twitterの開発者コンソールで取得したキー
oauth_nonce ランダム文字列 毎回ランダムな値を生成して署名に埋め込みます
oauth_signature_method "HMAC-SHA1"(固定) 署名のアルゴリズム。
oauth_timestamp 署名をした時刻。あまりにも古いものはサーバで弾くのかな?
oauth_token アクセストークン
oauth_version "1.0" 固定です。 最近は2とか出てますね。あっちはベアラ

これらは必ず必要になってくるパラメータです。
これ以外に、エンドポイントごとに存在するクエリストリングも利用します。

メッセージの組み立て

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の生成

#include <ctime>
std::string timestamp = std::to_string(time(nullptr));

ここまでで、必要なパラメータはすべて揃ったので、署名を生成していきます。
また今回は投稿ということで、 statusというぱらめーたがあるのでそれも利用します。

収集したパラメータを key=valueのような形式で、辞書順に並べていきます。

それを、 encode(HTTP_METHOD)&encode(URL)&encode(parameter) のような形式で連結します.ここでencode()はURLエンコードされた値を意味します。
またHTTP_METHODGETPOSTPUTもしくはDELETEとなりますが,今回使用するのは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);

そしたら署名していきます。

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, &amp;length);
 
auto sha1 = std::string(reinterpret_cast<char*>(result), length);

ここまでで得られるのは、ハッシュのダイジェスト値です。
これを更に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, &amp;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);

やっと署名が手に入りました。
これを oauth_signatureとしてヘッダに組み込みます。

oauthParam["oauth_signature"] = k64Sha1;
std::string oauthHeader = "authorization: OAuth ";
for(const auto&amp; [key, value] : oauthParam){
  paramString += (key + "=" + urlEncode(value) + ",");
}
 
// 末尾の`,`を取り除く
paramString.pop_back();

投稿する

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*)&amp;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);
}

------------------

size_t curlCallback(char* _ptr, size_t _size, size_t _nmemb,
                                    std::string* _stream) {
  int realsize = _size * _nmemb;
  _stream->append(_ptr, realsize);
  return realsize;
}

さいごに

ツイートするだけなら結構簡単.
実際に動いたコードはあるけどそれをあまり見ないで記憶で記事を書いてるから動かなくても許してください.(カス)
動いているソースはGitHubにあります

Discussion