# Hold'em Night 友達専用のテキサスホールデム(リングゲーム)サイト。Node.js + Express + Socket.IO。 「鍵入力 → ロビー → テーブル作成 → プレイ」まで動く。 ## ルール・仕様 - ノーリミット・テキサスホールデムのリングゲーム(最大9人)。ホストが START を押すと、以降は2人以上いる限り自動でハンドが続く。 - ブラインドはハンド開始時のレベルで固定。ヘッズアップはボタンが SB。 - 最低レイズ額は直前のレイズ幅以上。足りないオールインのレイズは、すでにアクションした人に再レイズ権を与えない。 - サイドポット・山分け(端数はボタンの左から)・誰もコールしなかった分の返金に対応。 - 全員オールインになったら手札を公開して残りのカードを順にめくる。ショーダウンでは残った全員の手札を公開。 - 持ち時間は1手30秒(`server/config.js` の `ACTION_TIME_MS`)。過ぎるとチェック(できなければフォールド)して「離席中」になり、「復帰する」を押すまで配られない。 - チップがなくなったら開始スタックでリバイできる。画面下に収支(現在のチップ − バイイン合計)を表示。 - ハンド中に退席するとフォールド扱い(出したチップはポットに残る)。 - ホストの一時停止中は、持ち時間・ブラインドタイマー・次のハンドの開始がすべて止まる。 ## ローカルで動かす ```bash cp .env.example .env # ACCESS_KEY を設定 npm install npm start ``` http://localhost:4671 を開く。 ## 設定(.env) 秘密情報はソースコードに書かず、プロジェクト直下の `.env`(git 管理外)か環境変数で渡す。`ACCESS_KEY` が無いとサーバは起動しない。 | 変数 | 既定値 | 内容 | | --- | --- | --- | | `ACCESS_KEY` | なし(必須) | 6桁の参加キー | | `PORT` | `4671` | 待ち受けポート | | `HOST` | `127.0.0.1` | 待ち受けアドレス。既定では外部から直接つながらない(nginx 経由で公開する) | | `PUBLIC_ORIGIN` | なし | 本番の公開URL `https://poker.kanawo.net`。設定するとこのドメイン以外(IP直打ちや別ドメイン)へのアクセスと、他オリジンからの操作を拒否する。ローカル開発では設定しない | | `SESSION_SECRET` | 自動生成 | Cookie 署名用。未指定なら `data/session-secret` に生成・保存 | ## セキュリティ - **参加キー**: 正しいキーを入れたブラウザに HttpOnly / SameSite=Lax(HTTPS 時は Secure)の Cookie を保存。アクセスのたびに有効期限(400日)が延長されるので、以降は入力不要。 - **総当たり対策**: 同じIP(IPv6 は /64 単位)から30分に5回間違えるとロック。サーバ全体で1時間に30回間違えると新規の鍵入力を一時停止(入室済みの人は影響なし)。失敗はログに出る(`journalctl -u holdem`)。 - **入室状態の取り消し**: キーを変えるか、`data/session-secret` を削除して再起動すると、全員の Cookie が無効になる。 - **CSRF / Cross-Site WebSocket**: 状態を変える API と WebSocket 接続は Origin が自サイトのときだけ受け付ける。Socket.IO は WebSocket のみ。 - **XSS**: 厳格な CSP(インラインスクリプト・インラインスタイル禁止)、表示時のエスケープ、名前から制御文字・不可視文字を除去。 - **その他**: クリックジャッキング防止(`frame-ancestors 'none'`)、`nosniff`、`no-referrer`、HTML/API は `no-store`、検索エンジン除外、エラー時に内部情報を返さない、リクエストサイズ・イベント連打・テーブル数の上限。 - 他人の席を操作できないよう、席の持ち主を示すプレイヤーIDはブラウザ内だけに保存し、他のプレイヤーには送らない。 - **カード**: 山札は暗号論的乱数(`crypto.randomInt`)でシャッフルしてサーバ内だけに保持。各プレイヤーには自分の手札だけを送り、他人の手札は公開時(オールイン・ショーダウン)まで送らない。アクションはサーバ側で手番・額をすべて検証する。 6桁のキーはそれ自体が強い秘密ではないので、上記の回数制限が守りの要になっている。キーは友達にだけ直接伝えること。 ## 構成 | パス | 内容 | | --- | --- | | `server/index.js` | HTTP ルーティング・セキュリティヘッダ・REST API・Socket.IO | | `server/auth.js` | 参加キー認証(Cookie)・回数制限・Origin チェック | | `server/tables.js` | テーブル状態(席・ボタン・ブラインド・タイマー・ハンドの開始/進行) | | `server/hand.js` | 1ハンドの進行(配札・ベッティング・サイドポット・ショーダウン) | | `server/cards.js` | 山札と役判定 | | `test/` | ゲーム処理のテスト(`npm test`) | | `server/config.js` | `.env` の読み込みと各種上限値 | | `public/assets/blinds.js` | ブラインド計算(サーバ・ブラウザ共用) | | `public/*.html`, `public/assets/*.js` | 各画面 | - テーブルはメモリ上にのみ保持(サーバ再起動で消える)。誰も接続していない状態が30分続くと自動削除。 - 名前とプレイヤーID はブラウザの localStorage に保存。リロードしても同じ席に戻れる。 - 作成者がホスト(START・一時停止ができる)。ホストが退席すると次の着席者に移る。 ## VPS へのデプロイ例(Ubuntu + nginx + systemd + HTTPS) 公開URLは https://poker.kanawo.net 。HTTPS なしで公開するとキーと Cookie が平文で流れるので、必ず nginx + Let's Encrypt 経由で公開する。 事前に DNS で `poker.kanawo.net` の A レコード(IPv6 があれば AAAA も)を VPS の IP に向けておく。 ```bash # 専用ユーザーで動かす sudo useradd --system --home /opt/holdem --shell /usr/sbin/nologin holdem sudo git clone <このリポジトリ> /opt/holdem cd /opt/holdem sudo npm ci --omit=dev # 秘密情報(本人のみ読める) sudo cp .env.example .env sudo nano .env # ACCESS_KEY=197107, PUBLIC_ORIGIN=https://poker.kanawo.net を設定 sudo mkdir -p data sudo chown -R holdem:holdem .env data sudo chmod 600 .env && sudo chmod 700 data ``` `/etc/systemd/system/holdem.service`: ```ini [Unit] Description=Hold'em Night After=network.target [Service] User=holdem WorkingDirectory=/opt/holdem ExecStart=/usr/bin/node server/index.js Environment=NODE_ENV=production Restart=always # サンドボックス NoNewPrivileges=true ProtectSystem=strict ProtectHome=true PrivateTmp=true PrivateDevices=true ReadWritePaths=/opt/holdem/data CapabilityBoundingSet= RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX [Install] WantedBy=multi-user.target ``` nginx(`/etc/nginx/sites-available/holdem`、WebSocket を通す): ```nginx # poker.kanawo.net 以外のホスト名(IP直打ち等)は接続ごと切る server { listen 80 default_server; listen [::]:80 default_server; server_name _; return 444; } server { server_name poker.kanawo.net; client_max_body_size 8k; location / { proxy_pass http://127.0.0.1:4671; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $http_host; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 1h; } } ``` ```bash sudo ln -s /etc/nginx/sites-available/holdem /etc/nginx/sites-enabled/ sudo certbot --nginx -d poker.kanawo.net # HTTPS 化(HTTP→HTTPS リダイレクトも設定される) sudo systemctl daemon-reload && sudo systemctl enable --now holdem # ファイアウォール: 4671 は開けない(nginx だけが 127.0.0.1 経由で使う) sudo ufw allow OpenSSH && sudo ufw allow 'Nginx Full' && sudo ufw enable ``` ## 更新(デプロイ済みのサーバに反映) テーブルはメモリ上にしかないので、再起動すると進行中のテーブルは消える。誰も遊んでいないときに行う。 ```bash cd /opt/holdem sudo git pull sudo npm ci --omit=dev sudo systemctl restart holdem ```