配信サーバがいない静的サイトを、スマホだけで表示するビューワーをNostrで作ってみた
- つくったもの
- まず「普通のウェブサイト」の仕組み
- カギになる考え方:場所ではなく「中身」で指定する
- 部品は2か所に分かれて置いてある
- それを全部、スマホの中で解決する
- ここまでの仕組み、実は Nostr です
- どうやって表示しているか(中身の話)
- で、なにが嬉しいの?
- 使ってみてね
この記事は9割くらいLLMが書いた温かみのない記事です。
つくったもの
Bouquet という Android アプリを作りました。
何をするアプリかというと、ひとことで言えば「そのサイトを配信する専用のサーバがいないのに、ウェブサイトが見られるビューワー」です。
普通、ウェブサイトを見るときは、どこかの会社のサーバにブラウザがアクセスして、そこから HTML や画像をもらってきます。example.com のサイトを配っているのは example.com のサーバ、という1対1の関係です。Bouquet にはこの「そのサイト専用の配信サーバ」がありません。
サーバが全くないわけではなく、サイトを構成するパーツは、後で説明するリレーや Blossom といった汎用のサーバにバラバラに置かれています。ただしそれらは「いろんな人のいろんなデータの置き場」であって、特定のサイトを配信するために存在しているわけではありません。バラバラの部品を集めて1つのサイトに組み立てるのは、サーバ側ではなくあなたのスマホです。
間に立つ「そのサイト専用のホスト」が一切いない、というのが今回のいちばんの肝です。
この記事では、まず「普通のウェブサイトの仕組み」をおさらいしてから、なぜそんなことができるのか、どうやって作ったのかを、説明していきます。
まず「普通のウェブサイト」の仕組み
https://example.com を開くと、ざっくりこういうことが起きています。
あなた ──「example.com ちょうだい」──▶ example.com のサーバ
あなた ◀──── HTML・CSS・画像 ──────── example.com のサーバ
ここで大事なのは、example.com というサーバが1台、全部を握っているという点です。
- そのサーバが落ちたらサイトは見られません。
- そのサーバの持ち主は、誰がいつ何を見たかを観測できます。
- ドメイン(
example.com)を止められたら、サイトは消えます。
普段は便利なんですが、見方を変えると「たった1つのホストにすべてを預けている」とも言えます。今回作ったものは、この「1つのホストにすべてを預ける」構造をなくす試みです。
カギになる考え方:場所ではなく「中身」で指定する
普通のウェブは「example.com の /index.html」という場所でファイルを指定します。場所で指定するから、その場所を持っているホストが必要になります。
Bouquet が乗っかっている仕組みでは、ファイルを中身で指定します。
ファイルの中身を計算にかけると「ハッシュ」という指紋のような文字列が出てきます。同じ中身なら必ず同じ指紋になり、中身が1文字でも変われば指紋もまったく変わります。つまり指紋さえ分かれば、それが「狙ったファイルそのものか」を、誰から受け取ったとしても自分で確かめられるということです。
これが効いてきます。中身で指定できるなら、ファイルは「信頼できる特定のサーバ」に置く必要がありません。どこから持ってきても、指紋が一致すれば本物だからです。
部品は2か所に分かれて置いてある
サイトのデータは、たった2種類の場所に分けて置かれています。登場人物はこの2つだけです。
1. リレー … 「どのファイルがどの指紋か」の対応表が置いてある
「リレー」は、短いメッセージを預かって配るだけのサーバです。Bouquet はここに、サイトの目次を置きます。目次といっても中身はシンプルで、
/index.html → 指紋(ハッシュ) abc123...
/style.css → 指紋(ハッシュ) def456...
/logo.png → 指紋(ハッシュ) 789xyz...
という「ファイルのパス → 指紋」の対応表です。ファイルの実体は入っていません。「このサイトは、こういう名前のファイルたちで出来ていて、それぞれの指紋はこれだよ」という地図だけです。
2. Blossom … 「ファイルの実体」が置いてある
「Blossom」は、ファイルの実体を指紋で出し入れできる倉庫です。「指紋 abc123... のファイルちょうだい」と言うと、その中身を返してくれます。
ここがポイントで、Blossom は中身を指紋で管理しているので、サイトの持ち主が誰かとか、どのサーバかとかは本質的に関係ありません。倉庫は何個あってもいいし、どの倉庫からもらっても、さっきの指紋で本物か確かめられます。
それを全部、スマホの中で解決する
ここまでをつなげると、Bouquet がやっていることはこうなります。
① リレーから目次(パス→指紋の対応表)をもらう
▼
② 目次に載っている指紋を使って、Blossom から実体をもらう
▼
③ もらった実体の指紋を、自分で計算して確かめる
▼
④ 全部そろったら、スマホの中だけでサイトを組み立てて表示する
①〜④を、全部あなたのスマホがやります。途中に「サイトを配信してくれるホスト」は登場しません。リレーは目次を渡すだけ、Blossom は実体を渡すだけで、どちらも「サイトを表示する」仕事はしていません。組み立ては最後にスマホの中で完結します。
普通のウェブだと、この「目次を読んで、ファイルを集めて、組み立てる」という頭を使う部分をサーバ側がやってくれていました。Bouquet はその頭脳をまるごとスマホ側に持ってきた、というわけです。
ここまでの仕組み、実は Nostr です
ここまで「リレー」という言葉を当たり前のように使ってきましたが、これは Nostr という仕組みのものです。
Nostr を知らない人向けにひとことだけ説明すると、Nostr は分散型 SNS のプロトコルです。「ドメインや会社のアカウントの代わりに、自分専用の鍵(公開鍵)で身元を表すネット」と言ってもいいです。普通のウェブの example.com にあたるものが、Nostr では人それぞれの鍵になります。そして、その鍵で署名されたメッセージを、誰でも立てられる「リレー」が預かって配ります。
そして Blossom は、その Nostr で使われるメディアサーバです。画像やファイルなどの実体を、これまで説明してきたとおりハッシュ(指紋)で出し入れする倉庫の役割を担います。この記事で「実体は Blossom に置く」と言ってきたのは、これのことです。
Nostr 自体についてもっと詳しく知りたい方は、mattn さんの記事 を読んでみてください。
Bouquet では、
- サイトの持ち主=1つの公開鍵(
npub1…で始まる文字列) - 目次=その鍵で署名された、リレー上のメッセージ
になっています。署名が付いているので、目次が本物の持ち主のものか(途中ですり替えられていないか)も確かめられます。実体のほうは前述の指紋で確かめられます。つまり入口から出口まで、ホストを信用しなくても自分で検証できるという構造です。
NIP-5A との関係
「静的サイトをどんな形でリレー / Blossom に置くか」というデータの形式は、NIP-5A という仕様で決められています。目次(パス→ハッシュの対応表)、ファイル実体の置き方といった部分です。Bouquet はこの形式をそのまま使っていて、独自のイベント種別やタグは一切足していません。置いてあるデータは NIP-5A 準拠です。
ただし、ここが大事なところなのですが、「そのデータをどうやって組み立ててサイトとして見せるか」については、NIP-5A は “ホストサーバを立てて解決する” やり方しか書いていません。 つまり仕様が想定している標準的な見せ方は、この記事の冒頭で「従来型」として挙げた、間に賢いホストが立つモデルのほうなのです。
この記事の肝である「中間のホストを置かず、目次の取得・実体の取得・ハッシュ検証・組み立てを全部スマホ側でやる」というやり方は、NIP-5A の本文には書かれていません。Bouquet が、仕様で定義済みのデータだけを使って、解決の部分を独自にクライアント側へ持ってきたものです。
なので正確に言うと、Bouquet は「NIP-5A のビューワー」というより、NIP-5A のデータをそのまま使いつつ、仕様が標準としているホスト経由の解決を、ホストなしのクライアント解決に置き換えてみた実装です(この方向を仕様にも追記しないか、という提案は別途しています)。
どうやって表示しているか(中身の話)
「途中にホストがないなら、WebView は何を読み込んでいるの?」という疑問が出てきます。答えは「スマホの中に一瞬だけ立てる、自分専用のローカルサーバ」です。
集めて検証し終わったファイルを、スマホの中の http://127.0.0.1(=自分自身)に対してだけ配るミニサーバを立て、そこを WebView に読ませています。127.0.0.1 は外には一切出ない、自分の端末の中だけのアドレスなので、ここでも外部のホストは登場しません。サイトを閉じればこのサーバも消えます。
で、なにが嬉しいの?
「サイト専用の配信サーバをなくす」と、具体的に何が変わるのか。大きく2つあります。どちらも リレーも Blossom も複数あってよく、目次は署名・実体はハッシュで検証できる という性質から出てきます。
1. 落ちにくい
普通のウェブだと、example.com のサーバが落ちればサイトは見られません。1台のサーバがそのまま単一障害点です。
Bouquet が見るサイトには、その「1台」がありません。目次は複数のリレーに、実体は複数の Blossom に置けます。あるリレーが落ちても別のリレーから同じ目次が取れるし、ある Blossom が落ちても、指紋(ハッシュ)が一致しさえすれば別の Blossom から取った実体でかまわないのです。「ここが死んだら終わり」という一点がないので、目次を持つリレーがどれか1つ生きていて、各ファイルの実体を持つ Blossom がどれか1つ生きてさえいれば、サイトは見られ続けます。
2. 検閲・改ざんに強い
専用の配信ホストがいるモデルでは、そのホストが要(かなめ)になります。ホストの持ち主に圧力をかける、ドメインを止める、サブドメインを落とす——どれか1つできれば、そのサイトを丸ごとオフラインにできます。途中にホストがいるということは、そこを押さえれば検閲できるということです。
Bouquet には押さえるべき一点がありません。複数のリレー/Blossom に散らばっていて、しかも誰でも新しく立てられます。どこか1つを止めても、利用者は別の経路から取りに行けます。
さらに、間に立つホストがいないので——
- すり替えられない: 目次は持ち主の署名付き、実体はハッシュで検証するので、途中の誰かが中身を差し替えても、あなたのスマホがその場で気づいて弾きます。
- 見られにくい: 「あなたがどのサイトのどのページを見たか」をまとめて観測できる中間ホストが存在しません。
使ってみてね
ソースとデモは GitHub に置いてあります。気になった方はぜひ。
- Reference: https://example.com`
- Reference: https://zenn.dev/mattn/articles/cf43423178d65c
- Reference: https://github.com/nostr-protocol/nips/blob/master/5A.md
- Reference: http://127.0.0.1`(=自分自身)に対してだけ配るミニサーバを立て、そこを
- Reference: https://github.com/kengirie/bouquet-android
Write a comment