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
crystal-p2p-server
- 0
- 0
- 0
- 0
- 0
- about 3 hours ago
- September 14, 2026
Fri, 23 Oct 2026 22:35:07 GMT