分散SNSソフトのMastodon、7月にmaster追従勢のトゥートがリモートのタイムラインに表示されなくなることがありました。トゥートを受け取る側のサーバで、bundle: Rejected Create activity
といったエラーが記録される問題が、シリアライザのキャッシュのキーを改善することでなおりました。
え?どゆこと?
ということで、Mastodonサーバどうしがやりとりをするときの様子を眺めてみることにしました。現在のMastodonはActivityPubという規格にしたがって実装されています。まず規格の内容を確認してから、Mastodonの実装の内容と稼働の様子を眺めてみます。
これは、分散SNSアドベントカレンダーの12月16日分の記事です。15日は@svchost@pawoo.netさんによる「otajodon.comで学んだLTLの功罪」でした。17日は焼肉ハブられたマンさんによる「プログラミング知識ほぼ0の俺がマストドンインスタンス作ってみた感想。」です。
まとめ
サーバへのリクエストやレスポンス、また、サーバからのリクエストやレスポンスなどを記録するようにコードや設定を追加したMastodonサーバを2台稼働させて、リモートアカウントの検索やフォローをして、お互いのやりとりを観察してみました。
リモートアカウントを検索すると、まずはWebFingerでリモートアカウントについての基本的な情報を得て、その後、WebFingerで得られたURLに対してHTTP署名をつけたリクエストを送り、リモートアカウントについてのより詳細な情報を取得していました。いっぽう、リモートサーバはWebFingerの受信に呼応してローカルサーバのインスタンスアカウントについての情報を、HTTP署名をつけて取得しに来ました。結果的に、ローカルサーバにはリモートアカウントの公開鍵を含めた情報が、リモートサーバにはローカルサーバのインスタンスアカウントについての公開鍵を含めた情報が記録されました。
リモートサーバのアカウントをフォローすると、ローカルサーバのジョブキューからリモートアカウントのInboxへActivitPubのFollow ActivityをPOSTリクエストとして送りました。ローカルアカウントによるHTTP署名がリクエストヘッダに付いていました。
ローカルサーバにトゥートを投稿すると、ローカルサーバのジョブキューからリモートアカウントの共有InboxへActivitPubのCreate ActivityをPOSTリクエストとして送りました。ローカルアカウントによる署名がリクエストヘッダとリクエストボディに付いていました。
Mastodonの配達しているトゥートがリモートに届かなくなっていたのは、シリアライザのキャッシュによって署名が不正な状態になっていたからのようです (結局よくわかってないや)。
今回眺めたリクエストのやり取りを再確認すると、ローカルサーバのドメインは、User Agentリクエストヘッダ、HTTP署名、また、ActivityPubでやりとりされる内容や署名に含まれていました。署名には、データベースのマイグレーションの時に生成されるインスタンスアカウントの鍵対やアカウントの生成の時に生成される鍵対が使われます。リモートアカウントとやりとりがあった時点で、リモートサーバに鍵対とドメインの対応づけが記録されます。あらかじめサーバのドメインを決めておくことと、鍵対をなくさないことが大事そうです。
ActivityPubでの規定
ActivityPubは分散ソーシャルネットワークサービスのための規格で、クライアントとサーバの間の通信、また、サーバどうしの通信について、投稿内容のやりとりのためのAPIを規定しています。ActivityPubの概要については、分散SNSアドベントカレンダーの12月7日の記事[Fediverse][Protocol] ActivityPubを深堀してみる でlocalYouserさんが解説してくださってます。Objects章では、「なりすましを防ぐため、サーバは受け取ったcontentを検証する必要がある(should)」と規定されています。でも具体的な方法は規定してないんだよね。
Mastodonはといえば、ActivityPubのサーバどうしの通信の規格に準拠した実装として、WebFingerでアカウントを見つけ、HTTP signatures spec (この記事ではHTTP署名と書きます)に従ってinboxへの配送内容を認証します。この記事ではこの部分を更に掘りすすんでみます。
ログをたくさん記録するようにする
この記事では、2019年11月21日ごろのMastodonを改造してローカルサーバへのリクエストやリモートサーバへのリクエストを記録するようにしました。改造後のコードはzunda/mastodonのlog-requestsブランチにもあります。
Railsにデバッグログを記録してもらう
config/environments/production.rb
やconfig/initializers/sidekiq.rb
にあるように、RAILS_LOG_LEVEL
環境変数を設定することでRailsやSidekiqが記録するログのレベルを設定することができます。
$ heroku config:set RAILS_LOG_LEVEL=debug
ログレベルをdebug
にするとPostgresとのやりとりも教えてもらえます。セッションIDなど秘密にしなきゃいけない情報も記録されちゃうのでプロダクション環境で記録するのは避けたほうが良さそうです。
User Load (1.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."id" ASC LIMIT $2 [["id", 1], ["LIMIT", 1]]
SessionActivation Exists (1.3ms) SELECT 1 AS one FROM "session_activations" WHERE "session_activations"."user_id" = $1 AND "session_activations"."session_id" = $2 LIMIT $3 [["user_id", 1], ["session_id", "dc7ab5cdb2885d9845f10a7cda8a0ba8"], ["LIMIT", 1]]
SessionActivation Load (0.9ms) SELECT "session_activations".* FROM "session_activations" WHERE "session_activations"."session_id" = $1 LIMIT $2 [["session_id", "dc7ab5cdb2885d9845f10a7cda8a0ba8"], ["LIMIT", 1]]
ローカルサーバへのリクエストとレスポンスを記録してもらう
lib/request_logger.rb
にRackミドルウェアを書いて、config/application.rb
から参照するようにしました。
config/application.rb
への変更点:
diff --git a/config/application.rb b/config/application.rb
index e1f7ae707..91355737b 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -16,6 +16,7 @@ require_relative '../lib/mastodon/version'
require_relative '../lib/devise/two_factor_ldap_authenticatable'
require_relative '../lib/devise/two_factor_pam_authenticatable'
require_relative '../lib/chewy/strategy/custom_sidekiq'
+require_relative '../lib/request_logger'
Dotenv::Railtie.load
@@ -121,6 +122,8 @@ module Mastodon
config.middleware.use Rack::Attack
config.middleware.use Rack::Deflater
+ config.middleware.use RequestLogger
+
config.to_prepare do
Doorkeeper::AuthorizationsController.layout 'modal'
Doorkeeper::AuthorizedApplicationsController.layout 'admin'
lib/request_logger.rb
の内容
# Add below into config/application.rb:
#
# config.middleware.use 'RequestLogger'
#
# Copied from https://gist.github.com/jugyo/300e93d6624375fe4ed8674451df4fe0
# and modified
#
# Please DO NOT USE this for production apps.
# This will leak clients' credentials to app logs.
#
class RequestLogger
def initialize app
@app = app
end
def call(env)
request = ActionDispatch::Request.new env
begin
response = @app.call(env)
req_body = request.body.read
log(request, req_body, response)
rescue Exception => exception
log(request, req_body, response, exception)
raise exception
end
response
end
def log(request, req_body, response, exception = nil)
status, res_headers, res_body = response
h = {
request: {
method: request.method,
fullpath: request.fullpath,
headers: Hash.new{|h,k| h[k] = Array.new },
},
response: {
status: status,
headers: res_headers,
},
}
request.env.each do |k, v|
if k.start_with?('HTTP_')
h[:request][:headers][k] << v
end
end
if x = parse(req_body)
h[:request][:body] = x
end
if x = parse(res_body.body)
h[:response][:body] = x
end
if exception
h[:exception] = {
type: exception.class.name,
message: exception.message,
}
end
Rails.logger.info(h.to_json)
rescue Exception => exception
Rails.logger.error(exception.message)
end
def parse(body)
unless body.blank?
begin
data = JSON.parse(body)
rescue JSON::ParserError
body.force_encoding('utf-8')
data = body.valid_encoding? ? body[0...512] : body.b[0...512].dump
end
return data
else
return nil
end
end
end
ブラウザとのやり取りだけではなく、リモートのMastodonサーバからのリクエストを記録してもらえます。
{
"request": {
"method": "GET",
"fullpath": "/.well-known/webfinger?resource=acct:zundan-mastodon-httplog.herokuapp.com@zundan-mastodon-httplog.herokuapp.com",
"headers": {
"HTTP_VERSION": [
"HTTP/1.1"
],
"HTTP_HOST": [
"zundan-mastodon-httplog.herokuapp.com"
],
"HTTP_CONNECTION": [
"close"
],
"HTTP_USER_AGENT": [
"http.rb/3.3.0 (Mastodon/3.0.1; +https://zundan-mastodon-remote.herokuapp.com/)"
],
"HTTP_X_REQUEST_ID": [
"9a6a523d-9b82-460b-8b87-00ce0568556c"
],
"HTTP_X_FORWARDED_FOR": [
"34.207.111.209"
],
"HTTP_X_FORWARDED_PROTO": [
"https"
],
"HTTP_X_FORWARDED_PORT": [
"443"
],
"HTTP_VIA": [
"1.1 vegur"
],
"HTTP_CONNECT_TIME": [
"1"
],
"HTTP_X_REQUEST_START": [
"1575881663884"
],
"HTTP_TOTAL_ROUTE_TIME": [
"0"
]
}
},
"response": {
"status": 200,
"headers": {
"Server": "Mastodon",
"X-Frame-Options": "DENY",
"X-Content-Type-Options": "nosniff",
"X-XSS-Protection": "1; mode=block",
"Vary": "Accept",
"Date": "Mon, 09 Dec 2019 08:54:23 GMT",
"Content-Type": "application/jrd+json; charset=utf-8",
"Cache-Control": "max-age=259200, public"
},
"body": {
"subject": "acct:zundan-mastodon-httplog.herokuapp.com@zundan-mastodon-httplog.herokuapp.com",
"aliases": [
"https://zundan-mastodon-httplog.herokuapp.com/actor"
],
"links": [
{
"rel": "http://webfinger.net/rel/profile-page",
"type": "text/html",
"href": "https://zundan-mastodon-httplog.herokuapp.com/about/more?instance_actor=true"
},
{
"rel": "self",
"type": "application/activity+json",
"href": "https://zundan-mastodon-httplog.herokuapp.com/actor"
}
]
}
}
}
セッションIDなど秘密にしなきゃいけない情報も記録されちゃうのでプロダクション環境で記録するのは避けたほうが良さそうです。
httplog gemに外部サービスとのやりとりを記録してもらう
Mastodonが利用しているhttplog
Gemではアプリのコードが外部とhttpで通信をする際のログの内容を設定することができます。Mastodonから提供されているイニシャライザ config/initializers/httplog.rb
を下記のように書き換えて、リクエスト・レスポンスヘッダを記録してもらいます。
HttpLog.configure do |config|
config.logger = Rails.logger
config.color = { color: :yellow }
config.log_connect = false
config.log_request = true
config.log_headers = true
config.log_data = false
config.log_status = true
config.log_response = false
config.log_benchmark = false
end
下記のように、まずリクエストの概要とヘッダを、続いてステータスコードとレスポンスヘッダが記録されます。
[httplog] Sending: GET https://zundan-mastodon-remote.herokuapp.com/.well-known/webfinger?resource=acct:remote@zundan-mastodon-remote.herokuapp.com
[httplog] Header: User-Agent: http.rb/3.3.0 (Mastodon/3.0.1; +https://zundan-mastodon-httplog.herokuapp.com/)
[httplog] Header: Connection: close
[httplog] Header: Host: zundan-mastodon-remote.herokuapp.com
[httplog] Status: 200
[httplog] Header: Connection: close
[httplog] Header: Server: Mastodon
[httplog] Header: X-Frame-Options: DENY
[httplog] Header: X-Content-Type-Options: nosniff
[httplog] Header: X-Xss-Protection: 1; mode=block
[httplog] Header: Vary: Accept, Accept-Encoding, Origin
[httplog] Header: Date: Mon, 09 Dec 2019 00:50:20 GMT
[httplog] Header: Content-Type: application/jrd+json; charset=utf-8
[httplog] Header: Cache-Control: max-age=259200, public
[httplog] Header: Etag: W/"b91adfc5323491547a25eb7469712cf9"
[httplog] Header: X-Request-Id: d2a1abeb-d777-48fa-9f82-5c170802f190
[httplog] Header: X-Runtime: 0.263245
[httplog] Header: Transfer-Encoding: chunked
[httplog] Header: Via: 1.1 vegur
ここで、config.log_data = true
としてレスポンスのボディを記録させてしまうと、app/lib/request.rb
でレスポンスのボディを利用する時に、http
GemがHTTP::StateError: body has already been consumed
というエラーを投げるようになります。config/initializers/httprb-response-body.rb
というファイルを作成してHTTP::Response::Body
のreadpartial
メソッドとto_s
メソッドにモンキーパッチを当てて、レスポンスをもらいながらログを記録することにしました。
# frozen_string_literal: true
#
# Monkey patch for
# https://github.com/httprb/http/blob/v3.3.0/lib/http/response/body.rb
module HTTP
class Response
class Body
def readpartial(*args)
stream!
chunk = @stream.readpartial(*args)
if chunk
chunk.force_encoding(@encoding)
Rails.logger.debug("Response body: #{chunk}") unless chunk.blank?
end
chunk
end
def to_s
return @contents if @contents
raise StateError, "body is being streamed" unless @streaming.nil?
begin
@streaming = false
@contents = String.new("").force_encoding(@encoding)
while (chunk = @stream.readpartial)
@contents << chunk.force_encoding(@encoding)
Rails.logger.debug("Response body: #{chunk}") unless chunk.blank?
chunk.clear # deallocate string
end
rescue
@contents = nil
raise
end
@contents
end
end
end
end
下記のようなログを記録してくれます。
Response body: {"subject":"acct:remote@zundan-mastodon-remote.herokuapp.com","aliases":["https://zundan-mastodon-remote.herokuapp.com/@remote","https://zundan-mastodon-remote.herokuapp.com/users/remote"],"links":[{"rel":"http://webfinger.net/rel/profile-page","type":"text/html","href":"https://zundan-mastodon-remote.herokuapp.com/@remote"},{"rel":"self","type":"application/activity+json","href":"https://zundan-mastodon-remote.herokuapp.com/users/remote"},{"rel":"http://ostatus.org/schema/1.0/subscribe","template":"https://zundan-mastodon-remote.herokuapp.com/authorize_interaction?uri={uri}"}]}
あ、この実装だと当然リクエストボディは記録できないんですよね。今は時間がないので、リクエストを受けた側で記録されたリクエストボディを確認することにします。
サーバの作成
そういうわけで、ログをたくさん記録するサーバを2つ、ローカルサーバとしてzundan-mastodon-a.herokuapp.com
を、そしてリモートサーバとしてzundan-mastodon-b.herokuapp.com
として作り、それぞれにalice
とbob
を登録してみました。これからこの2人のやりとりを覗いていきますね。
データベースのマイグレーションを走らせて、アカウントを1つ登録した段階で、実は2つのアカウントが登録されています。id: -99のものはインスタンスどうしの通信に代理として使われるようです。
> SELECT id, username, domain, actor_type FROM accounts;
id | username | domain | actor_type
-----+---------------------------------+--------+-------------
-99 | zundan-mastodon-a.herokuapp.com | | Application
1 | alice | |
(2 rows)
通信内容の記録
リモートサーバのアカウントを探す
作りたてのサーバ2つを稼働させて、ローカルサーバのaliceとしてブラウザからログインして、リモートサーバのbobを検索してみました。
下記のように、まずはWebFingerでリモートアカウントについての基本的な情報を得て、その後、WebFingerで得られたURLに対してHTTP署名をつけたリクエストを送り、リモートアカウントについてのより詳細な情報を取得していました。いっぽう、リモートサーバはWebFingerの受信に呼応してローカルサーバのインスタンスアカウントについての情報を、HTTP署名をつけて取得しに来ました。結果的に、ローカルサーバにはリモートアカウントの公開鍵を含めた情報が、リモートサーバにはローカルサーバのインスタンスアカウントについての情報が記録されました。
ローカルサーバのブラウザインターフェースの検索窓にフォーカスが移ると/api/v1/suggestions
へのGETリクエストがHTTP_ACCEPT: application/json, text/plain, */*
で送られるようです。結果は空でした。
{
"response": {
"status": 200,
"headers": {
"Server": "Mastodon",
"Content-Type": "application/json; charset=utf-8"
},
"body": []
}
}
ブラウザから/api/v2/search?q=@bob@zundan-mastodon-b.herokuapp.com&resolve=true&limit=5
へのGETリクエストが届き、処理が始まりました。
検索リクエストが届くと、まずリモートサーバへのWebFingerを送りました。
Sending: GET https://zundan-mastodon-b.herokuapp.com/.well-known/webfinger?resource=acct:bob@zundan-mastodon-b.herokuapp.com
Header: User-Agent: http.rb/3.3.0 (Mastodon/3.0.1; +https://zundan-mastodon-a.herokuapp.com/)
Header: Connection: close
Header: Host: zundan-mastodon-b.herokuapp.com
リモートサーバからは検索対象についての情報がStatus 200、Content-Type: application/jrd+json; charset=utf-8
で返却されました。
{
"subject": "acct:bob@zundan-mastodon-b.herokuapp.com",
"aliases": [
"https://zundan-mastodon-b.herokuapp.com/@bob",
"https://zundan-mastodon-b.herokuapp.com/users/bob"
],
"links": [
{
"rel": "http://webfinger.net/rel/profile-page",
"type": "text/html",
"href": "https://zundan-mastodon-b.herokuapp.com/@bob"
},
{
"rel": "self",
"type": "application/activity+json",
"href": "https://zundan-mastodon-b.herokuapp.com/users/bob"
},
{
"rel": "http://ostatus.org/schema/1.0/subscribe",
"template": "https://zundan-mastodon-b.herokuapp.com/authorize_interaction?uri={uri}"
}
]
}
次にリモートサーバからは、/actor
へのGETリクエストが届きました。リモートサーバの/actor#main-key
によるHTTP署名が付いていました。インスタンスアカウントについての情報を返しました。
リモートサーバはリクエストの送り先をどうやって決めたんだろう?
{
"request": {
"method": "GET",
"fullpath": "/actor",
"headers": {
"HTTP_VERSION": [
"HTTP/1.1"
],
"HTTP_HOST": [
"zundan-mastodon-a.herokuapp.com"
],
"HTTP_CONNECTION": [
"close"
],
"HTTP_USER_AGENT": [
"http.rb/3.3.0 (Mastodon/3.0.1; +https://zundan-mastodon-b.herokuapp.com/)"
],
"HTTP_DATE": [
"Thu, 12 Dec 2019 07:48:57 GMT"
],
"HTTP_ACCEPT_ENCODING": [
"gzip"
],
"HTTP_ACCEPT": [
"application/activity+json, application/ld+json"
],
"HTTP_SIGNATURE": [
"keyId=\"https://zundan-mastodon-b.herokuapp.com/actor#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date accept\",signature=\"VGtjjtIGDriQ4vyF3BF7rdTafcI21qGc2AuDYeoA840mZmkHWArgT7MPYCor75e54QnL+LTRJS8QPD6o0aJKWd4rvMItu+RkzJamcwaAFskIrs12gvYU0bnk9Oy7gfYRBBFfLUjDTYE96t5Q1Prub0uzPrZCxesDY2vB0Kc3zLZ4zkHyk4eXHa7Lk7Cu/owj4RBc4yume7OIYkQnPMZxpPbroGAC/DiwxNiqxaKjXybkcWcGnedQdDvj/kqk1dNOiVbabMVLs0qmHrCSd+rv2pJhabz9oW8uG05LcT3NHyAzgAuDCCOLhyjxQQkx87h2Qyt5hiFFRoTKf+mSt+xBWA==\""
]
}
},
"response": {
"status": 200,
"headers": {
"Server": "Mastodon",
"X-Frame-Options": "DENY",
"X-Content-Type-Options": "nosniff",
"X-XSS-Protection": "1; mode=block",
"Date": "Thu, 12 Dec 2019 07:48:57 GMT",
"Content-Type": "application/activity+json; charset=utf-8",
"Cache-Control": "max-age=600, public"
},
"body": {
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"toot": "http://joinmastodon.org/ns#",
"featured": {
"@id": "toot:featured",
"@type": "@id"
},
"alsoKnownAs": {
"@id": "as:alsoKnownAs",
"@type": "@id"
},
"movedTo": {
"@id": "as:movedTo",
"@type": "@id"
},
"schema": "http://schema.org#",
"PropertyValue": "schema:PropertyValue",
"value": "schema:value",
"IdentityProof": "toot:IdentityProof",
"discoverable": "toot:discoverable"
}
],
"id": "https://zundan-mastodon-a.herokuapp.com/actor",
"type": "Application",
"inbox": "https://zundan-mastodon-a.herokuapp.com/actor/inbox",
"preferredUsername": "zundan-mastodon-a.herokuapp.com",
"url": "https://zundan-mastodon-a.herokuapp.com/about/more?instance_actor=true",
"manuallyApprovesFollowers": true,
"publicKey": {
"id": "https://zundan-mastodon-a.herokuapp.com/actor#main-key",
"owner": "https://zundan-mastodon-a.herokuapp.com/actor",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1y0tCWNc8P1befA0KFj6\n91zJBo5g0sBIO0xEQVDO/7N3wotXjQHiHnX/HrMAe1giuQXVpbStp4aLygRtyH/p\nQNTp2R4mOm7Ov+XRWbK5VM0fzYHyooK7fkI0dGnF2cqGJdyD7VS9OynJ1ii9nHiv\nfdWlea27H5TDAnq3NO7rBIaW9qaPg80uJSn4sfC7X0ktnuiDp0syUIUqcBgiXaXU\nsFkeu2H5pCSEdU8O5NUSs/gln6eEhz9AMbGtA/+U9ilGh+oUxkdBeFWe/5xAdV/U\nUaDtNO0l/djezoIFN5WK0vI6UvBHBtGhJsQifQux1fukhxERw30XAG9bpxMbvnsr\nEwIDAQAB\n-----END PUBLIC KEY-----\n"
},
"endpoints": {
"sharedInbox": "https://zundan-mastodon-a.herokuapp.com/inbox"
}
}
}
}
面白いことに、ログにはリモートサーバからの同じリクエストがもう1つ、それに対応するレスポンスと一緒に記録されていました。
次に、リモートサーバから、インスタンスアカウントへのWebFingerが届いて情報を返しました。
{
"request": {
"method": "GET",
"fullpath": "/.well-known/webfinger?resource=acct:zundan-mastodon-a.herokuapp.com@zundan-mastodon-a.herokuapp.com",
"headers": {
"HTTP_VERSION": [
"HTTP/1.1"
],
"HTTP_HOST": [
"zundan-mastodon-a.herokuapp.com"
],
"HTTP_CONNECTION": [
"close"
],
"HTTP_USER_AGENT": [
"http.rb/3.3.0 (Mastodon/3.0.1; +https://zundan-mastodon-b.herokuapp.com/)"
]
}
},
"response": {
"status": 200,
"headers": {
"Server": "Mastodon",
"X-Frame-Options": "DENY",
"X-Content-Type-Options": "nosniff",
"X-XSS-Protection": "1; mode=block",
"Vary": "Accept",
"Date": "Thu, 12 Dec 2019 07:48:57 GMT",
"Content-Type": "application/jrd+json; charset=utf-8",
"Cache-Control": "max-age=259200, public"
},
"body": {
"subject": "acct:zundan-mastodon-a.herokuapp.com@zundan-mastodon-a.herokuapp.com",
"aliases": [
"https://zundan-mastodon-a.herokuapp.com/actor"
],
"links": [
{
"rel": "http://webfinger.net/rel/profile-page",
"type": "text/html",
"href": "https://zundan-mastodon-a.herokuapp.com/about/more?instance_actor=true"
},
{
"rel": "self",
"type": "application/activity+json",
"href": "https://zundan-mastodon-a.herokuapp.com/actor"
}
]
}
}
}
リモートサーバでは返却された情報をPostgresに格納しました。
INSERT INTO "accounts" ("username", "domain", "public_key", "created_at", "updated_at", "uri", "url", "locked", "last_webfingered_at", "inbox_url", "shared_inbox_url", "protocol", "featured_collection_url", "fields", "actor_type", "discoverable", "also_known_as")
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) RETURNING "id"
[["username", "zundan-mastodon-a.herokuapp.com"], ["domain", "zundan-mastodon-a.herokuapp.com"], ["public_key", "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1y0tCWNc8P1befA0KFj6\n91zJBo5g0sBIO0xEQVDO/7N3wotXjQHiHnX/HrMAe1giuQXVpbStp4aLygRtyH/p\nQNTp2R4mOm7Ov+XRWbK5VM0fzYHyooK7fkI0dGnF2cqGJdyD7VS9OynJ1ii9nHiv\nfdWlea27H5TDAnq3NO7rBIaW9qaPg80uJSn4sfC7X0ktnuiDp0syUIUqcBgiXaXU\nsFkeu2H5pCSEdU8O5NUSs/gln6eEhz9AMbGtA/+U9ilGh+oUxkdBeFWe/5xAdV/U\nUaDtNO0l/djezoIFN5WK0vI6UvBHBtGhJsQifQux1fukhxERw30XAG9bpxMbvnsr\nEwIDAQAB\n-----END PUBLIC KEY-----\n"], ["created_at", "2019-12-12 07:48:57.890086"], ["updated_at", "2019-12-12 07:48:57.890086"], ["uri", "https://zundan-mastodon-a.herokuapp.com/actor"], ["url", "https://zundan-mastodon-a.herokuapp.com/about/more?instance_actor=true"], ["locked", true], ["last_webfingered_at", "2019-12-12 07:48:57.882914"], ["inbox_url", "https://zundan-mastodon-a.herokuapp.com/actor/inbox"], ["shared_inbox_url", "https://zundan-mastodon-a.herokuapp.com/inbox"], ["protocol", 1], ["featured_collection_url", ""], ["fields", "{}"], ["actor_type", "Application"], ["discoverable", false], ["also_known_as", "{}"]]
次にローカルサーバからGETリクエストをリモートサーバの/users/bob
に送りました。Accept-Encoding: gzip
、Accept: application/activity+json, application/ld+json
で、/actor#main-key
によるHTTP署名が付いていました。リモートサーバからのレスポンスは200でボディは下記のようなものでした。
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"toot": "http://joinmastodon.org/ns#",
"featured": {
"@id": "toot:featured",
"@type": "@id"
},
"alsoKnownAs": {
"@id": "as:alsoKnownAs",
"@type": "@id"
},
"movedTo": {
"@id": "as:movedTo",
"@type": "@id"
},
"schema": "http://schema.org#",
"PropertyValue": "schema:PropertyValue",
"value": "schema:value",
"IdentityProof": "toot:IdentityProof",
"discoverable": "toot:discoverable"
}
],
"id": "https://zundan-mastodon-b.herokuapp.com/users/bob",
"type": "Person",
"following": "https://zundan-mastodon-b.herokuapp.com/users/bob/following",
"followers": "https://zundan-mastodon-b.herokuapp.com/users/bob/followers",
"inbox": "https://zundan-mastodon-b.herokuapp.com/users/bob/inbox",
"outbox": "https://zundan-mastodon-b.herokuapp.com/users/bob/outbox",
"featured": "https://zundan-mastodon-b.herokuapp.com/users/bob/collections/featured",
"preferredUsername": "bob",
"name": "",
"summary": "<p></p>",
"url": "https://zundan-mastodon-b.herokuapp.com/@bob",
"manuallyApprovesFollowers": false,
"discoverable": null,
"publicKey": {
"id": "https://zundan-mastodon-b.herokuapp.com/users/bob#main-key",
"owner": "https://zundan-mastodon-b.herokuapp.com/users/bob",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqVY0KCfnhep54ESjcBqs\nj5Ht3u1ovmM1Z+5HYUYJBiDJyPJ0887lqtrPhG8jbj4+FxDO7wlVx9E7WuXhu357\nQ8FUNxtZB9bXlKwuZTy8zF47k0QUHzeroCB09qrxu+FCTbxQP1fAI/ADWO5/1CvD\n8cxGu5ZarN9fiOLMe2JsS9rSKTt5q+ckC29tfTeMlXBlblIyxjViE/u4TRS3hYKB\n2bN5pqCJvz+d4to/iMizNJlD5js4q31hfiVicGvrLwInFJ9mmCHVJQ4iSwBcZhPm\nwOWS4gGBJIUtCFklje3f9uc6csvrgftY3BrXx+Mb7MaSYOQQDrBX5HOqqoPbaNKq\nEwIDAQAB\n-----END PUBLIC KEY-----\n"
},
"tag": [],
"attachment": [],
"endpoints": {
"sharedInbox": "https://zundan-mastodon-b.herokuapp.com/inbox"
}
}
このリクエストとレスポンスも、もう1度繰り返し記録されていました。
次にローカルサーバからはGETリクエストをリモートサーバの/users/bob/outbox
に送りました。上記と同様のリクエストヘッダを付けHTTP署名していました。リモートサーバからのレスポンスは200でボディは下記のようなものでした。
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://zundan-mastodon-b.herokuapp.com/users/bob/outbox",
"type": "OrderedCollection",
"totalItems": 0,
"first": "https://zundan-mastodon-b.herokuapp.com/users/bob/outbox?page=true",
"last": "https://zundan-mastodon-b.herokuapp.com/users/bob/outbox?min_id=0&page=true"
}
次にローカルサーバからはGETリクエストをリモートサーバの/users/bob/following
に送りました。上記と同様のリクエストヘッダを付けHTTP署名していました。リモートサーバからのレスポンスは200でボディは下記のようなものでした。
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://zundan-mastodon-b.herokuapp.com/users/bob/following",
"type": "OrderedCollection",
"totalItems": 0,
"first": "https://zundan-mastodon-b.herokuapp.com/users/bob/following?page=1"
}
次にローカルサーバからはGETリクエストをリモートサーバの/users/bob/followers
にも送りました。上記と同様のリクエストヘッダ、HTTP署名でした。リモートサーバからのレスポンスは200でボディは下記のようなものでした。
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://zundan-mastodon-b.herokuapp.com/users/bob/followers",
"type": "OrderedCollection",
"totalItems": 0,
"first": "https://zundan-mastodon-b.herokuapp.com/users/bob/followers?page=1"
}
ローカルサーバはPostgresにリモートアカウントの情報を格納しました。
INSERT INTO "accounts" ("username", "domain", "public_key", "created_at", "updated_at", "note", "uri", "url", "last_webfingered_at", "inbox_url", "outbox_url", "shared_inbox_url", "followers_url", "protocol", "featured_collection_url", "fields", "actor_type", "discoverable", "also_known_as")
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19) RETURNING "id"
[["username", "bob"], ["domain", "zundan-mastodon-b.herokuapp.com"], ["public_key", "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqVY0KCfnhep54ESjcBqs\nj5Ht3u1ovmM1Z+5HYUYJBiDJyPJ0887lqtrPhG8jbj4+FxDO7wlVx9E7WuXhu357\nQ8FUNxtZB9bXlKwuZTy8zF47k0QUHzeroCB09qrxu+FCTbxQP1fAI/ADWO5/1CvD\n8cxGu5ZarN9fiOLMe2JsS9rSKTt5q+ckC29tfTeMlXBlblIyxjViE/u4TRS3hYKB\n2bN5pqCJvz+d4to/iMizNJlD5js4q31hfiVicGvrLwInFJ9mmCHVJQ4iSwBcZhPm\nwOWS4gGBJIUtCFklje3f9uc6csvrgftY3BrXx+Mb7MaSYOQQDrBX5HOqqoPbaNKq\nEwIDAQAB\n-----END PUBLIC KEY-----\n"], ["created_at", "2019-12-12 07:48:58.125482"], ["updated_at", "2019-12-12 07:48:58.125482"], ["note", "<p></p>"], ["uri", "https://zundan-mastodon-b.herokuapp.com/users/bob"], ["url", "https://zundan-mastodon-b.herokuapp.com/@bob"], ["last_webfingered_at", "2019-12-12 07:48:57.982457"], ["inbox_url", "https://zundan-mastodon-b.herokuapp.com/users/bob/inbox"], ["outbox_url", "https://zundan-mastodon-b.herokuapp.com/users/bob/outbox"], ["shared_inbox_url", "https://zundan-mastodon-b.herokuapp.com/inbox"], ["followers_url", "https://zundan-mastodon-b.herokuapp.com/users/bob/followers"], ["protocol", 1], ["featured_collection_url", "https://zundan-mastodon-b.herokuapp.com/users/bob/collections/featured"], ["fields", "[]"], ["actor_type", "Person"], ["discoverable", false], ["also_known_as", "{}"]]
これでやっと、ブラウザから/api/v2/search?q=@bob@zundan-mastodon-b.herokuapp.com&resolve=true&limit=5
へのGETリクエストへのレスポンスを、Status 200、Content-Type: application/jrd+json; charset=utf-8
で返すことができました。
{
"accounts": [
{
"id": "2",
"username": "bob",
"acct": "bob@zundan-mastodon-b.herokuapp.com",
"display_name": "",
"locked": false,
"bot": false,
"created_at": "2019-12-12T07:48:58.125Z",
"note": "<p></p>",
"url": "https://zundan-mastodon-b.herokuapp.com/@bob",
"avatar": "https://zundan-mastodon-a.herokuapp.com/avatars/original/missing.png",
"avatar_static": "https://zundan-mastodon-a.herokuapp.com/avatars/original/missing.png",
"header": "https://zundan-mastodon-a.herokuapp.com/headers/original/missing.png",
"header_static": "https://zundan-mastodon-a.herokuapp.com/headers/original/missing.png",
"followers_count": 0,
"following_count": 0,
"statuses_count": 0,
"last_status_at": null,
"emojis": [],
"fields": []
}
],
"statuses": [],
"hashtags": []
}
最後に、ブザウザから/api/v1/accounts/relationships?id[]=2
へのGETリクエストが届き、Status 200、Content-Type: application/jrd+json; charset=utf-8
で下記のボディを返却しました。
[
{
"id": "2",
"following": false,
"showing_reblogs": false,
"followed_by": false,
"blocking": false,
"blocked_by": false,
"muting": false,
"muting_notifications": false,
"requested": false,
"domain_blocking": false,
"endorsed": false
}
]
ここで、今後の観察で2つのサーバの動作の比較がしやすいように、リモートサーバのbobからローカルサーバのaliceも検索しておきました。
アカウントをフォローする
ローカルサーバのaliceとして上記で見つけたリモートサーバのbobをフォローしてみます。ブラウザからリモートアカウントをフォローすると、SidekiqからロモートサーバにFollow Activityが送られました。HTTP署名はフォロー元のローカルアカウントによるものでした。
リモートアカウントをフォローするには、ブラウザでリモートアカウントの検索する必要がありました。レスポンスは上記と同様で、ローカルサーバでのリモートアカウントのID 2が含まれています。
次に、ブラウザからローカルサーバの/api/v1/accounts/relationships?id[]=2
へのGETリクエストが届きました。レスポンスはStatusは200でボディは下記の通り:
[
{
"id": "2",
"following": false,
"showing_reblogs": false,
"followed_by": false,
"blocking": false,
"blocked_by": false,
"muting": false,
"muting_notifications": false,
"requested": false,
"domain_blocking": false,
"endorsed": false
}
]
最後にブラウザからローカルサーバの/api/v1/accounts/2/follow
にPOSTリクエストが届きました。リクエストボディは下記の通り。
{
"reblogs": true
}
これに対してのローカルサーバからのレスポンスはStatus 200でボディは下記の通り。この時点ではフォローの処理は進んでおらず、後ほどSidekiqからリモートサーバへのリクエストが送られます。
{
"id": "2",
"following": true,
"showing_reblogs": true,
"followed_by": false,
"blocking": false,
"blocked_by": false,
"muting": false,
"muting_notifications": false,
"requested": false,
"domain_blocking": false,
"endorsed": false
}
ジョブキューの処理を進め、ローカルサーバのSidekiqからリモートサーバの/users/bob/inbox
へPOSTリクエストを送りました。ローカルサーバの/alice#main-key
によるHTTP署名が付いていました。リクエストボディは下記の通り、ActivitPubのFollow Activityでした。リモートサーバからは、Status 202でボディの無いレスポンスが返されました。
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://zundan-mastodon-a.herokuapp.com/b984cd13-58e4-4465-aad4-980a82b6e8fa",
"type": "Follow",
"actor": "https://zundan-mastodon-a.herokuapp.com/users/alice",
"object": "https://zundan-mastodon-b.herokuapp.com/users/bob"
}
次に、ローカルサーバのSidekiqからリモートサーバの/users/bob/collections/featured
へGETリクエストを送りました。ローカルサーバの/actor#main-key
によるHTTP署名が付いていました。リモートサーバからは、Status 200で下記のレスポンスが返されました。
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://zundan-mastodon-b.herokuapp.com/users/bob/collections/featured",
"type": "OrderedCollection",
"orderedItems": []
}
リモートサーバのbobからもローカルサーバのaliceをフォローしておきました。
トゥートを配送する
ローカルサーバにaliceとしてブラウザからトゥートを投稿しました。ブラウザからは、/api/v1/statuses
にPOSTリクエストが届きました。リクエストのボディは下記の通り:
{
"status": "テストですと",
"in_reply_to_id": null,
"media_ids": [],
"sensitive": false,
"spoiler_text": "",
"visibility": "public",
"poll": null
}
下記のようなレスポンスをStatus 200で返しました。ちゃんと日本語だと判定されてる。
{
"id": "103309317046763904",
"created_at": "2019-12-15T01:57:57.183Z",
"in_reply_to_id": null,
"in_reply_to_account_id": null,
"sensitive": false,
"spoiler_text": "",
"visibility": "public",
"language": "ja",
"uri": "https://zundan-mastodon-a.herokuapp.com/users/alice/statuses/103309317046763904",
"url": "https://zundan-mastodon-a.herokuapp.com/@alice/103309317046763904",
"replies_count": 0,
"reblogs_count": 0,
"favourites_count": 0,
"favourited": false,
"reblogged": false,
"muted": false,
"bookmarked": false,
"pinned": false,
"content": "<p>テストですと</p>",
"reblog": null,
"application": null,
"account": {
"id": "1",
"username": "alice",
"acct": "alice",
"display_name": "",
"locked": false,
"bot": false,
"created_at": "2019-12-12T07:42:58.184Z",
"note": "<p></p>",
"url": "https://zundan-mastodon-a.herokuapp.com/@alice",
"avatar": "https://zundan-mastodon-a.herokuapp.com/avatars/original/missing.png",
"avatar_static": "https://zundan-mastodon-a.herokuapp.com/avatars/original/missing.png",
"header": "https://zundan-mastodon-a.herokuapp.com/headers/original/missing.png",
"header_static": "https://zundan-mastodon-a.herokuapp.com/headers/original/missing.png",
"followers_count": 1,
"following_count": 1,
"statuses_count": 1,
"last_status_at": "2019-12-15T01:57:57.239Z",
"emojis": [],
"fields": []
},
"media_attachments": [],
"mentions": [],
"tags": [],
"emojis": [],
"card": null,
"poll": null
}
ローカルアカウントaliceはリモートアカウントbobにフォローされているので、Sidekiqからトゥートを配達しました。リモートサーバ共有の/inbox
にContent-Type: application/activity+json
で、alice#main-key
によるHTTP署名を付けて下記の内容をPOSTしました。Create Activityで、HTTPヘッダだけではなくリクエストボディのObjectにも署名が付いていました。Status 202でボディの無いレスポンスが返されました。
{
"@context": [
"https://www.w3.org/ns/activitystreams",
{
"ostatus": "http://ostatus.org#",
"atomUri": "ostatus:atomUri",
"inReplyToAtomUri": "ostatus:inReplyToAtomUri",
"conversation": "ostatus:conversation",
"sensitive": "as:sensitive",
"toot": "http://joinmastodon.org/ns#",
"votersCount": "toot:votersCount"
}
],
"id": "https://zundan-mastodon-a.herokuapp.com/users/alice/statuses/103309317046763904/activity",
"type": "Create",
"actor": "https://zundan-mastodon-a.herokuapp.com/users/alice",
"published": "2019-12-15T01:57:57Z",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://zundan-mastodon-a.herokuapp.com/users/alice/followers"
],
"object": {
"id": "https://zundan-mastodon-a.herokuapp.com/users/alice/statuses/103309317046763904",
"type": "Note",
"summary": null,
"inReplyTo": null,
"published": "2019-12-15T01:57:57Z",
"url": "https://zundan-mastodon-a.herokuapp.com/@alice/103309317046763904",
"attributedTo": "https://zundan-mastodon-a.herokuapp.com/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://zundan-mastodon-a.herokuapp.com/users/alice/followers"
],
"sensitive": false,
"atomUri": "https://zundan-mastodon-a.herokuapp.com/users/alice/statuses/103309317046763904",
"inReplyToAtomUri": null,
"conversation": "tag:zundan-mastodon-a.herokuapp.com,2019-12-15:objectId=1:objectType=Conversation",
"content": "<p>テストですと</p>",
"contentMap": {
"ja": "<p>テストですと</p>"
},
"attachment": [],
"tag": [],
"replies": {
"id": "https://zundan-mastodon-a.herokuapp.com/users/alice/statuses/103309317046763904/replies",
"type": "Collection",
"first": {
"type": "CollectionPage",
"next": "https://zundan-mastodon-a.herokuapp.com/users/alice/statuses/103309317046763904/replies?only_other_accounts=true&page=true",
"partOf": "https://zundan-mastodon-a.herokuapp.com/users/alice/statuses/103309317046763904/replies",
"items": []
}
}
},
"signature": {
"type": "RsaSignature2017",
"creator": "https://zundan-mastodon-a.herokuapp.com/users/alice#main-key",
"created": "2019-12-15T01:57:57Z",
"signatureValue": "fpAmHqxW3L0hPuQGU3JLjliWGt0LtIbKDOg2uASmRs4qkLcXWP+dK5XHO6y8GulLZDMbN864JU9c9c17148HxfWmDKzV2ImlStDU/EqLit4M8tC+a+8AC05VYcKxD6xXmS6OZt3xaMxrb2Fx9MZZ64tegDDugDEN0DQpeNuABrwKgxyuOe4dHbw6OIU47iyRyxWiPjOpaL4b8r8x2MzYIDV1rwbqnuqJCbJJdPZK4Se1n0YnLmtRXQvL800o214J//nWKZ2PAyaKiR3ViEGi2szEZBUkIK+dcsKADreaC8s3JS/DfcSVRDMKJwFUvQwjstWhwT3kfNIAOqXU6Lv20Q=="
}
}