【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_version1.0

さて,oauth_consumer_keyoauth_tokenはTwitterのAPI利用申請時に取得することができます.oauth_signature_methodは今回はHMAC_SHA1というアルゴリズムで決まっていますし,oauth_version1.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_METHODGETもしくは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&amp; [key, value] : signingParam){
  paramString += (key + "=" + "value" + "&amp;");
}
// 末尾の`&amp;`を取り除く
paramString.pop_back();
signingBase = std::string("POST") + "&amp;" + urlEncode(url) + urlEncode(paramString);

署名

署名にはopensslを利用するのが恐らく一番楽だと思います.署名に使用する鍵はAPI利用申請時に取得したConsumer SecretAccess Token Secretを使用します.これらをURLエンコードして&で結合します.(おそらくURLエンコードの必要はありませんが,定義上そうなっています.)

std::string consumerSecret = "your consumer secret";
std::string accessTokenSecret = "your access token secret";
std::string signingKey = urlEncode(consumerSecret) + "&amp;" + 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, &amp;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, &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);

認証ヘッダの組み立て

認証ヘッダを組み立てるにあたって,まずはoauthParamにSHA1署名をoauth_signatureというキーで登録する.

oauthParam["oauth_signature"] = k64Sha1;

そうしたら,key=encode(value)の形で連結したものをカンマ区切りでつなげていき,authorization: OAuthという句のあとにくっつける

std::string oauthHeader = "authorization: OAuth ";
for(const auto&amp; [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*)&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);
}

ちなみに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&amp; _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 &amp; static_cast<int>(c));
    }
  }

  return out.str();
}

さいごに

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

参考

おすすめ

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です