Skip to content

Repository files navigation

itouOJ

image

自架的程式解題系統(OJ)。前後端用 Next.js 一體開發,評測引擎依語言分兩條路:C/C++/Python/JavaScript 走自架的 sandbox-runner(Linux namespaces + cgroup v2 + seccomp-bpf 從零刻的沙箱),Java 暫時繼續走 Piston

正式站:oj.itousouta.me

目錄

功能

  • 帳號註冊 / 登入(第一個註冊的使用者自動成為管理員),支援 Google / Discord 登入
  • 題目列表、Markdown + KaTeX 數學式題敘、範例測資
  • CodeMirror 程式碼編輯器(C++ / C / Python / Java / JavaScript),自動保存草稿
  • 即時判題:AC / WA / TLE / MLE / RE / CE,逐筆測資顯示時間與記憶體
  • 提交紀錄、排行榜、個人頁面
  • 課程(題單):一組題目 + 說明,使用者加入後追蹤解題進度,可設公開加入或加入代碼
  • 公告:置頂公告、Markdown 內容,管理員可新增 / 編輯 / 刪除
  • 管理後台:出題、測資編輯、時間 / 記憶體限制、公開 / 隱藏題目、課程與公告管理
  • 亮暗雙主題切換

技術架構

架構 技術
前端 + 後端 Next.js 16(App Router)+ TypeScript + Tailwind CSS v4
資料庫 SQLite + Prisma 7(better-sqlite3 driver adapter)
評測引擎 sandbox-runner(自架,C/C++/Python/JavaScript)+ Piston(Docker,Java)
判題佇列 in-process promise chain(src/lib/judge.ts),伺服器重啟自動恢復未完成的提交
                                  ┌──> sandbox-server (127.0.0.1:8090)
瀏覽器 ──> nginx ──> Next.js (:3000)─┤     C/C++/Python/JavaScript
                       │  SQLite   └──> Piston (127.0.0.1:2000, Docker)
                       │                 Java

src/lib/execute.ts 依語言分流,兩條路徑互相獨立(其中一邊掛掉不影響另一邊)。sandbox-runner 是這個專案自己刻的沙箱(namespace 隔離 + cgroup 資源限制 + seccomp syscall 白名單),細節、動機、架構圖見 sandbox-runner/README.md

支援語言(版本對應 src/lib/languages.ts,時間 / 記憶體倍率是相對題目原始限制的放寬倍數):

語言 版本 時間倍率 記憶體倍率 評測引擎
C++ GCC 10.2.0 1x 1x sandbox-runner
C GCC 10.2.0 1x 1x sandbox-runner
Python 3.12.0 3x 1x sandbox-runner
JavaScript Node 20.11.1 3x 2x sandbox-runner
Java 15.0.2 2x 2x Piston

專案結構

src/
├─ app/               # Next.js App Router 頁面與 API routes
│  ├─ admin/          # 管理後台(題目、課程、公告)
│  ├─ api/            # 後端 API(auth、courses、submissions、run...)
│  ├─ problems/       # 題目列表 / 詳情
│  ├─ courses/        # 課程列表 / 詳情
│  ├─ announcements/  # 公告列表 / 詳情
│  ├─ submissions/    # 提交紀錄
│  ├─ ranking/        # 排行榜
│  └─ users/          # 使用者頁面
├─ components/        # 共用 React 元件
├─ lib/               # 判題邏輯、語言設定、共用工具(judge.ts、languages.ts...)
└─ generated/prisma/  # `prisma generate` 產出的 client(不手動編輯)

prisma/
├─ schema.prisma      # 資料庫 schema
└─ migrations/        # migration 歷史

deploy/                # 部署腳本與設定(見「部署」章節)

sandbox-runner/         # 自架評測沙箱(C/C++/Python/JavaScript 用),詳見其 README

client/                 # Windows 收件程式與打包工具,詳見 `client/README.md`

本地開發

npm install                 # 會自動 prisma generate
npx prisma migrate dev      # 建立 SQLite 資料庫
npm run dev

.env 設定(參考):

DATABASE_URL="file:./dev.db"
AUTH_SECRET="<openssl rand -hex 32>"
PISTON_URL="http://localhost:2000"   # Piston 位址(Java 用)
SANDBOX_URL="http://localhost:8090"  # sandbox-runner 位址(C/C++/Python/JavaScript 用,不設也是這個預設值)
COOKIE_SECURE="0"                    # 上 HTTPS 後改 1

# Google 登入(選用;沒設定就不顯示 Google 按鈕)
GOOGLE_CLIENT_ID=""
GOOGLE_CLIENT_SECRET=""

# Discord 登入(選用;沒設定就不顯示 Discord 按鈕)
DISCORD_CLIENT_ID=""
DISCORD_CLIENT_SECRET=""

# APP_URL="https://oj.example.tw"    # 正式環境對外網址(組 OAuth redirect 用)

Google 登入設定

Google Cloud Console 建立「OAuth 用戶端 ID」(類型:網頁應用程式),授權重新導向 URI 填 http://localhost:3000/api/auth/google/callback。正式環境要再加一組 https://<你的網域>/api/auth/google/callback —— Google 不接受純 IP 或 http 的正式網址,所以正式站要先有網域 + HTTPS 才能開 Google 登入,並在 .env 設好 APP_URL。第一次用 Google 登入會自動建立帳號(沿用「第一個使用者是管理員」規則)。

Discord 登入設定

Discord Developer Portal 建立 Application,在 OAuth2 頁籤取得 Client ID / Client Secret,並在 Redirects 加上 http://localhost:3000/api/auth/discord/callback。正式環境要再加一組 https://<你的網域>/api/auth/discord/callback,並在 .env 設好 APP_URL。第一次用 Discord 登入一樣會自動建立帳號。

Piston 不在本機時,可用 SSH tunnel 接遠端的:ssh -N -L 2000:localhost:2000 user@server

部署

  1. 伺服器啟動 Piston(只綁 localhost,Piston 沒有認證機制):

    docker run --privileged -v /opt/piston-data:/piston --tmpfs /tmp:exec \
      -dit --restart=always -p 127.0.0.1:2000:2000 \
      -e PISTON_COMPILE_TIMEOUT=15000 -e PISTON_RUN_TIMEOUT=20000 \
      -e PISTON_OUTPUT_MAX_SIZE=33554432 \
      --memory=4g --memory-swap=4g --cpus=3 --pids-limit=1024 \
      --name piston_api ghcr.io/engineer-man/piston

    --memory / --cpus / --pids-limit 是限制容器對主機的總資源上限(跟評測本身的時間/記憶體限制是兩回事——單筆評測的限制是 judge.ts 依題目設定傳給 Piston,由內部 isolate/cgroup 逐筆強制執行,SIGKILL 後判 TLE/MLE)。沒有這層的話,Piston 容器預設可以吃光主機全部 CPU/RAM,isolate 出 bug 或題目限制設太大時會拖垮同一台主機上的 nginx / Next.js。數字要照主機規格調(範例是 4 核心 8GB 主機,留 1 核心給系統本身)。

    容器還在跑的話可以不重建直接套用:

    docker update --memory=4g --memory-swap=4g --cpus=3 --pids-limit=1024 piston_api

    原版 Piston 有三個問題會弄壞大測資(>100KB)甚至讓整個評測服務當掉,每次重建容器後都要重新打補丁docker restart 不會弄丟,docker rm + docker run 會):

    # 1) HTTP API body 上限預設 100KB,判題送不進大測資 → 調成 16MB
    
    docker exec piston_api sed -i \
      "s/body_parser.json()/body_parser.json({ limit: '16mb' })/; s/body_parser.urlencoded({ extended: true })/body_parser.urlencoded({ extended: true, limit: '16mb' })/" \
      /piston_api/src/index.js
    
    # 2) stdin 寫入後立刻 destroy(),緩衝區沒寫完就被丟掉,程式只收得到前 ~200KB → 拿掉那行
    
    docker exec piston_api sed -i '/proc.stdin.destroy();/d' /piston_api/src/job.js
    
    # 3) 使用者程式在 stdin 還沒寫完前就結束(提早 return / RE),父行程繼續寫入已關閉的 pipe
    #    會噴未捕捉的 EPIPE,整個 Piston process 直接崩潰(影響當下所有人的提交)→ 補一個空的 error handler
    
    docker exec piston_api sed -i \
      "s/proc.stdin.write(this.stdin);/proc.stdin.on('error', () => {}); proc.stdin.write(this.stdin);/" \
      /piston_api/src/job.js
    docker restart piston_api

    docker cp 到這個容器的 /tmp 常常悄悄失敗(/tmp 掛的是 tmpfs),要塞檔案進容器的話改用 /root 之類的一般目錄。

    Piston 本身的沙箱只管資源(CPU/記憶體/時間)跟檔案系統範圍,不管使用者程式碼能不能呼叫子程序。submission 裡直接 import subprocess / os.system,就能在容器裡跑任意指令。

    現在正式判題只有 Java 走 Piston;C/C++/Python/JavaScript 已經換成 sandbox-runner 的核心層級防護,不依賴這裡的補丁。不過 Piston 內建的 Python 套件在「新增語言」步驟時還是會被安裝出來,所以這份 sitecustomize.py 仍然要放進去。

    補丁檔在 deploy/piston-python-sitecustomize.py。把它裝到 Python 套件的 site-packages 後,會用 sys.addaudithook 在直譯器層級擋掉:

    • subprocess
    • os.system / os.popen / os.fork
    • ctypes
    • socket

    audit hook 裝上去後使用者程式碼無法移除,比字串黑名單擋 import 紮實:

    scp deploy/piston-python-sitecustomize.py root@<server>:/root/sitecustomize.py
    ssh root@<server> "docker cp /root/sitecustomize.py piston_api:/piston/packages/python/3.12.0/lib/python3.12/site-packages/sitecustomize.py"

    這份檔案放在 /piston/packages/...,跟語言套件一樣是掛在 /opt/piston-data volume 上,docker rm + docker run 重建容器也不會弄丟(不像上面 3 個補丁要重打);只有換 Python 版本或砍掉 /opt/piston-data 重裝套件時才需要重新放一次。

  2. 安裝語言(照 src/lib/languages.ts 的版本):

    curl -X POST http://localhost:2000/api/v2/packages -H 'Content-Type: application/json' \
      -d '{"language":"python","version":"3.12.0"}'
    # gcc 10.2.0 / java 15.0.2 / node 20.11.1 同理
  3. 啟動 sandbox-runner(C/C++/Python/JavaScript 的評測引擎,取代 Piston):

    apt install libseccomp-dev libmicrohttpd-dev libcjson-dev build-essential
    cd sandbox-runner && make
    cp deploy/sandbox-server.service /etc/systemd/system/
    systemctl daemon-reload && systemctl enable --now sandbox-server

    必須用 root 執行(建立 namespace/cgroup 需要的權限沒辦法給非特權使用者),只綁 127.0.0.1:8090。詳細架構、安全模型見 sandbox-runner/README.md。伺服器 .env 記得設 SANDBOX_URL="http://127.0.0.1:8090"(不設也會用這個預設值)。

  4. 部署本體:npm ci && npx prisma migrate deploy && npm run build,用 systemd 跑 next start(範例在 deploy/online-judge.service),前面掛 nginx 反向代理(deploy/nginx-oj.conf)。

  5. 網域與 HTTPS(正式站 https://oj.itousouta.me):DNS 加 A 記錄指到伺服器(Cloudflare 上選 DNS only),裝 certbot python3-certbot-nginx 後跑 certbot --nginx -d oj.itousouta.me --redirect(自動續簽由 certbot.timer 處理)。伺服器 .env 記得設 APP_URL="https://oj.itousouta.me"COOKIE_SECURE="1" 和 Google / Discord 憑證。

日常更新(改完程式碼後)

# 1. commit 修改(部署腳本打包的是已 commit 的內容,沒 commit 的改動不會上去)
git add -A
git commit -m "說明你改了什麼"

# 2. 一鍵部署:打包 → 上傳 → npm ci → migrate → build → 重啟服務
.\deploy\deploy.ps1

# 3. 同步到 GitHub
git push
  • 想先在本地看效果:npm run devhttp://localhost:3000
  • 評測功能要先接上伺服器:ssh -N -L 2000:localhost:2000 -L 8090:localhost:8090 root@<server>(2000 是 Piston、Java 用;8090 是 sandbox-runner,其他語言用)
  • 改了 prisma/schema.prisma 的話,先在本地跑 npx prisma migrate dev --name <名稱> 產生 migration 再 commit,部署腳本會自動在伺服器套用

新增語言

依要不要走 sandbox-runner 分兩種:

  • 走 Piston(目前只有 Java):Piston 裝套件(POST /api/v2/packages),在 src/lib/languages.ts 加一筆對應(檔名、版本、時間/記憶體倍率)。
  • 走 sandbox-runner:先在 sandbox-runner/src/seccomp.c 建立/擴充該語言的 seccomp 白名單(方法論見 sandbox-runner/README.md),sandbox-runner/src/server.cLANGS 表加一筆,再到 src/lib/languages.ts 加對應、src/lib/execute.tsSANDBOX_LANGUAGES 集合加進去。

About

Self-hosted online judge with a from-scratch Linux sandbox

Topics

Resources

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages