0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【第042回】振り出し回数カウンターで幽霊部屋を排除!ルームIDの世代管理とステートレス識別

0
Posted at

こんにちは!2ちゃんねる互換掲示板システム「 つゔぁいちゃんねる(2ch.biz) 」の開発録、第42回です。

前回(第41回)では、シグナリングサーバーの無条件自動生成を止め、存在しない部屋へのアクセスをエラーとして遮断する仕組みを作りました。

しかし、このエラー処理を組み込んだ直後、開発チーム内で新たな議論が巻き起こりました。

「『部屋が見つかりません』とだけ言われると、 単にURLを打ち間違えたのか、それとも昨日まで盛り上がっていた部屋が全員退席して終了したのか が分からない!」
「でも、過去の部屋リストをデータベースに保存したら、私たちの誇る『ゼロストレージ(ログを一切残さない)』の設計思想に反してしまうのでは…?」

データベースに部屋の履歴テーブルを1つも作らず、プライバシーを完全に死守したまま、どうやって「タイポ(出鱈目なID)」と「終了した部屋」を完璧に見分けるのか?

本稿では、 「たった1つの整数カウンター」 だけで過去・現在・未来の部屋を瞬時に見分ける、極めてエレガントなステートレス識別アルゴリズムの全貌を解説します!


1. 履歴を持たないサーバーのジレンマ

ユーザーフレンドリーなサービスを目指すなら、エラーメッセージは親切であるべきです。

  • URLを打ち間違えた人には : 「そんなIDの部屋は一度も存在しません。入力内容を確認してください」
  • 昨日解散した部屋のリンクを踏んだ人には : 「このチャットルームは全員が退席したため、正常に終了しました」

通常なら、データベースに rooms_history テーブルを作り、発行したルームIDと終了日時を保存しておけば簡単に判定できます。

-- 【通常のWebサービスのアプローチ】
SELECT status FROM rooms_history WHERE room_id = 'xxx';
-- → 過去に作られていれば 'closed'、1件もなければ 'not_found'

しかし、つゔぁいちゃんねるのP2Pチャットの絶対原則は 「サーバーに過去ログや部屋の痕跡を一切残さないこと」 です。
誰がいつどんな部屋を開設したかというメタデータすら、DBに蓄積したくありませんでした。

「DBに過去の部屋を1件も保存しない」かつ「出鱈目なIDと終了したIDを高精度に見分ける」――この相反する要求をどう両立させたのでしょうか?


2. 解決策:単一整数カウンターによる「状態空間の3分割」

私たちが考案したのは、サーバー側で保持する 「これまでに発行した部屋の総数(p2p_room_counter)」という、たった1つの整数値 を利用した判定アルゴリズムです。

ルームIDのフォーマットを、以下のように 「連番カウンター + 暗号学的乱数」 でハイブリッド構成にします。

$$\text{ルームID形式}: \quad \texttt{room_} + \langle\text{発行番号 } N\rangle + \texttt{_} + \langle\text{ランダム文字列}\rangle$$
(例: room_1_a8b9c0d1, room_42_k3m9p2x1, room_105_7f2a1b9c)

この設計により、入力されたルームIDから正規表現で番号 $N$ を取り出すだけで、サーバーは一切の履歴テーブルを見ることなく、以下の 3つの状態 を瞬時に判別できます。

3つの判定パターン

  1. 未来の番号($N > \text{Max}$ または形式不正) :
    • サーバーがまだ100回しか部屋を発行していないのに、room_9999_xyz や hoge123 が送られてきたら、それは 「存在し得ない出鱈目なID」 です。
    • ➔ エラー: INVALID_ROOM_ID(「指定されたルームIDは存在しません」)
  2. 過去の番号($1 \le N \le \text{Max}$ かつメモリ上に非存在) :
    • 過去に確実にサーバーが発行した番号だが、現在のメモリマップ rooms に存在しないということは、 「全員が退席して綺麗サッパリ消滅した過去の部屋」 であることが論理的に確定します。
    • ➔ エラー: ROOM_CLOSED(「全員が退席したため終了しました」)
  3. 現在の番号(メモリ上に実在) :
    • 誰かが滞在している現役の部屋です。
    • ➔ 正常入室(パスワード認証や承認フローへ進む)

3. 実装の全貌

(1) ルームIDの発行とカウンターの永続化

サーバー側では、部屋が新しく作成されるたびにアトミックにカウンターをインクリメントし、SQLiteの管理設定(admin_settings)に単一の数値として保存します。

// server.js: 部屋作成ロジック
function generateNextRoomId() {
    // 1. カウンターを +1 インクリメント(永続化)
    currentRoomCounter++;
    saveSettingToDB('p2p_room_counter', currentRoomCounter);

    // 2. 8文字の暗号学的ランダム文字列を生成(総当たり防止)
    const randomSuffix = crypto.randomBytes(4).toString('hex'); // 8文字のhex

    // 3. room_<カウンター>_<乱数> を組み立て
    return `room_${currentRoomCounter}_${randomSuffix}`;
}

(2) 判定ロジックの実装

// 参加リクエスト受信時
if (action === 'join') {
    const { roomId, isCreate } = payload;

    if (!isCreate) {
        // 正規表現でカウンター番号 N をパース
        const match = String(roomId).match(/^room_(\d+)_[a-z0-9]+$/);
        
        if (!match) {
            // 形式自体が不正な場合
            return res.json({
                success: false,
                error: 'INVALID_ROOM_ID',
                message: '指定されたルームIDは存在しません。IDが正しいかご確認ください。'
            });
        }

        const roomNum = parseInt(match[1], 10);

        // パターンA: まだ発行されていない未来の番号
        if (roomNum <= 0 || roomNum > currentRoomCounter) {
            return res.json({
                success: false,
                error: 'INVALID_ROOM_ID',
                message: '指定されたルームIDは存在しません。IDが正しいかご確認ください。'
            });
        }

        // パターンB & C: 発行済み番号の存在チェック
        const activeRoom = rooms.get(roomId);
        if (!activeRoom) {
            // 過去に発行されたが、現在はメモリ上にない=全員退席して終了した部屋
            return res.json({
                success: false,
                error: 'ROOM_CLOSED',
                message: 'このチャットルームは全員が退席したため終了しました。'
            });
        }

        // パターンC: 現在アクティブな部屋(入室処理へ)
    }
}

4. 自動テストによる検証結果

テストスクリプトを実行し、状態空間の判定精度を検証しました。

テスト入力 サーバー最新番号: 10 期待される判定 実行結果
hoge_invalid - INVALID_ROOM_ID(形式不正) ✅ PASS
room_999_abcd1234 未来の番号(999 > 10) INVALID_ROOM_ID(未発行) ✅ PASS
room_5_abcd1234 過去の番号(現在0人) ROOM_CLOSED(終了済み) ✅ PASS
room_10_abcd1234 現在稼働中(参加者あり) 入室成功(パスワード確認へ) ✅ PASS

データベースには「現在の最大発行番号」というたった1行の整数しか保存されていないにもかかわらず、ユーザーには「タイポなのか」「解散したのか」が完璧に区別されて親切に伝わるようになりました。


5. まとめと次回予告

履歴をDBに溜め込まず、整数カウンター1つで状態空間を過去・現在・未来に切り分けるステートレス識別。
プライバシーの死守と親切なUXという、本来相反する2つのゴールを両立させた美しいエンジニアリングの成果です。

しかし、この仕組みを導入した直後、セキュリティ担当から鋭い指摘が入りました。

「ちょっと待ってくれ。ルームIDに room_105_... のように連番カウンターがそのまま露出していると、 悪意ある攻撃者に『今このサイトで累計何個の部屋が作られたか』という利用規模が丸見えになってしまうのでは? 」
「さらに、直近の番号(104や103)を推測して総当たりアタックを仕掛けられるリスクがある!」

連番の便利さを維持しながら、外部からは完全にランダムな暗号文字列に見せるにはどうすればいいのか?

次回は、 【第043回】連番推測による部屋への不正侵入を防げ!難読化カウンター埋め込みによるルームID保護 をお届けします。お楽しみに!

本番サービス : https://2ch.biz/

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?