Scala
Twitter
OAuth
Akka
Akka-HTTP

Akka HTTP で Twitter をやる

Akka とは Scala において並行/分散プログラミングを行うために設計されたライブラリ群であり、アクターモデルの実装をその基礎に置いている。早速アクターモデルなる用語が出てきたが、今回の話ではそれほど重要ではない。

やろうとしていることは、Akka に含まれるライブラリのひとつ、Akka HTTP を用いて Twitter にアクセス(ツイートの投稿、データの取得、etc.)することである。一度に全部を書くと(筆者が)大変なので、いくつかの項目に分けて記事を立てることにする。今回のゴールは、Akka HTTP から Twitter への投稿を行い、レスポンスを受け取ることである。レスポンスは基本的に JSON 形式で来るので、何かしらのパーサに投げれば値を取得することができるだろう。

本記事を粗方最後まで書いた上で、本記事の内容の多くを巷で広く使われている『URL エンコーディング』なる『用語』に関する記述に割いたことを、筆者としては悩ましく思う。結論として、筆者は『URL エンコーディング』なる語を「使ってはならない廃棄されるべき語」であると考えるに至った。

動機と背景

青い鳥が悪いので赤く染めてやろうと思った

こんなことをやろうと思った背景には、最近の Twitter があまりに酷く公式クライアントが使用に耐えなくなったことや、今後の API の変更によって既存の Twitter 用ライブラリが使えなくなる可能性が出てきたことなど、ユーザとしても趣味で bot を書く者としても Twitter の現状に多くの不満を抱えたことがある。高々5、6個とはいえ、クリティカルでない操作をするのにいちいちログインするのは面倒だ。かなりの頻度でパスワードのリセットを行わなければならない。しかし、API を叩くツールとして Twitter 社が公式に提供しているのは Twurl という大変しょっぱいものだ。いや、ドキュメントの新しい部分を読んだ限りでは、Twurl はもう解雇されかかっているようだ。青い鳥を半分こにして焼き鳥にしたところで不味そうという以上のことはない。

筆者が元々 bot のプログラムを Akka を用いて書いていたことから、ネットワークへのインターフェイスを Akka HTTP で書こうと思い立ったのは自然の成り行きだった。Akka のドキュメントにある複数の例に Tweet の文字列を見つけることができ、そのようなタスクを指向して開発されていたことが伺える。

しかし残念なことに、現行の Akka HTTP では Twitter にアクセスしてツイートを投稿するなどのことができないのだ。具体的には OAuth 1.0a の実装を欠いている。ドキュメントにないのはもちろん、ソースを見ても実装しようとした形跡がない。Twitter が未だに obsolete にされた OAuth 1.0a を使い続けていることはひとつの問題ではあるが、しかし現状「Akka HTTP では Twitter にアクセスできない」ことは紛れもない事実である。

Akka HTTP を用いて Twitter にアクセスするということをやってみようと思うに至った経緯はこのようなものである。不思議なことに(Twitter と Scala とは切っても切れない関係があるにもかかわらず)検索してもヒットしないので、このようなことを試みる者は相当に奇特なのかもしれない。

難航した部分

実際にやってみた感想は、それなりのマゾプログラマでもなければこんなことをやらないだろうということだった。

ひとつは、ドキュメントにない機能を実装する以上、Scaladoc やソースと向き合って何が可能で何が不可能であるか(どのような隠し機能があるか)を見極める必要があったということである。

もうひとつ、現実的に邪悪なのは、俗に『URL エンコーディング』と呼ばれているものが許しがたい程度に曖昧であることだった。後者はネットワークプログラミングを行うすべての場合に影響し得る重大な落とし穴である。その結論が最初に述べた「使ってはならない廃棄されるべき語」であるということだ。

目標

最終的なゴールは、Akka HTTP に実装されている機能に最小限の追加を行うことによって Twitter が API として提供している機能の大半を Scala 上で利用できるようにすることである。鈍重で飛べない鳥に用はないのだ。

今回はその最初のステップとして、Akka HTTP から Twitter への投稿を行い、レスポンスを受け取ることである。言い換えれば、OAuth 1.0a が規定する Authorization (Authentication ではないことに注意。OAuth の名前に見られるように、意図的に混乱させるような記述が多いことにも気をつける必要がある。)ヘッダを含めたリクエストを構築することである。

Akka HTTP について

何をするためのものか

Akka はいくつかの異なるライブラリを含んでいるが、本記事で扱うのは Akka HTTP なるものである。名前から明らかなように、Akka HTTP はネットワークプログラミングをための API を提供するライブラリであり、HTTP サーバおよびクライアントの実装である。

Akka HTTP の設計思想は明快だ。「鶏を割くに焉んぞ牛刀を用いん」、つまりは「道具は仕事に合ったものを使うべきである。Web フレームワークは Web サーバとして機能するプログラムを書く以外の用途に適したものではない」ということ、一言で言えば『適材適所』である。これは筆者の思想に合っている。ドキュメントによれば、Akka HTTP は “not-a-framework” であることを意図して設計されている。ネットワークプログラミングに関する話にもかかわらず、ここまでに『フレームワーク』という文字列を引用符の中で一度しか使わなかったのは意図してのことである。「Akka HTTP はライブラリであってフレームワークではない」

Akka HTTP と Akka Streams

Akka HTTP は、極めて大雑把に言えば、アクターモデルの実装である Akka Actors の上に構築された HTTP サーバ/クライアント API を提供するレイヤーである。しかし、単純に Akka Actors の API を使っているわけではなく、その間に Akka Streams という別のレイヤーを挟んでいる。

アクターモデルは大雑把に言えば、ネットワーク上のホスト間の通信とよく似た方法で、並行処理が抱えてきた問題(スレッドの競合、デッドロックなど)の解決を図るものである。しかし、アクターモデル単体では、実際のネットワークプログラミングを行うためにいくつもの障壁がある。例えば、通信速度やバッファの管理はなかなか困難な問題である。一般的に使われる TCP においては、送信順序通りに届かないパケットの整列や、途中で失われたパケットの再送要求を出すことが必要である。また、受信側が処理しきれない速度でデータを送ることは避けるべきである。逆に、受信側としては送信側に処理が追いつかないことを知らせることができると良いだろう。Akka Streams はそのような目標を実現するための API を提供するライブラリである。

Akka Streams は Akka Actors の上に構築されているものの、プログラミングのスタイルは見かけ上だけでなく考え方としても大きく異なっている。Akka Streams において Akka Actors の存在はかなりの部分で隠蔽されている。

一方で、今回の主役である Akka HTTP は、Akka Streams をデータモデルからネットワークの送受信まで広く用いている。従って、Akka HTTP である程度以上のプログラムを書くためには Akka Streams にかなり踏み込む必要がある。もっとも、今回の話では Akka Streams についても触れる程度のことしかしないので、あまり難しく考える必要はない。複数のことを一度にやるのは筆者に対して厳しい要求である。

開発元について

Akka は Apache 2.0 ライセンスのもとで公開されているオープンソースのライブラリ群であり、Lightbend なる会社が開発を行っている。ひとつ重要なこととして Akka は単なるサードパーティ製ライブラリではなく、Scala の公認ライブラリであることがある。Scala の著作権は、設計者の Martin Odersky が所属するスイス連邦工科大学ローザンヌ校 (EPFL: École Polytechnique Fédérale de Lausanne) が保持している。では、この Lightbend(以前は Typesafe という名前だった)なる会社が何物かというと、その Odersky が立ち上げた会社であり、2018年6月現在における Scala の実質的な開発元である。

最初のコード

まずは OAuth 1.0a の認証を実装する前のコードを書く。当然ながら、この時点ではユーザ認証が必要な Twitter リソースへのアクセスはまだできない(エラーメッセージを受け取ることはできる)。本節の目的は、Akka HTTP のプログラムがどのようなものであるかを示すことである。具体的にやることは にリクエストを投げ、レスポンスを受け取ることである。

なお、以下ではビルドから実行までを sbt 上で行うため、先に sbt をインストールする必要があることを断っておく。

依存の記述

適当にフォルダを作ってプロジェクトのルートとし、その中に設定ファイルの build.sbt を作る。内容はとりあえず

name := "akka-twitter-sandbox"

version := "0.0.1"

scalaVersion := "2.12.6"

lazy val akkaVersion = "2.5.12"
lazy val akkaHTTPVersion = "10.1.1"

libraryDependencies ++= Seq(
  "com.typesafe.akka" %% "akka-stream" % akkaVersion,
  "com.typesafe.akka" %% "akka-http" % akkaHTTPVersion,
  "commons-codec" % "commons-codec" % "1.11",
)

とする。libraryDependencies の上二つが Akka HTTP を使うためのものである。Akka Streams のバージョン(Akka 本体のバージョン)と Akka HTTP のバージョンが違うことに注意。互換性についてはドキュメントを参照すべきだが、基本的に最新バージョンを書いておけばいい。commons-codec は HMac の計算と Base64 エンコーディングに使う。

コード全体

まずは実際に実行できるコード全体を示す(Akka のドキュメントには、説明のためだけの実際には実行できないコード例が多いので注意)。コードはプロジェクトフォルダのルートに置いてもコンパイルしてくれるので、おもちゃのプロジェクトでは特に src/main/scala/ 以下に置く必要はない。

import scala.io.StdIn
import scala.concurrent.Future
import scala.concurrent.duration._
import scala.util.{Failure, Success}
import scala.annotation.tailrec

import akka.actor.ActorSystem
import akka.stream.ActorMaterializer
import akka.util.ByteString
import akka.http.scaladsl.Http
import akka.http.scaladsl.model._
import akka.http.scaladsl.model.HttpMethods._

object RequestExample {

  implicit val system = ActorSystem()
  implicit val materializer = ActorMaterializer()
  implicit val executionContext = system.dispatcher

  def makeRequest(url: String) = {
    val request = HttpRequest(
      method = GET,
      uri = url
    )

    println(s"request:\n$request\n")

    val responseFuture: Future[HttpResponse] = Http().singleRequest(request)

    println("[complete] request")

    val strictEntity: Future[HttpEntity.Strict] = responseFuture.flatMap { res =>
      println("[complete] response")
      println(s"response:\n${res}")
      res.entity.toStrict(10.seconds)
    }

    strictEntity.foreach { e =>
      println("[complete] receiving body")
      val body = e.data.utf8String
      println(s"body:\n${body}\n")
    }
  }

  def shutdown = {
    Http().shutdownAllConnectionPools().onComplete { _ =>
      materializer.shutdown
      system.terminate
    }
  }

}

object Main {

  final val url = "https://oauth.net"

  @tailrec
  def input(f: String => Unit): Unit = {
    print("Waiting...\n\n")
    val s = StdIn.readLine()
    if (s != null && !s.isEmpty) {
      f(s)
      input(f)
    }
  }

  def main(args: Array[String]): Unit = {
    try {
      RequestExample.makeRequest(url)
      input { _ => }
    } finally {
      RequestExample.shutdown
    }
  }

}

この最初のコードでは、Akka Streams に踏み込む部分を最小限に留めるようにした。単純にリクエストを投げてレスポンスを受け取るだけの用途には、十中八九このパターンで十分である。少なくとも今回の目的に関して言う限りには、ドキュメントに出てくるような様々な使い方をする必要は多分そうそうないだろう。よって以後も大体このパターンで書くことにする。

オブジェクト Main はこのアプリケーションのメインオブジェクトであり、その中で行うことは、リクエストを投げた後に "Waiting..." という文字列を表示するだけである。入力なしで Enter キーを叩くと、即座にアプリケーションの終了手続きに入る。

主要なコードは RequestExample の方にある。

Akka HTTP を使うための『暗黙の値』宣言

上のコードのどこに Akka Streams が関与しているかを見出すことはおそらく簡単ではないだろう。その答えは今回の話では重要ではないので、とりあえず

import akka.actor.ActorSystem
import akka.stream.ActorMaterializer

...

implicit val system = ActorSystem()
implicit val materializer = ActorMaterializer()
implicit val executionContext = system.dispatcher

の5つは akka.http が含まれていないものの、Akka HTTP を使うためには事前に宣言しておく必要がある。(全くどうでもいいことだが、Actor Materializer とは妙に SF チックな響きだ。訳すと『俳優具現化装置』といったところだろうか。ちなみに、俳優を終わらせるときは毒(PoisonPill)を送りつけるという物騒な手段を取る。こういう言葉のチョイスは嫌いじゃない。)

このように implicit val で宣言される『暗黙の値』は Scala の特徴的な文法であり、同時に諸刃の剣と言えるものだ。上の3行のどれも欠けてはならないが、それらが実際にどこで使われているのかはソースを見るだけでは分からない。というよりはむしろ、特定の文脈でいくつものメソッドが共通の値を要求する場合に、それをいちいち引数として明示的に与えることを避けてコードの冗長性を減らすことこそが、暗黙の値のひとつの意義である。その意味で見えないことは理に適っている。

リクエストの構築

本体が空の単純なリクエストの構築は HttpRequest に対して method にメソッドを akka.http.scaladsl.model.HttpMethods の定数として、uri に URL を文字列として渡せばよい。URL はクエリを含んでもよい。スキームが https であってもやり方は同じである。今回のリクエストはすべて本体が空なので、すべてこの形式である。

例えば GET http:// のリクエストは

val request = HttpRequest(method = HttpMethods.GET, uri = "http://...")

のように構築できる。import akka.http.scaladsl.model.HttpMethods._ を宣言しておけば method = GET と書ける。

レスポンスの受信

リクエストの結果としてレスポンスが Future の結果として返される。成功か失敗かは statusCode に格納されている。個々の StatusCode はオブジェクト StatusCodes の定数として定義されている。object StatusCodes それにはステータス番号、メッセージ、成功か失敗かあるいはリダイレクトか、失敗ならばその責任がクライアント側とサーバ側のどちらにあるか、といったものが含まれる。

レスポンスヘッダには、受け取ったデータの形式の情報などが含まれる。

さて、次に最も重要なレスポンス本体の取得だが、それはやや積もる話なので次の大節を立てて説明する。

レスポンス本体の受信

ストリームを消費する義務

Akka HTTP が Akka Streams を利用していることは前に述べたが、Akka HTTP においてデータの送受信はストリームとして行われる。一度レスポンスを受け取ったならば、その本体のデータを受信するストリームを『消費する義務』が発生する。受信側はいかなる場合でもストリームの消費を怠ってはならない。レスポンスの本体が不要な場合は、ストリームを破棄するという形で消費しなければならない。さもなくば、Akka Streams はストリームの処理が滞っていると判断し、送信元にストップをかけるかもしれない。

ストリームというと Java の InputStream / OutputStream や C++ の iostream のようなものを想像されるかもしれないが、Akka Streams におけるストリームはそのようなブロッキングを要する I/O とは全く異なった方法で処理される。

Akka が提唱する通り、ストリームは非同期、ノンブロッキングで処理される。プログラマは原則としてストリームの『処理の流れ』を書くだけで、実際のストリームがいつ、どのスレッドで、何物によって処理されるのかということには関知しないし、結果は通常、その処理を書いたスレッドには返されない。Scala においてそれらは Future の仕組みによって実現される。

プログラマが書いた『処理の流れ』を実行し、ストリームを実際に消費するのは、アクターシステム上に生み落とされたアクターである。しかし、Akka Streams においてアクターの存在はかなりの部分で隠蔽され、プログラム上には見える形で存在していないことが多い。上のプログラムもそうなっていて、RequestExapmle オブジェクトの初期化時の宣言を除いて、アクターのアの字も見えない。

その(大抵の場合)見えないアクターを生み出すものこそが、初期化時に宣言された ActorMaterializer なのだ。それは通常、ストリームを消費する命令を出すメソッドに暗黙の引数として渡される。だから我々には materializer がどのメソッドに渡されているのか見えないのだ。

ストリームを消費する方法

上の例においてストリームを消費しているのは次の部分である:

val strictEntity: Future[HttpEntity.Strict] = responseFuture.flatMap { res =>
  println("[complete] response")
  res.entity.toStrict(30.seconds)
}

これを前半と後半に分けて説明する。

前半は、レスポンスを受け取ると同時に entity(レスポンスの本体)の toStrict なるメソッドを実行している。このメソッドは、「送られてくるデータを最後まで受け取って、以後は普通にメモリ上にあるバイト列として扱えるようにする」という処理を行わせる命令である。レスポンスが有限の(あまり大きくない)データだと判っている場合、余計なことを考えずに toStrict してしまうのが最も簡単だと思われる。

toStrict の引数の 30.seconds は受信のタイムアウトであり、30秒以内にデータが最後まで届かなければエラーとすることを指示している。タイムアウトになる原因としては、例えば、サーバが応答を停止した場合、通信速度が遅すぎる場合、受信しようとしているデータが大きすぎる場合など、様々なことが考えられるが、とにもかくにも30秒を過ぎたら一律でぶった切るという実に単純明快な戦略である。

ここで、toStrict はストリームを消費する命令を出すメソッドである。先に言ったことに従えば、ActorMaterializer はこの toStrict が暗黙の引数として要求するのである。実際の定義は次の通り:

def toStrict(timeout: FiniteDuration)(implicit fm: Materializer): Future[Strict]

よって、先に ActorMaterializer を宣言しておかないと、コンパイラは「toStrict の暗黙の引数が見つからない」と怒ってくる。

本体のデータの取得

toStrict の結果としてクラス HttpEntity.Strict のオブジェクトが(Future の結果として)返される。メモリ上に収められたデータはアクセサ data によって取得できる。データの型は akka.util.ByteString であり、名前から判る通り単なるバイト列である。テキストデータの場合は、utf8String で文字列に変換できる(もちろんデータは文字列が UTF-8 でエンコードされたものでなければならない)。上のコードでは、次の部分でレスポンス本体を表示している:

strictEntity.foreach { e =>
  println("[complete] receiving body")
  val body = e.data.utf8String
  println(s"body:\n${body}\n")
}

この例では結果を表示するだけでメソッドの結果と返すことはしていない。それが必要ならば foreachmap に変えてやればよい(もっとも、上の例では printf で締めているので、結果はその後に書く必要がある)。リクエストを送信してレスポンスを受信、処理する過程のどこかしらでエラーが起これば、処理は中断されてエラーメッセージが表示される。それでメインスレッドが中断されることはない。処理を実行しているアクターが落ちるだけだ。

アプリケーションの終了

Enter キーを押すとアプリケーションを終了すると書いたが、アクターシステムが生きていると、そのまま固まって何もできなくなり、Ctrl+C などによって強制終了するしかない。

アクターシステムは明示的に終了させなければならない。そのための処理が shutdown に書かれている。

def shutdown = {
  Http().shutdownAllConnectionPools().onComplete { _ => // コネクションの切断
    materializer.shutdown // materializer の終了
    system.terminate // アクターシステムの終了
  }
}

実を言うとこれは Akka を強制終了させる処理である、キープされている接続があっても強制的に切断する。そのために時々文句を言ってくるが、アプリケーションの終了自体に影響はない。安全に終了したい場合はもっとマシな方法を使うべきだが、ドキュメントを見てもインターネットを検索しても、安全に終了させることへの関心は特にはないようだ。なお、ドキュメントのコードでは終了処理が省略されていることが多々あるので注意が必要である。

以上で最初のコードの説明を終える。

URL のエスケープ処理に関する問題と解決

筆者の時間を大量に食った問題は、URL のエスケープ処理(俗に『URL エンコーディング』と呼ばれるものだが、後に述べる問題があるため、筆者は使ってはならない廃棄されるべき語であると考える。以後、筆者がこの語を使うことは、限られた言及を除いては一切ない)の実情であった。

それは単に不案内な筆者が躓いたというだけでは済まされない問題であり、ここで取り上げることにした。さらに言えば、OAuth 1.0a の Authorization ヘッダを作成するためには避けては通れない部分であり、本記事の小さなゴールを達成するためにはどうしても言及しなければならなかった。とはいえ大きな回り道となるため、あまり踏み込みたくはなかったのが本音だ。

URL に含めることができない文字

前節の例では意図的に避けたので問題は生じなかったはずだが、Akka HTTP は文字列として与えられた URL に規定上使えない文字が含まれる場合、不正な URL であると見なして例外を投げてくる。自動的にエスケープ処理をしてくれるということはない。よって、リクエストの構築の前にそれを行う必要がある。

ここでいう URL に含められない文字とは、大雑把には(以下、0xHH で16進表記が HH に等しい1バイトのデータを表す)

  • URL に含めること自体が許されない文字:スペース 0x20 や改行コード LF = 0x0A、すべての非 ASCII 文字(例:例)
  • URL において特別な記号として扱われる文字:# = 0x23@ = 0x40 など

を指している。厳密には RFC3986 において規定されている。場所によって違うなどの点はあるものの、本記事および今後の続きについて言う限りは、クエリに使えない文字だけが関心事である。

OAuth 1.0a の署名や Twitter に投稿するときの URL を作成するにあたっては、上のような文字をエスケープすることは必須である。例えば、日本語が書けないことを差し置いても、スペースもリプライもハッシュタグも存在しない Twitter で可能なことを考えてみると、単語を吐き捨てるか、あるいは、円周率をひたすら綴ることくらいしか思い当たらない(日本語の表記ではスペースで区切ることをしないので、日本語が書けさえすれば、独り言をつぶやき続ける分にはさほど困らないだろう)。

URL について

先に挙げた RFC3986 において定義されているものは、実際は URI (Uniform Resource Identifier) であり、URL (Uniform Resource Locator) は URL は URI のうち、意味的にリソースの位置を示すものとされている(URL それ自体についての明確な定義はない)。最初に URL を定義した文書は RFC1738 である。RFC3986 はそれを上書きするものであり、再定義された URL は URI の規定に準拠しなければならない。

RFC3986 では URL に非 ASCII 文字を含めることができないため、それを補完するものとして RFC3987 において IRI (Internationalized Resource Identifier) が定義された。IRI は『字面として』(≠符号化されたバイト列として)非 ASCII 文字列を含む URI と同様の文字列であり、その規定において特に重要な部分は URI との間の相互変換規則である。あくまで通用するものが URI であることは変わりない。

実情を考えると IRI はさほど通用する語ではなく、URI についてもあまり一般的に通用しないと思われるため、以下では両者をまとめて URL と呼ぶことにする。

非 ASCII 文字を含む URL を URI に変換するときは、IRI の規定に従って次のように行う:

  1. 文字列を UTF-8 のバイト列に符号化する
  2. URI の場合と同様のエスケープ処理を行う

いろいろ細かい規定はあるものの、クエリのみを処理する分にはこの2ステップで十分である。

Akka HTTP における実装

Akka HTTP における URL 回りの実装は akka.http.scaladsl.model.Uri にある(クラス名が URL でないことは、URI の規定に準拠することを強調するためだろうか?また、URI でもないのは java.net.URI との区別を明確にするためだろうか?)。HttpRequest のパラメータ uri に指定した文字列は、クラス Uri のインスタンスに暗黙的に変換される。その過程で URL の解析にトラブルがあると例外が投げられる。先に言った通り、URL の中にエスケープされるべき文字があると、不正な形式であるとして例外が飛んでくる。

幸いでもないことに、クエリを別の方法で結合してやれば何らかの方式でエスケープ処理をしてくれることが確認できた(おそらく x-www-form-urlencoded 方式。なぜなら、同じ方式でフォームデータを処理しているから)。これについてはドキュメントにそれらしい記述が見当たらず、ソースを読んでいる最中に発見したと言っていい。この記事の冒頭でドキュメントに書かれていることを肯定的に評価したが、さすがに不親切に過ぎると思うことが多々あるとも感じている。もっとも、調べていくうちに Akka HTTP を責めても仕方がないと思うようになった。

以後では使わないので敢えて言及するまでもないことだが、Akka HTTP のクエリのエスケープ処理は、OAuth 1.0a のパラメータエンコーディングや Twitter に投稿するための URL の作成には使えないことが判っている。そもそも Akka HTTP は URL のエスケープを処理する API を提供していない。それは Akka HTTP の内部でのみ使われていて、外からアクセスできないことになっている。ある意味ではそうした方が好ましいと言えるのは、URL と呼ばれているものが、現実には想像以上に面倒な代物であったことによる。

というわけで、Akka HTTP のリクエストの構築においては、事前に URL にエスケープ処理を施してから渡さなければならない。

とまぁ、ここまでの話で終わりにできるなら、例えば Apache Commons Codec の URLCodec クラスを使えばいいだろうという話になるのだが、残念なことにそれでは上手く行かなかったということを以下で述べる。

エスケープの曖昧性

URL のエスケープ処理に関する悩ましい問題は、その方式について統一された規定が存在しないことと、さらに悪いことに、互換性のない方式が並行して用いられていることである。具体例を挙げると、"ab c/d, あ" という文字列(スペースは 0x20)に対して、Apache Commons Codec の URLCodec.encode をかけると

ab+c%2Fd%2C+%E3%81%82

となる。これは HTTP 4.01 が定めるフォームデータ(HTML の < form &rt; 要素の submit によって送られるデータ)の形式 application/x-www-form-urlencoded に準拠している。また、RFC3986 が定める URI の規定に抵触しないという意味でも適切にエスケープが為されていると言える。もちろん、URLCodec.decode をかければ正しく "ab c/d, あ" に戻る。

上の例では、+RFC3986 における文字の分類で sub-delims に含まれ、クエリの中で文字として使うことができる。sub-delims は 区切り文字に使うことが許されている文字ということだが、その意味については実装依存である。よって、スペースのエスケープとして用いてもよく、あるいは + という文字そのものとして用いてもよく、あるいは他の解釈をしてもよい。

この曖昧性こそが、本節の問題が起こってしまう原因なのだ。

現実問題として、スペースを + に置換することは好ましくない結果を生む場合が少なくない。それは、ウェブサービスが + をスペースに対してエスケープを行ったものとして認識しない場合である。他ならぬ Twitter こそがその一例である。例えば、上記のエスケープ後の文字列 "ab+c%2Fd%2C+%E3%81%82" をツイートのテキストに指定して送ると、"ab+c/d,+あ" という内容のツイートが生成されてしまうことを確認している。

これを避けるためには、スペース 0x20 についてもパーセントエンコーディングを行って %20 とする必要がある。その結果として得られる文字列は

ab%20c%2Fd%2C%20%E3%81%82

である。この文字列は RFC3986 の中で unreserved と pct-encoded に含まれる文字(pct-encoded はパーセントエンコードされた文字であり、実際は %HH の長さ3の ASCII 文字列からなる)以外を含んでいないため、必ず "ab c/d, あ" として復元される。

最終的な解決

結論から言えば、RFC3986 の文字の分類に従って

  • unreserved に含まれる文字についてはパーセントエンコーディングを一切行わない
  • unreserved に含まれない文字については必ずパーセントエンコーディングを行う

という方式を採用することになった。この方式によれば、エスケープ処理後の文字列は、RFC3986 の規定に従う限りは実装に依存せず、常に元の文字列に復元されることが保証される。よって、Apache Commons Codec の URLCodec.decode によって正しく元の文字列を得ることができる。

この方式は OAuth 1.0aParameter Encoding に準拠したものである。すなわち、OAuth 1.0a の実装のためには必ずそれを行う場面がある。それならいっそのこと全ての場合で使ってしまおうというわけである。フォームデータの application/x-www-urlencoded については今のところ考えないことにする。

以後、文字列全体にわたってこの方式でエスケープ処理を施すことを『厳密エンコーディング』と呼ぶことにしよう。

というわけで、問題は解決した。めでたしめでたし。

以上の事柄について次の記事は大いに参考になるだろう:

URLエンコードについておさらいしてみた @sisisin

なお、比較的最初に言った通り、筆者は『URL エンコード』という語は使ってはならず廃棄されるべきであると考えているので、本記事の以後の部分、唯一「まとめ」を除いて、その語を使うことは決してない。

ここまで書くのに疲れたのでこの辺りで打ち切って続きは次回としてしまいたいところではあるが、まだ最初の一歩にすら届いていない以上は話を終えるわけにはいかない。次節でようやく本題に入る。

Twitter にツイートを投稿する

大きく回り道をしたが、ようやく本筋に戻れた。最後の目標は Twitter にツイートを投稿するにあたって、Akka HTTP のデータモデルに OAuth 1.0a の実装を加えてやることである。

Authorization ヘッダの作成

基本的には上のコード例に OAuth 1.0a の実装を加えて、Authorization ヘッダをリクエストにくっつけてやるだけである。やり方は 2018 年にもなって OAuth 1.0a の仕様を読めばわかるので、読者の演習問題とする(多分今更そんなことをする必要に迫られる場面はないと思うが)。注意することといえば、あらゆる場面で『厳密エンコーディング』を使うことだ。決して他の方式を使ってはならない。結果として次のようなものが得られればよい:

oauth_consumer_key="0685bd9184jfhq22",
oauth_token="ad180jjd733klru7",
oauth_signature_method="HMAC-SHA1",
oauth_signature="wOJIO9A2W5mFwDgiDvZbTSMK%2FPY%3D",
oauth_timestamp="137131200",
oauth_nonce="4572616e48616d6d65724c61686176",
oauth_version="1.0"

これらのうち oauth_signature を作るところで HMAC を計算する必要があるが、それには Apache Commons Codec の HmacUtils クラスを利用するのが簡便だろう。Base64 エンコーディングには 同 Base64 クラスを用いることができる。ただひとつ、URLCodec クラスを使ってはならないことだけは注意する。

さて、OAuth 1.0a のすべての要素が揃ったところで、それを Akka HTTP に Authorization ヘッダ用のデータとして渡す必要がある。ヘッダの抽象クラスは HttpHeader であり、具体的なクラス群は akka.http.scaladsl.headers 以下に置かれている。

その中に Authorization なるクラスを見つけることができる。名前からしてこれが求めているものであろうと検討がつく。次はその使い方だ。それを知るためには、クラスそのものよりもコンパニオン・オブジェクトを見る方がよい場合が多い。と思って見てみたら、残念ながら求めるものはなさそうだ。もう一度クラスの方に戻ってコンストラクタを見ると、

new Authorization(credentials: HttpCredentials)

と書かれている。HttpCredentials なるたったひとつの引数を取るということから、実際に作るべきは HttpCredentials であることが判る。それをコンストラクタの引数に渡せばよいというわけだ。

それではと、HttpCredentials を見ると、抽象クラスであってそれ自体をそのまま作るわけではないことが判る。そういう場合はもちろんコンパニオン・オブジェクトを見る、と思ったら、ない!ということで、HttpCredentials を作る手段は提供されていないように思われる。

次に見るべきは、それを実装しているサブクラスである。抽象クラスを直接継承して実装するというのは基本的に開発者がやることであり、ドキュメントに何の説明もない以上は避けられるべきだろう。正直、もう少し説明が欲しい。

当然ながら OAuth1 の文字は見えないが、GenericHttpCredentials なるクラスの存在が確認できる。その実体はというと case class であって、単に HttpCredentials の抽象メソッドが返すべきパラメータを持つだけのものだということが判る。

final case class GenericHttpCredentials(scheme: String, token: String, params: Map[String, String] = Map.empty) extends HttpCredentials with Product with Serializable

おそらくこれが目当てものだろう。しかし、ソースを確認するまで明言はできない。というわけで HttpCredentials.scala を読むと、どうやらこちらの読みは当たっていたようだ。めでたしめでたし。

なお、紛らわしい名前だが、BasicHttpCredentials はユーザ名とパスワードによる Basic 認証のためのものであり、ここでは目当てのものではない。ただ、Basic 認証が実装されていることは確認できる。他に HttpCredentials のサブクラスとして OAuth2BearerToken が確認できる。

見つかった GenericHttpCredentialsOAuth 1.0a の Authorization ヘッダの情報を与えてインスタンスを作成する。scheme は "OAuth" を指定する。token には oauth_ で始まるパラメータ群を ,(コンマ:0x2C)で結合してひとつの文字列にしたものを指定する。params には何も指定しない。なお、case class なので new は不要である。

これを new Authorization(ここでは new は必要)の引数に指定してインスタンスを作成してやれば、リクエストのヘッダに与えることのできるモノが得られる。

ツイートの投稿

すべての要素出揃ったところで、いよいよ本記事のゴールを決めよう。まず必要なものを確認する:

  • ツイートを投稿するアカウント(何をツイートしても構わないならばメインでやってもよいが、失敗してフォロワーに突っ込まれたとしても筆者は責任を負わない)
  • Consumer Key & Secret および Access Token & Secret. 持っていないならば Twitter Application Management で取得しておく
  • ツイートを投稿する API の情報(Twitter のウェブサイトはコロコロ変わるので、ここにリンクを貼ることができない):
  • ツイート本文:テストのためには、スペースと日本文字を含めるとよい。文字数制限に引っかからないように短い文字列を指定する(例:sealed abstract あざらし)

以上のデータを用いて、リクエストを HttpRequest として、ヘッダに上の情報から作成した Authorization を加えて構築する:

final val `statuses/update` = "https://api.twitter.com/statuses/update.json"

def urlWithQuery(url: String, query: Seq[(String, String)]): String =
  // URL と厳密エンコーディングされたパラメータを結合してリクエスト URL を作る
  // 出力例:"https://api.twitter.com/1.1/statuses/update.json?status=abc%20def"

val tw = ... // ツイート本文
val query = Seq("status" -> tw) // キーと値はこの時点では分けておいた方が無難
val auth = ... // Authorization ヘッダ
val request = HttpRequest(
  method = POST,
  uri = urlWithQuery(`statuses/update`, query),
  headers = List(auth)
)

後は最初のコードの request をそれに置き換えて実行するだけだ。

結果として長い JSON が返ってきたら成功、短い JSON が返ってきたら失敗である(乱暴!)。

リクエストが拒否されたならば、原因は概ね oauth_signature の作成手順に誤りがあることだろう。まずは Signature Base String がちゃんと形式に則っているか、パラメータが辞書式順序でソートされているか、所々で必要なエスケープ処理(厳密エンコーディング)に漏れがないか、あるいは必要な =& までエスケープしていないか、などを確認すべきだろう。宜しくないことに、OAuth 1.0a では同じ文字列に複数回エスケープ処理をかけることがあるため、どの段階で =& がエスケープされるかを厳密にチェックしなければならない。

リクエストが受理された場合でも、タイムラインを確認して正しいツイートが表示されることを確認する必要がある。エスケープ処理に誤りがあると、oauth_signature の計算は通っても、ツイートとしては誤ったものを投稿してしまうことがある。

ツイートを行うコードの例

この例を実際に走らせるには、自分で取得したキーおよびトークンを key(拡張子なし)という名前のファイルに書いてプロジェクトルートに置く必要がある。ファイルには次の4行を書く:

Consumer key
Consumer secret
Access token
Access token secret

後は sbt を開いて run すればよい。

まとめと今後

こうも長い話になってしまったのは、URL というものが抱えるエスケープ処理の問題を曖昧性なく書くことが、OAuth 1.0a の実装において、また、Twitter への投稿において、必要不可欠だったからだ。そこで定義した『厳密エンコーディング』とは、RFC3986 の分類に従って

  • unreserved に含まれる文字についてはパーセントエンコーディングを一切行わない
  • unreserved に含まれない文字については必ずパーセントエンコーディングを行う

という方式である。なお、Content-Type: application/x-www-form-urlencoded についてはその限りではないかもしれないが、スペースを + にするか %20 にするかでトラブルを生じる可能性を憂慮するくらいなら、最初から『厳密エンコーディング』を用いた方がマシだと思う。

なお、OAuth 1.0a は既に obsolete であり、今後新たに使われるべきでないとされている。2018年6月現在の Twitter においては Application-only Authentication (App-only) と一部の新 API のみが OAuth 2.0 に対応している。

なお、Akka HTTP には OAuth 2.0 の Bearer を用いて Authorization ヘッダを作る機能が申し訳程度に実装されている。よって、App-only で利用する限りは、特に追加で実装すべきことはない。Basic Authorization については、ユーザ名、パスワードとも英数字およびアンダースコア _ = 0x5f でない場合は危ないかもしれない。

本記事を書くにあたって、2018年にもなってなぜこんなことに悩まされなければならないのか?ということが多かった。今後?そんなものはない。

謝辞

2018年にもなって Twitter がここまで使用に耐えなくなったと思わされることがなければ、Akka HTTP で Twitter をやろうなどとは本気で考えなかったかもしれません。Twitter を使いづらいものにしてくださった Twitter 社の青い鳥貴族の皆様へ感謝の杯を捧げます。焼き鳥はご自身で調達してください。