配信サーバがいない静的サイトを、スマホだけで表示するビューワーを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 に置いてあります。気になった方はぜひ。

Write a comment
No comments yet.