- Optional ADMIN_KEY (6 digits, distinct from ACCESS_KEY): entering it on the key screen signs in as admin (role-specific HMAC cookie; member cookies stay valid). Logged-in members can open /key?admin to enter it. - /range (range chart + color/category quiz, ported from poker-hand and restyled to the site design) and its files under admin/ are served only to admins; everyone else gets 404. Card images cached privately for a day. - Lobby shows a link to the page for admins only (GET /api/me). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
10 KiB
10 KiB
Hold'em Night
友達専用のテキサスホールデム(リングゲーム)サイト。Node.js + Express + Socket.IO。
「鍵入力 → ロビー → テーブル作成 → プレイ」まで動く。
ルール・仕様
カードは実物で配り、このサイトはチップの代わりに使う(ベット・ポット・スタックの管理)。
- ノーリミット・テキサスホールデムのリングゲーム(最大9人)。ホストが START を押すと、以降は2人以上いる限り自動でハンドが続く。
- 画面にディーラーボタン(D)・SB・BB と、いまのストリート(PREFLOP / FLOP / TURN / RIVER)が出るので、それに合わせて実物のカードを配る。
- ブラインドは自動で支払われ、ハンド開始時のレベルで固定。ヘッズアップはボタンが SB。
- 各自が自分のスマホで Fold / Check / Call / Bet / Raise を行う。最低レイズ額は直前のレイズ幅以上。足りないオールインのレイズは、すでにアクションした人に再レイズ権を与えない。
- ベッティングが終わると次のストリートへ。全員オールインならその時点でショーダウンになる(残りのカードは実物でめくる)。
- ショーダウン: カードを見せ合い、勝った人を画面で選んで確定する(選べるのはショーダウンに残った人とホスト)。サイドポットがあるときはポットごとに選ぶ。複数選ぶと山分け(端数はボタンの左から)。
- 全員フォールドしたら残った人が自動でポットを獲得。誰もコールしなかった分は自動で返る。
- 「離席」にすると次のハンドから配られない(手番が来たら自動でチェック/フォールド)。「復帰する」で戻る。
- チップがなくなったら開始スタックでリバイできる。画面下に収支(現在のチップ − バイイン合計)を表示。
- ハンド中に退席するとフォールド扱い(出したチップはポットに残る)。
- ホストの一時停止中は、アクション・勝者の確定・ブラインドタイマー・次のハンドの開始がすべて止まる。
ローカルで動かす
cp .env.example .env # ACCESS_KEY を設定
npm install
npm start
設定(.env)
秘密情報はソースコードに書かず、プロジェクト直下の .env(git 管理外)か環境変数で渡す。ACCESS_KEY が無いとサーバは起動しない。
| 変数 | 既定値 | 内容 |
|---|---|---|
ACCESS_KEY |
なし(必須) | 6桁の参加キー |
ADMIN_KEY |
なし(任意) | 6桁の管理者キー(ACCESS_KEY と別の値)。入力すると管理者として入室する |
PORT |
4671 |
待ち受けポート |
HOST |
127.0.0.1 |
待ち受けアドレス。既定では外部から直接つながらない(nginx 経由で公開する) |
PUBLIC_ORIGIN |
なし | 本番の公開URL https://poker.kanawo.net。設定するとこのドメイン以外(IP直打ちや別ドメイン)へのアクセスと、他オリジンからの操作を拒否する。ローカル開発では設定しない |
SESSION_SECRET |
自動生成 | Cookie 署名用。未指定なら data/session-secret に生成・保存 |
管理者(ハンドレンジ学習)
ADMIN_KEY を入力して入室すると管理者になり、ロビーに「♠ ハンドレンジ学習」が表示される(/range:レンジ表と色・分類クイズ)。管理者は通常のプレイもそのままできる。
- 初めて開くときは鍵入力画面で
ADMIN_KEYを入れる。すでに参加キーで入室済みのブラウザはhttps://poker.kanawo.net/key?adminを開いて管理者キーを入れる。 - 管理者以外には
/rangeとその画像・スクリプトは 404 を返す(ページがあること自体を見せない)。ファイルはpublic/の外のadmin/に置いている。 - 管理者キーを変えると、管理者の Cookie だけが無効になる。
セキュリティ
- 参加キー: 正しいキーを入れたブラウザに 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はブラウザ内だけに保存し、他のプレイヤーには送らない。
- チップ操作: アクションはサーバ側で手番・額をすべて検証する。ショーダウンの勝者を確定できるのは残った人とホストだけで、ポットの対象外の人は選べない。
6桁のキーはそれ自体が強い秘密ではないので、上記の回数制限が守りの要になっている。キーは友達にだけ直接伝えること。
構成
| パス | 内容 |
|---|---|
server/index.js |
HTTP ルーティング・セキュリティヘッダ・REST API・Socket.IO |
server/auth.js |
参加キー認証(Cookie)・回数制限・Origin チェック |
server/tables.js |
テーブル状態(席・ボタン・ブラインド・タイマー・ハンドの開始/進行) |
server/hand.js |
1ハンドのチップの動き(ブラインド・ベッティング・サイドポット・勝者への分配) |
test/ |
チップ処理のテスト(npm test) |
server/config.js |
.env の読み込みと各種上限値 |
public/assets/blinds.js |
ブラインド計算(サーバ・ブラウザ共用) |
public/*.html, public/assets/*.js |
各画面 |
admin/ |
管理者専用ページ(ハンドレンジ学習・カード画像) |
- テーブルはメモリ上にのみ保持(サーバ再起動で消える)。誰も接続していない状態が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 に向けておく。
# 専用ユーザーで動かす
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:
[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 を通す):
# 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;
}
}
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
更新(デプロイ済みのサーバに反映)
テーブルはメモリ上にしかないので、再起動すると進行中のテーブルは消える。誰も遊んでいないときに行う。
cd /opt/holdem
sudo git pull
sudo npm ci --omit=dev
sudo systemctl restart holdem