crystal-p2p-server

crystal-p2p-server

ゲーム固有の状態を解釈しない、WebRTCシグナリング用のCrystalサーバです。

前提

  • Crystal 1.21以降
  • Shards
  • curl

ルームストアはデフォルトでオンメモリです。Redisを使わない場合はRedisサーバを起動する必要はありません。

ビルド

リポジトリのルートから実行します。

cd crystal-p2p-server
shards install
shards build

ビルドが成功すると、bin/p2p-server が作成されます。

設定

設定ファイルを使う場合は、サンプルをコピーします。

cp config.example.yml config.yml

config.yml が存在しない場合も、組み込みのデフォルト値で起動できます。設定ファイルのパスは P2P_CONFIG で変更できます。

主な設定項目は次のとおりです。

host: 0.0.0.0
port: 8080
room_store: memory # memory または redis
default_room_capacity: 2
max_room_capacity: 8
room_ttl_seconds: 900

redis:
  url: redis://127.0.0.1:6379/0
  namespace: p2p

Redisを使う場合は、room_storeをredisに変更し、Redisサーバを起動してください。

起動

bin/p2p-server

設定ファイルを指定する場合:

P2P_CONFIG=config.yml bin/p2p-server

デフォルトでは 0.0.0.0:8080 で待ち受けます。終了するには Ctrl-C を押します。

curlによる動作確認

サーバを起動した状態で、別のターミナルから実行します。

ヘルスチェック

curl -i http://127.0.0.1:8080/healthz

次のようなレスポンスが返れば成功です。

HTTP/1.1 200 OK
...

{"status":"ok"}

ルーム作成

max_playersはホストを含む最大参加人数です。次の例では、最大4人のルームを作成します。

curl -i \
  -X POST \
  -H 'Content-Type: application/json' \
  -d '{"max_players":4}' \
  http://127.0.0.1:8080/rooms

201 Createdと、次の項目を含むJSONが返れば成功です。

{
  "room_id": "...",
  "host_token": "...",
  "join_token": "...",
  "join_url": "http://localhost:8080/join?room=...&token=...",
  "max_players": 4,
  "expires_at": 0
}

デフォルト人数でルーム作成

max_playersを省略すると、default_room_capacityが使われます。

curl -i \
  -X POST \
  -H 'Content-Type: application/json' \
  -d '{}' \
  http://127.0.0.1:8080/rooms

デフォルト設定では max_players が 2 になります。

入力値のエラー確認

最小人数は2人です。次のリクエストは 422 Unprocessable Entity になります。

curl -i \
  -X POST \
  -H 'Content-Type: application/json' \
  -d '{"max_players":1}' \
  http://127.0.0.1:8080/rooms

max_room_capacityを超えた人数を指定した場合も、422 Unprocessable Entityになります。

WebSocket接続

ルーム作成レスポンスの room_id と host_token または join_tokenを使って、次のURLへ接続します。

ws://localhost:8080/ws?room=<room_id>&token=<host_token_or_join_token>

curlではHTTP APIの確認まで行えます。WebSocketのOffer/Answer/ICE候補の中継確認には、WebSocket対応クライアントを使用してください。

複数ゲストのルームでは、peer_joinedイベントのpeer_idを使い、シグナリングメッセージに対象のpeer_idをtoとして指定します。

パスキー認証

このサーバは、パスキー(WebAuthn)の認証サーバも兼ねます。ログインすると ES256 で署名したアクセストークン(JWT)を発行し、そのトークンで P2P サーバの機能を使えます。他のシステムは、公開鍵(JWKS)を使ってトークンを自分で検証できます。

設定はすべて config.yml の auth セクションにあります(項目の説明は config.example.yml を参照してください)。主な項目は次のとおりです。

  • rp_id: パスキーを結びつけるドメインです。登録後は変更できません。本番では親ドメイン(例: umr.biz)にすると、すべてのサブドメインで同じパスキーを使えます。
  • allowed_origins: パスキー操作を行うページのオリジンです。rp_id 自身か、そのサブドメインである必要があります。
  • issuer: トークンの iss クレームです。
  • audiences / default_audiences / p2p_audience: トークンを発行できる対象、指定がない場合に発行する対象、P2P サーバが要求する対象です。
  • passkey_user_name: パスキーと一緒に認証器へ保存され、パスキーの選択画面に表示される名前(WebAuthn の user.name と user.displayName)です。空ならユーザー名と表示名を使います。固定の文字列(例: umr.biz)にすると、パスワードマネージャーにユーザー名も表示名も残りません。64 バイトまでです。
  • cors_allowed_origins(トップレベル): ブラウザから直接このサーバを呼べるオリジンです。"*" を入れると、すべてのオリジンを許可します。トークンは Cookie ではなく Authorization ヘッダーで送るので、他のサイトのページが利用者のログイン状態を使うことはできません。
  • user_store: memory(再起動で消えます)または redis です。
  • signing_key_path: 署名鍵(EC P-256、PEM)の場所です。generate_signing_key: true のとき、ファイルがなければ初回起動時に生成します。
  • access_token_ttl_seconds: アクセストークンの有効期限です(デフォルトは 1 週間)。
  • token_binding: トークンを、ログインした端末のリクエストヘッダー(デフォルトは User-Agent)に紐づけます。値は bnd クレームに入り、完全一致で照合します。
  • sealing: 個人情報を暗号化する鍵(AES-256-GCM)の設定です。鍵は初回起動時に生成でき、入れ替え後の古い鍵は previous_key_paths に置きます。

API

base_path(デフォルトは /auth)以下に公開されます。

メソッド パス 内容
POST /auth/register/options 登録用のオプションを返します。{"username", "display_name"}。Bearer トークンを付けると、そのユーザーにパスキーを追加します
POST /auth/register/verify 登録を検証し、トークンを発行します。{"challenge_id", "credential", "audience"}
POST /auth/login/options 認証用のオプションを返します。username を省略すると、ユーザー名なしのログインになります
POST /auth/login/verify 認証を検証し、トークンを発行します。{"challenge_id", "credential", "audience"}
POST /auth/logout Bearer トークンを失効させます。{"all": true} なら、そのユーザーの全トークンを失効させます
GET /auth/me Bearer トークンのユーザー情報を返します
PATCH /auth/me ニックネームを変更し、新しいトークンを返します。{"display_name"}。前後の空白を除いて NFC に正規化し、制御文字は受け付けません
POST /auth/me/delete/options 退会の直前に行うパスキー認証のオプションを返します。本人のパスキーだけを受け付けます
DELETE /auth/me 共通アカウントを削除します(退会)。{"challenge_id", "credential"} で、その場のパスキー認証が必要です。ユーザー名はすぐ再利用できるようになり、AS には通知しません
GET /auth/jwks.json トークン検証用の公開鍵(JWKS)を返します
GET /auth/configuration issuer、JWKS の場所、端末との紐づけの手順を返します
POST /auth/seal Bearer トークンのユーザーの個人情報を暗号化します。{"data"}
POST /auth/unseal 暗号文を復号して、呼び出し元に返します。{"sealed"}。復号できなければ 422 unsealable を返します

/auth/me、/auth/seal、/auth/unseal、/rooms、WebSocket では、トークンの署名・期限に加えて、端末との紐づけ(リクエストの HTTP ヘッダー)と失効も確認します。

/auth/seal と /auth/unseal が受け取るのは、アクセストークン、HTTP ヘッダー、平文または暗号文だけです。ブラウザから直接呼んでも、AS 経由で呼んでも同じで、結果は呼び出し元に返します。暗号文はユーザーに紐づくので、他人の暗号文は復号できません。

credential は、navigator.credentials.create() / get() の結果を base64url の JSON にしたものです(client/views/index.erb に変換例があります)。

P2P サーバでの利用

require_for_rooms / require_for_websocket が true の場合、p2p_audience を含むトークンが必要です。

  • POST /rooms: Authorization: Bearer <access_token> を付けます。
  • WebSocket: ブラウザはヘッダーを付けられないため、サブプロトコルとして渡します。
new WebSocket(`ws://localhost:8080/ws?room=${roomId}&token=${hostToken}`, ["p2p.bearer", accessToken]);

テスト

crystal spec

src/passkey_kit/ は、WebAuthn・CBOR・COSE・JWT・JWKS・AES-256-GCM を扱う自前のライブラリです(暗号の計算は OpenSSL を呼びます)。アプリケーション側のコードに依存しないので、将来 shard として切り出せます。

AS(認証サーバを利用するアプリケーションサーバ)

AS は、毎回のリクエストで認証サーバに問い合わせる必要はありません。

  • トークンの検証: AS の中で完結します。JWKS と /auth/configuration を取得してキャッシュし、署名・iss・aud・exp と、端末との紐づけ(bnd)を確認します。失効は認証サーバでしか分からないため、AS の中だけでは有効期限まで有効として扱われます。
  • 個人情報: AS は利用者から平文を受け取り、/auth/seal で暗号化した文字列を利用者に返します。利用者はそれをローカルストレージに保存します。表示するときは AS が /auth/unseal で復号します。AS も認証サーバも個人情報を保存しません。
  • AS が /auth/seal と /auth/unseal を呼ぶときは、利用者のアクセストークン(Authorization: Bearer)と、紐づけに使うヘッダー(User-Agent など)を、利用者から受け取った値のまま HTTP ヘッダーに付けます。

Ruby 用のヘルパー client/lib/auth_client.rb を使うと、これらを request を渡すだけで呼べます。

auth = AuthClient.new(server_url: "https://an.umr.biz", base_path: "/auth", issuer: "https://an.umr.biz", audience: "umr")
claims = auth.authenticate(request)   # 認証サーバへの通信なし。無効なら AuthClient::InvalidToken
sealed = auth.seal(request, { "email" => "user@example.com" })
data   = auth.unseal(request, sealed) # 復号できなければ nil

デモクライアントと E2E テスト

client/ は、認証サーバを利用する側(umr.biz を想定)の Ruby 製デモです。/auth/* を認証サーバへ中継し(本番では nginx の役割)、/api/* ではトークンを AS の中で検証します。設定は client/config.yml にあります(ない場合は client/config.example.yml を使います)。

cd client
bundle install
bundle exec ruby app.rb

ブラウザで http://localhost:4567/ を開くと、実際のパスキーで登録・ログイン・ログアウト・ルーム作成・個人情報の暗号化と復号を試せます。

サーバとデモクライアントを起動した状態で、ソフトウェア認証器を使った E2E テストを実行できます。

cd client
bundle exec ruby test/e2e_test.rb
Repository

crystal-p2p-server

Owner
Statistic
  • 0
  • 0
  • 0
  • 0
  • 0
  • about 3 hours ago
  • September 14, 2026
License

Links
Synced at

Fri, 23 Oct 2026 22:35:07 GMT

Languages