🍰

[Raspberry Pi OS]オフラインのRaspberry Pi Zeroだけで作るおうちActivityPub(PSH)

8 min read

はじめに

この記事は、Fediverse Advent Calendar 2021 25日目の記事です。

昨日はよく隣り合わせになるID:weepさんの「Fediverse Advent Calendar 2021」24日目
でした。

こんにちは黒ヰ樹です。
今回は拙作PSHの紹介をします。

https://gitlab.com/tkithrta/psh
https://psh.vercel.app/
https://psh.vercel.app/u/a

最初に公開したのは2019/06/16で、結構前から公開していた作品となる、初めて作った
Free/Libre and Open Source Softwareとなります。
約2年半色々なことがあったように思えます。
ちょうど当時knzk.me(リンク先音量注意)をやめたり転職活動をしていたものですから、色々あってPSHを作成、公開しました。

PSHとは

まずPSHとはなにか紹介したいと思います。
PSHはデータベースやファイルの保存などを行わない、送信(POST, PUSH)のみを行うエッジコンピューティングで動かすことを想定したActivityPub実装です。
当時はZeit Nowで動かしていましたが、Vercelに社名変更、ドメイン変更が行われるようになったことで仕様が変わったため長らくメンテが行われていない状況が続いていました。

https://vercel.com/

しかしながら今回、大規模アップデートを行い、ついに1.0.0を達成することができました!

PSH v1

v1以前のRC版とv1版がどのように違うのか紹介していきたいと思います。

Raspberry Pi対応

ついに念願のRaspberry Piに対応しました!
オフラインのRaspberry Pi Zero WHで問題なく動作することを確認できているので、5ドルで買えるRaspberry Pi Zeroの初期ロットでも動くはずです。

動かすためにはまずRaspberry Pi ImagerあたりでRaspberry Pi OSを入れます。
そうすると標準で様々なaptパッケージ、Pythonパッケージが入っていると思うので、
ダウンロードした圧縮ファイルをUSBメモリで移動して以下のコマンドを叩けばPSHを動かすことができます。

$ openssl ecparam -genkey -name prime256v1 -out localhost.key
$ openssl req -new -sha256 -subj /CN=localhost -key localhost.key -out localhost.csr
$ openssl x509 -req -signkey localhost.key -in localhost.csr -out localhost.crt
$ od -vAn -tx1 -w32 -N32 /dev/urandom | tr -d ' ' >> secret.txt
$ yes n | ssh-keygen -b 4096 -m PEM -t rsa -N '' -f id_rsa

$ export PYTHONWARNINGS=ignore
$ export CURL_CA_BUNDLE=localhost.crt
$ export SECRET="$(tail -n1 secret.txt)"
$ export PRIVATE_KEY="$(cat id_rsa)"

これをまとめたものがinit.shとcommand.shとしてPSHに同梱しているので、以下のコマンドでも同じことができます

$ bash init.sh
$ source command.sh

最後に

$ flask run -h localhost -p 8080 --cert localhost.crt --key localhost.key

を叩けば自己署名証明書でhttps化したlocalhostが生成されるのでDilloまたはChromiumあたりで確認できます。

$ dillo https://localhost:8080/

おうちActivityPubを試すためには別ホスト別ポートでもう一つ建てる必要があります。今回は8008ポートで建てます

$ flask run -h localhost -p 8008 --cert localhost.crt --key localhost.key

あとは後述するcurlでコマンドを叩けばお互いActivityPubを飛ばし合っていることを確認できます。
ポートの違いではなくホストやドメインの違いで角煮にしたい場合は管理者権限で/etc/hostsを編集する必要があります。

Gitpod対応

また、今回Gitpodにも対応しました。
この場合自己署名証明書は必要なくなり、一時的に外部サーバーとも連合できます。
そもそもPSH v1は以前作ったStrawberryFields Flaskをフォークして少し変えただけなので同じように動かせます。

https://gitlab.com/acefed/strawberryfields-flask

Raspberry Piとは異なりgunicornとpython-dotenvを使うことができます。

$ pip install -r requirements.txt
$ bash init.sh
$ source command.sh
$ gunicorn app:app -b 0:8080

このコマンドを自動化した.gitpod.ymlに同梱してあるので、Gitpodボタンを押すだけで簡単にビルドできます。
もしFlask組み込みのWebサーバーで動かす場合、Raspberry Piとは異なり$ flask run -h 0.0.0.0 -p 8080コマンドで起動する必要があります。
Gitpodに関する解説は色んな記事で書いてきたので省略します。
例えば今回Gitpod Misskeyの紹介記事をMisskey Advent Calendarで書いたのでこちらを読めば分かるかもしれないです。

https://zenn.dev/tkithrta/articles/21bb7e49f6941e

https://adventar.org/calendars/6273

Vercel対応

前からZeit Nowで動かしていたのでドメイン変更が行われた後もVercelである程度動いていたのですが、
次のVercelのバージョンからZeit Nowの頃残していた機能がすべて使えなくなるらしい
(now.shドメインは変わらずリダイレクトされる)ので、ちゃんとVercel対応することにしました。
この場合自己署名証明書は必要なくなり、GunicornなしでVercel組み込みランタイムサーバーで安全に動き、永続的に外部サーバーとも連合できます。
もちろん無料です。ある程度の制限はありますが。

ちなみにVercelはGitインテグレーションでビルドする方法とcliからログインしてビルドする方法があり、私はcliからビルドする方法で以前建てていたのですが、
今回からGitLabのプライベートリポジトリ(PSH Canary)経由でGitインテグレーションをすることにしました。
こうすればGitpodからGitLabのプライベートリポジトリを編集して、GitLabインテグレーションでVercelへ自動的にデプロイされるようになりますし、
開発版をコミットする心理的負担も減ります。

依存関係

Webアプリケーションフレームワークは変わらずFlask、HTTPリクエストクライアントはRequestsです。両方ともRaspberry Pi OSに標準で入っています。
以前は暗号化周りでPyCryptodomeを使用していたのですが、サイズが大きかったりRaspberry Pi OSに標準で入っていなかったりしたので
Cryptographyに変更しました。これならRaspberry Pi OSから標準で入っていますし軽いです。
また現時点で鍵生成周りはPythonでやっておらず、OpenSSHのssh-keygenとOpenSSLを使用しています。ここらへんは別の開発と互換性を持たせたり
Pythonランタイム入っていないWindows10から鍵生成できるようにする関係でそうしています。
もちろんRaspberry Pi OSには両方入っています。

その他Windows10に標準で入っているtarとcurlを使ったり、
GitLabやGitpod、Vercelを使用している関係上gitに依存しています。
Raspberry Pi OSに入っているので問題ないですね!
Raspberry Pi OSはオフラインのRaspberry Pi Zeroであっても様々なことができるのでおすすめです。
例えば今回Redisをビルドした記事をRaspberry Pi Advent Calendarで書いたりしました。

https://zenn.dev/tkithrta/articles/fb2b182dd88d91

https://adventar.org/calendars/6256

シンプルな設計

特に難しいことをしていないですし、ファイル数も少ないためRaspberry Pi OSに入っているvim, nano, Geany, Thonnyだけで簡単に作れます。
Pythonファイルは今の所app.pyだけです。

画像は作れないので別の方法で頑張ってください。

以前はJinja2を使用したHTMLテンプレートでトップページやユーザーページを表示していたのですが、
一度なくすことにしました。もう少し練ってから追加したいと思います。
その他は以前と変わらずシンプルな設計で、ランダム生成したSECRET文字列を含むURLへ何らかの方法を使ってPOSTするだけです。

curlでクエリをPOST

/s/-/u/の間にある-は環境変数SECRETです。都度ランダム生成した文字列に読み替えてください。

$ curl -X POST "https://localhost:8080/s/-/u/a?type=type&id=https://localhost:8008/u/a"

まずこうPOSTすればJSON(JSON-LD)に含まれるtypeの中身を表示できます。
これは別にActivityPubを使っておらず、どちらかというとFlaskやRequestsのテストとして使える機能です。
このようにtypeとidを使用したquery-stringを変更するだけで様々な機能が使えます。

Follow実装

$ curl -X POST "https://localhost:8080/s/-/u/a?type=follow&id=https://localhost:8008/u/a"
$ curl -X POST "https://localhost:8080/s/-/u/a?type=undo_follow&id=https://localhost:8008/u/a"

こうすればidで指定したActorをフォローすることができます。
オフラインでもターミナルで動作を確認できますし、GitpodやVercelだと実際に
Mastodon、Pleroma、Misskeyなど外部サーバーへフォローを送信できる機能です。
PSHで最初に実装した機能です。

Like実装

$ curl -X POST "https://localhost:8080/s/-/u/a?type=like&id=https://localhost:8008/u/a/s/1"
$ curl -X POST "https://localhost:8080/s/-/u/a?type=undo_like&id=https://localhost:8008/u/a/s/1"

こうすればidで指定したNoteなどにいいねをつけることができます。
PSH RCの頃無理やり実装した機能ですが、以前より簡単にできるようになりました。
Mastodon、Pleroma、MisskeyそれぞれNoteのURLは異なりますが、全て問題なく機能します。

Accept Follow実装

Mastodon、Pleroma、Misskeyからフォローすることができます。
通常ですとフォローしたユーザーからNoteなどをPullするような動作を返すべきですが、PushしかしないActivityPub実装なのでフォローされても何もしません。
フォローするメリット皆無ですが一応反射的にPushできることを確認するため実装しています。

ちなみに以前Accept Followの仕組みを応用してVercel専用で特定のユーザーのCreate Noteを受信したら反射していいねしてくれる反射型ActivityPub実装を作ったりしました。

https://gitlab.com/tkithrta/likestream

こういった自由度あるのがActivityPubのいいところですね。

PSH Roadmap

現在ののPSHの機能で必要十分と思うか、他のActivityPub実装と比べ物にならないほど機能不足と思うかは人それぞれですが、一応今後追加する予定の機能を紹介したいと思います。

config.json実装

これを実現するためにはファイルの読み込み権限を新たに考慮する必要があるのですが割と簡単なので早めにやりたいです。

htmlテンプレート対応

Jinja2でnoscriptなページを提供したいです。

Heroku対応

現時点でもProcfileを提供しているので多分問題なく動くと思いますがDokkuとHerokuish、Cloud Native Buildpacksあたりも対応できるようにします。

Docker対応

GitLabではAuto Buildを有効にすればDockerfileがなくても自動的にDockerコンテナイメージを作成してくれますが、distroless.dockerfileファイルを提供してDistrolessコンテナイメージも提供したいです。

curlでJSONをPOST

クエリを投げることしかできないのと、URLエンコーディングやってもURLには長さに限界があるのでJSONをPOSTして解決したいです。

BasicまたはDigest認証機能

Digest認証はできる気がしないのですがBasic認証は頑張れば自力で書けると思うので認証機能をちゃんとつけておきたいです。

OpenSSHとOpenSSLをCryptographyに変更

$ python secret.pyあたりでサクッと鍵生成できるようにしたいです。

複数アカウント実装

for文とif文でちゃんと書く必要があるのですが今の実装との兼ね合いもあるので結構難しかったりします。

Create Post実装

実はRC版のPSHでもCreate Post機能は実装したことあるのですが、どうやらidの指定がおかしかったらしく、そこら辺ちゃんと再設計して実装したいです。

Verify機能

Create Post機能実装するあたりで脆弱性と向き合う必要が出てくるのでまずはinboxに入ってくるSignヘッダーをちゃんと検証できるようにしたいです。

DB実装(SQLite, Redis, DBaaS)

書き込み関係は可能な限りしない予定ですがそれでも各種データを保存できないまま運用するのは辛いのである程度は保存できるようにします。
SQLiteはPythonに標準で入っておりRaspberry Pi OSでも動くことが確認できているのですがHerokuやVercelでは使えないので別のDBも使えるようにする必要があります。
PostgreSQLを使うことも検討しましたがRaspberry Pi OSで動くかどうか怪しかったので、すでに動くことが判明しているRedisを使うことになると思います。

またRedisを使ったDBaaSとしてUpstashではデータ永続化をサポートしているのでこちらにも対応できるのではないでしょうか。

一応多分実装しないと思うけどやりたいことも書いておきます。

  • バイナリ保存機能
  • RSS機能
  • Bearer認証機能(OAuth2, ActivityPub C2S)
  • Announce機能
  • 型ヒントでジェネリクス

おわりに

最後になればなるほど雑になっていってるのはAdvent Calendarに間に合っていないからなんですね。
最後までお読みいただきありがとうございました。

Discussion

ログインするとコメントできます