Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

itousouta15/itouOJ

Open more actions menu

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

本地開發

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 ( namespaces + cgroup v2 + seccomp-bpf ) for C/C++/Python/JavaScript judging.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages

Morty Proxy This is a proxified and sanitized view of the page, visit original site.