はしのした


2023-08-21 ActivityPub 対応(3)

このブログを ActivityPub 対応にするまでに必要だったことをまとめた記事の後編です。 なんだか思ってた以上に長い記事になっちゃったぞ?

  相手方の Inbox に投稿を配送

Create アクティビティ

Inbox に投稿を配送する場合は、先に作成した Note オブジェクトを object に格納した Create アクティビティを使います。 アクティビティもオブジェクトの一種である以上、ID(パーマリンク)が必要になりますので、Note のときと同様にラッパスクリプトを書く形で対応しました。 また、Follow アクティビティへの対応 で必要であった Accept アクティビティも、同じスクリプトで出力できるようにしています(約55行)。

これを相手方に送る際は、今度はこちらが HTTP ヘッダの一部に署名して、その内容を Signature ヘッダに記載する必要があります。 また、Misskey との連携にあたっては、Authorization ヘッダも必要です。内容は "Signature" + 半角スペース + Signature ヘッダの値とすれば OK です。

実際に署名付きのヘッダを作成するときに使っている関数(約30行)を、以下に示します。 PRIVATE_KEY には PEM 形式の署名鍵(秘密鍵)が入っています。 今更ですが、署名鍵(秘密鍵)はよそに漏れないように十分注意しましょう。

 1def make_signed_header(json, uri_str)
 2  # check URI
 3  uri = URI.parse(uri_str) rescue nil
 4  return nil if ! uri
 5  
 6  # generate string to sign
 7  host = uri.host
 8  date = Time.now.utc.httpdate
 9  targ = uri.path
10  targ += "?" + uri.query if uri.query && uri.query != ""
11  digest = "SHA-256=" + Base64.strict_encode64(OpenSSL::Digest::SHA256.digest(json))
12  str_to_sign = "(request-target): post #{targ}\nhost: #{host}\ndate: #{date}\ndigest: #{digest}"
13
14  # generate signature
15  keypair = OpenSSL::PKey::RSA.new(PRIVATE_KEY)
16  signature = Base64.strict_encode64(keypair.sign("sha256", str_to_sign))
17  sig_header = "keyId=\"#{ID}#main-key\"," +
18    "algorithm=\"rsa-sha256\"," +
19    "headers=\"(request-target) host date digest\"," +
20    "signature=\"#{signature}\""
21
22  # output as an HTTP POST request
23  req = Net::HTTP::Post.new(uri.request_uri)
24  req["Host"] = host
25  req["Date"] = date
26  req["Digest"] = digest
27  req["Signature"] = sig_header
28  req["Authorization"] = "Signature #{sig_header}"
29  req["Content-Type"] = "application/activity+json"
30  req["Accept-Encoding"] = "gzip"
31  req.body = json
32  req
33end

配送スクリプト

あとは記事が追加されたときに、対応する Create アクティビティを各フォロワーの Inbox に配送するだけです。 ただ、相手先のサーバが停止してしまっているときに、更新処理で長時間待たされるのは避けたいです。 配送は別スクリプトで行うと良いでしょう。 やるべきことは、配送キューに入っているアクティビティを、順番に配送先の Inbox に POST していくだけです(約75行)。

注意すべきは、CGI として呼ばれたスクリプトが終了しても、配送スクリプトが終了しないようにすること、つまり配送スクリプトを daemon にすることです。 Ruby には Process.daemon というメソッドがあるので、それを使えばいいかなと思っていたのですが、どうもうまく行きませんでした。 最終的には、こちらの記事に書かれていた方法を参考に、fork を二重に行う形で対応しています。

  届いた Like や返信をブログに表示

ここまで行けば、あとは記録したリアクションをブログに表示するだけです。 ここで改めて、最初に作った tDiary プラグインが出てきます。 各セクションの終わりに、リアクション一覧を取得して表示する処理を追加していきます(約60行)。 この辺りは、動作確認と並行して作っていきました。

実際にどう見えるかを示すために、試しにセルフで反応を送ってみます。

以上で、現時点での実装をひと通り説明しきりました。 現時点でのコードの行数は全部で1,020行です。 時間があったらコードを整理してどこかに公開しようと思いますが、少し時間がかかりそうです。もうしばらくお待ちください。

Fediverse 上での反応(全2件)

SoLa4/:parrot_sorena:

SoLa4: 返信はこんな感じで表示されます。 (2023-08-22 01:31)

  Mastodon/Misskey との連携確認

ひと通り個別のスクリプトの動作が確認できたら、あとは実際に連携してみます。 とはいえ、いきなり本番環境でやるのも怖いので、まずはローカル環境で試します。 たまたま Ubuntu がインストールされたミニ PC が転がっていたので、そちらに Docker で MastodonMisskey、Nginx を立ち上げました(リンク先は参考にしたページ)。これまでの例で出てきた amitie.plb.local がこのマシンです。 これと Apache が動いている開発用 PC(こちらが arle.plb.local)との間で、オブジェクトをやりとりしてみます。

話がややこしいのは、ActivityPub のやりとりには原則として HTTPS が必要というところです。 何らかの形で証明書をインストールし、通信相手が信頼できることを確認してもらわないといけません。 今回は mkcert でローカル認証局を作成し、それを各環境に入れていくことにします。

まず、Ubuntu 環境で mkcert のビルド済バイナリを入手します。 その後、以下の要領でローカル認証局や証明書を生成します(このページを参考にしました)。

1mkcert -install
2cd `mkcert -CAROOT`
3openssl pkcs12 -export -inkey rootCA-key.pem -in rootCA.pem -out rootCA.pfx
4cd
5mkcert arle.plb.local
6mkcert amitie.plb.local

ここで得られた rootCA.pem がサーバで、rootCA.pfx が Windows のクライアントで、それぞれ必要になります。 また、ホームディレクトリに各マシンの証明書が作成されます。 こちらは Nginx や Apache で使います。

次に、ローカル認証局を各マシンに登録していきます。

Windows の場合は、rootCA.pfx を取り出して、右クリック →「PFX をインストール」からインポートできます。 インポート先は「信頼されたルート証明機関」とします。

Ubuntu の場合は、rootCA.pem を拡張子 crt の適当な名前にリネームしてから、/usr/local/share/ca-certificates/ にコピーします。 その後、update-ca-certificates を実行すると、コピーしたファイルが自動的にローカル認証局として認識されます。いずれも要 sudo です。 なお、この手順は、Mastodon の docker を立てる際にも必要です。 Dockerfile の USER mastodon の切り替えの直前あたりに加えておきましょう。

Node.js は独自の証明書ストアを持っているようです。 こちらにローカル認証局を登録する場合は、rootCA.pem をコピーした先のパスを環境変数 NODE_EXTRA_CA_CERTS に書いておけば良いようです。 Misskey の docker を立てる際には、Dockerfile で NODE_ENV 環境変数 を指定しているあたりに一緒に書いておきましょう。

最後に、Mastodon や Misskey はデフォルトではローカル IP からの Inbox へのアクセスを弾いてしまうので、ローカル環境で動かす場合には許可が必要です。 Mastodon は .env.production の ALLOWED_PRIVATE_ADDRESSES という設定、 Misskey は .config/default.yml の allowedPrivateNetworks という設定で、 それぞれ許可する範囲(例えば 192.168.1.0/24)を指定します。 各環境の hosts でサーバ名が解決できるようにするのもお忘れなく。

Mastodon や Misskey がちゃんと動き出したら、デフォルトのアカウントを作成して、そこからフォロー、返信、いいね等、色々試してみましょう。 ひと通り問題ないことが確認できたら、本番環境に持って行って完了です。