Mastodonどうしのお話を盗み聞きしてみる

分散SNSソフトのMastodon、[7月にmaster追従勢のトゥートがリモートのタイムラインに表示されなくなる](https://mastodon.crazynewworld.net/@hanage999/102409801315624762)ことがありました。トゥートを受け取る側のサーバで、`bundle: Rejected Create activity`といったエラーが記録される問題が、[シリアライザのキャッシュのキーを改善する](https://github.com/tootsuite/mastodon/pull/11271)ことでなおりました。

え？どゆこと？

ということで、Mastodonサーバどうしがやりとりをするときの様子を眺めてみることにしました。現在のMastodonはActivityPubという規格にしたがって実装されています。まず規格の内容を確認してから、Mastodonの実装の内容と稼働の様子を眺めてみます。

これは、[分散SNSアドベントカレンダー](https://adventar.org/calendars/4408)の12月16日分の記事です。15日は@svchost@pawoo.netさんによる「[otajodon.comで学んだLTLの功罪](https://fusetter.com/tw/lVMq4)」でした。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](https://www.w3.org/TR/activitypub/)は分散ソーシャルネットワークサービスのための規格で、クライアントとサーバの間の通信、また、サーバどうしの通信について、投稿内容のやりとりのためのAPIを規定しています。ActivityPubの概要については、[分散SNSアドベントカレンダー](https://adventar.org/calendars/4408)の12月7日の記事[[Fediverse][Protocol] ActivityPubを深堀してみる ](https://www.bluecore.net/2019/12/07/fediverseprotocol-activitypub%e3%82%92%e6%b7%b1%e5%a0%80%e3%81%97%e3%81%a6%e3%81%bf%e3%82%8b/)でlocalYouserさんが解説してくださってます。[Objects](https://www.w3.org/TR/activitypub/#obj)章では、「なりすましを防ぐため、サーバは受け取ったcontentを検証する必要がある(should)」と規定されています。でも具体的な方法は規定してないんだよね。

Mastodonはといえば、[ActivityPubのサーバどうしの通信の規格に準拠](https://docs.joinmastodon.org/development/activitypub/)した実装として、[WebFinger](https://tools.ietf.org/html/rfc7033)でアカウントを見つけ、[HTTP signatures spec](https://tools.ietf.org/html/draft-cavage-http-signatures-10) (この記事ではHTTP署名と書きます)に従ってinboxへの配送内容を認証します。この記事ではこの部分を更に掘りすすんでみます。

## ログをたくさん記録するようにする
この記事では、[2019年11月21日ごろのMastodon](https://github.com/tootsuite/mastodon/tree/a60b602ade955c8ce620facae160ffabd7893d4b)を改造してローカルサーバへのリクエストやリモートサーバへのリクエストを記録するようにしました。改造後のコードは[zunda/mastodonのlog-requestsブランチ](https://github.com/zunda/mastodon/tree/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など秘密にしなきゃいけない情報も記録されちゃうのでプロダクション環境で記録するのは避けたほうが良さそうです。

```sql
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`への変更点:

```patch
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`の内容

```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サーバからのリクエストを記録してもらえます。

```json
{
  "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で通信をする際の[ログの内容を設定する](https://github.com/trusche/httplog#configuration)ことができます。Mastodonから提供されているイニシャライザ `config/initializers/httplog.rb`を下記のように書き換えて、リクエスト・レスポンスヘッダを記録してもらいます。

```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](https://github.com/httprb/http)が`HTTP::StateError: body has already been consumed`というエラーを投げるようになります。`config/initializers/httprb-response-body.rb`
というファイルを作成して`HTTP::Response::Body`の`readpartial`メソッドと`to_s`メソッドにモンキーパッチを当てて、レスポンスをもらいながらログを記録することにしました。

```rb
# 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`として[作り](https://github.com/zunda/mastodon/wiki/CreateInstanceOnHeroku)、それぞれに`alice`と`bob`を登録してみました。これからこの2人のやりとりを覗いていきますね。

データベースのマイグレーションを走らせて、アカウントを1つ登録した段階で、実は2つのアカウントが登録されています。id: -99のものはインスタンスどうしの通信に代理として使われるようです。

```sql
> 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, */*`で送られるようです。結果は空でした。

```json
{
  "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`で返却されました。

```json
{
  "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署名が付いていました。インスタンスアカウントについての情報を返しました。

リモートサーバはリクエストの送り先をどうやって決めたんだろう？

```json
{
  "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が届いて情報を返しました。

```json
{
  "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に格納しました。


```sql
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でボディは下記のようなものでした。

```json
{
  "@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でボディは下記のようなものでした。

```json
{
  "@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でボディは下記のようなものでした。

```json
{
  "@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でボディは下記のようなものでした。

```json
{
  "@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にリモートアカウントの情報を格納しました。

```sql
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`で返すことができました。

```json
{
  "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`で下記のボディを返却しました。

```json
[
  {
    "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でボディは下記の通り:

```json
[
  {
    "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リクエストが届きました。リクエストボディは下記の通り。

```json
{
  "reblogs": true
}
```

これに対してのローカルサーバからのレスポンスはStatus 200でボディは下記の通り。この時点ではフォローの処理は進んでおらず、後ほどSidekiqからリモートサーバへのリクエストが送られます。

```json
{
  "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でボディの無いレスポンスが返されました。

```json
{
  "@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で下記のレスポンスが返されました。

```json
{
  "@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リクエストが届きました。リクエストのボディは下記の通り:

```json
{
  "status": "テストですと",
  "in_reply_to_id": null,
  "media_ids": [],
  "sensitive": false,
  "spoiler_text": "",
  "visibility": "public",
  "poll": null
}
```

下記のようなレスポンスをStatus 200で返しました。ちゃんと日本語だと判定されてる。

```json
{
  "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でボディの無いレスポンスが返されました。

```json
{
  "@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=="
  }
}
```