-
Notifications
You must be signed in to change notification settings - Fork 0
채점 기능 구현 #57
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
채점 기능 구현 #57
Changes from all commits
4764810
cfc6d35
2b84dcd
7ce815f
3f3a929
72e6003
efdcad0
4a08ead
423b696
8636864
b048d58
c938c50
75dde35
7b590e8
61cdecf
47c17be
bd0387e
2a88bdc
f30bff8
577c92c
c573429
75ba7b4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| FROM ubuntu:22.04 | ||
|
|
||
| RUN apt-get update | ||
| RUN apt-get install -y --no-install-recommends \ | ||
| gcc g++ \ | ||
| python3 pypy3 \ | ||
| openjdk-8-jdk-headless \ | ||
| git make pkg-config libcap-dev libsystemd-dev asciidoc \ | ||
| curl jq \ | ||
| && rm -rf /var/lib/apt/lists/* | ||
|
|
||
| # config isolate | ||
| RUN git clone https://github.com/ioi/isolate.git | ||
| RUN cd isolate && make install | ||
| RUN rm -rf isolate | ||
|
|
||
| # install testlib | ||
| RUN curl -L https://raw.githubusercontent.com/MikeMirzayanov/testlib/master/testlib.h \ | ||
| -o /usr/include/testlib.h | ||
|
Comment on lines
+18
to
+19
|
||
|
|
||
| # create judge user | ||
| RUN useradd -m judge | ||
| WORKDIR /home/judge | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| services: | ||
| grader: | ||
| build: | ||
| context: . | ||
| privileged: true | ||
w8385 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| container_name: coduck-grader | ||
| entrypoint: [ "sleep", "infinity" ] | ||
| restart: unless-stopped | ||
| volumes: | ||
| - ./uploads:/home/judge/uploads | ||
w8385 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| use tokio::process::Command; | ||
|
|
||
| use crate::errors::DockerError; | ||
|
|
||
| async fn ensure_exists_in_container(container: &str, path: &str) -> Result<(), DockerError> { | ||
| let status = Command::new("docker") | ||
| .args(["exec", container, "test", "-e", path]) | ||
| .status() | ||
| .await | ||
| .map_err(|_| DockerError::Spawn)?; | ||
|
|
||
| if !status.success() { | ||
| return Err(DockerError::FileNotFound(path.to_string())); | ||
| } | ||
|
|
||
| Ok(()) | ||
| } | ||
|
|
||
| async fn cp_container(src: &str, dst: &str) -> Result<(), DockerError> { | ||
| Command::new("docker") | ||
| .arg("cp") | ||
| .arg(src) | ||
| .arg(dst) | ||
| .output() | ||
| .await | ||
| .map_err(|_| DockerError::Spawn)?; | ||
|
|
||
| Ok(()) | ||
| } | ||
|
|
||
| pub async fn to_container( | ||
| container: &str, | ||
| local_src: &str, | ||
| container_dst: &str, | ||
| ) -> Result<(), DockerError> { | ||
| cp_container(local_src, &format!("{}:{}", container, container_dst)).await?; | ||
| ensure_exists_in_container(container, container_dst).await?; | ||
| Ok(()) | ||
| } | ||
|
|
||
| pub async fn from_container( | ||
| container: &str, | ||
| container_src: &str, | ||
| local_dst: &str, | ||
| ) -> Result<(), DockerError> { | ||
| ensure_exists_in_container(container, container_src).await?; | ||
| cp_container(&format!("{}:{}", container, container_src), local_dst).await?; | ||
| Ok(()) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| use crate::errors::DockerError; | ||
| use std::process::Stdio; | ||
| use tokio::process::Command; | ||
|
|
||
| pub struct CommandOutput { | ||
| pub status: std::process::ExitStatus, | ||
| pub stdout: String, | ||
| pub stderr: String, | ||
| } | ||
|
|
||
| impl CommandOutput { | ||
| pub fn ensure_success<E>(self, map_err: impl FnOnce(&CommandOutput) -> E) -> Result<Self, E> { | ||
| if self.status.success() { | ||
| Ok(self) | ||
| } else { | ||
| Err(map_err(&self)) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| pub async fn exec(container: &str, args: &[&str]) -> Result<CommandOutput, DockerError> { | ||
| let output = Command::new("docker") | ||
| .arg("exec") | ||
| .args(["-u", "judge"]) | ||
| .arg(container) | ||
| .args(args) | ||
| .stdout(Stdio::piped()) | ||
| .stderr(Stdio::piped()) | ||
| .output() | ||
| .await | ||
| .map_err(|_| DockerError::Spawn)?; | ||
| Ok(CommandOutput { | ||
| status: output.status, | ||
| stdout: String::from_utf8_lossy(&output.stdout).to_string(), | ||
| stderr: String::from_utf8_lossy(&output.stderr).to_string(), | ||
| }) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| pub mod cp; | ||
| pub mod exec; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| use serde::{Deserialize, Serialize}; | ||
| use std::fmt::{Display, Formatter, Result}; | ||
|
|
||
| #[derive(Debug, PartialEq, Serialize, Deserialize)] | ||
| pub enum DockerError { | ||
| UnsupportedExtension(String), | ||
| InvalidFilename, | ||
| Spawn, | ||
| FileNotFound(String), | ||
| } | ||
|
|
||
| impl Display for DockerError { | ||
| fn fmt(&self, f: &mut Formatter<'_>) -> Result { | ||
| match self { | ||
| DockerError::UnsupportedExtension(extension) => { | ||
| write!(f, "Unsupported file extension: {extension}") | ||
| } | ||
| DockerError::InvalidFilename => write!(f, "Invalid filename"), | ||
| DockerError::Spawn => write!(f, "Failed to spawn docker process"), | ||
| DockerError::FileNotFound(filename) => write!(f, "File not found: {filename}"), | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| use crate::errors::DockerError; | ||
| use serde::{Deserialize, Serialize}; | ||
|
|
||
| #[derive(Debug, PartialEq, Serialize, Deserialize)] | ||
| pub enum IsolateError { | ||
| Docker(DockerError), | ||
| UnsupportedExtension(String), | ||
| InvalidFilename, | ||
| InvalidBoxId(i32), | ||
| InitFailed(String), | ||
| CompileFailed(String), | ||
| CleanupFailed(String), | ||
| ExecuteFailed(String), | ||
| } | ||
|
|
||
| impl std::fmt::Display for IsolateError { | ||
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
| match self { | ||
| IsolateError::Docker(err) => write!(f, "Docker error: {}", err), | ||
| IsolateError::UnsupportedExtension(extension) => { | ||
| write!(f, "Unsupported file extension: {extension}") | ||
| } | ||
| IsolateError::InvalidFilename => write!(f, "Invalid filename"), | ||
| IsolateError::InitFailed(msg) => write!(f, "Isolate initialization failed: {msg}"), | ||
| IsolateError::InvalidBoxId(id) => write!(f, "Invalid box ID returned by isolate: {id}"), | ||
| IsolateError::CompileFailed(msg) => write!(f, "Isolate compile error: {msg}"), | ||
| IsolateError::ExecuteFailed(msg) => write!(f, "Isolate execution failed: {msg}"), | ||
| IsolateError::CleanupFailed(msg) => write!(f, "Isolate cleanup failed: {msg}"), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl From<DockerError> for IsolateError { | ||
| fn from(e: DockerError) -> Self { | ||
| IsolateError::Docker(e) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| use crate::errors::IsolateError; | ||
| use axum::response::{IntoResponse, Response}; | ||
| use axum::Json; | ||
| use reqwest::StatusCode; | ||
|
|
||
| #[derive(Debug)] | ||
| pub enum JudgeError { | ||
| Isolate(IsolateError), | ||
| NotFound(String), | ||
| } | ||
|
|
||
| impl std::fmt::Display for JudgeError { | ||
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
| match self { | ||
| JudgeError::Isolate(err) => write!(f, "Isolate error: {}", err), | ||
| JudgeError::NotFound(msg) => write!(f, "Not found: {}", msg), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl From<IsolateError> for JudgeError { | ||
| fn from(e: IsolateError) -> Self { | ||
| JudgeError::Isolate(e) | ||
| } | ||
| } | ||
|
|
||
| impl IntoResponse for JudgeError { | ||
| fn into_response(self) -> Response { | ||
| match self { | ||
| JudgeError::Isolate(e) => ( | ||
| StatusCode::INTERNAL_SERVER_ERROR, | ||
| Json(serde_json::json!({ "error" : e.to_string() })), | ||
| ), | ||
| JudgeError::NotFound(msg) => ( | ||
| StatusCode::NOT_FOUND, | ||
| Json(serde_json::json!({ "error" : msg })), | ||
| ), | ||
| } | ||
| .into_response() | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,9 @@ | ||
| mod docker; | ||
| mod isolate; | ||
| mod judge; | ||
| mod language; | ||
|
|
||
| pub(crate) use docker::*; | ||
| pub(crate) use isolate::*; | ||
| pub(crate) use judge::*; | ||
| pub(crate) use language::*; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| use crate::file_manager::Language; | ||
|
|
||
| #[derive(Debug)] | ||
| pub struct CompileConfig { | ||
| pub output_path: String, | ||
| pub argv: Vec<String>, | ||
| } | ||
|
|
||
| #[derive(Debug)] | ||
| pub struct ExecuteConfig { | ||
| pub argv: Vec<String>, | ||
| } | ||
|
|
||
| impl CompileConfig { | ||
| pub fn new(category: &str, filename: &str, language: Language) -> CompileConfig { | ||
| let source_path = format!("{}/{}", category, filename); | ||
| let output_path = format!("{}/{}", category, filename.split('.').next().unwrap()); | ||
|
||
|
|
||
| let python_temp = format!( | ||
| "\"import py_compile; py_compile.compile(r'{}')\"", | ||
| source_path | ||
| ); | ||
| let argv = match language { | ||
| Language::Cpp => vec![ | ||
| "--processes=4", | ||
| "--", | ||
| "/usr/bin/g++", | ||
| &source_path, | ||
| "-o", | ||
| &output_path, | ||
| "-O2", | ||
| "-Wall", | ||
| "-lm", | ||
| "-static", | ||
| "-std=gnu++17", | ||
| ], | ||
| Language::Python => { | ||
| vec!["--", "/usr/bin/pypy3", "-W", "ignore", "-c", &python_temp] | ||
| } | ||
| Language::Java => vec![ | ||
| "--processes=32", | ||
| "--", | ||
| "/usr/lib/jvm/java-8-openjdk-amd64/bin/javac", | ||
| "-J-Xms1024m", | ||
| "-J-Xmx1920m", | ||
| "-J-Xss512m", | ||
| "-encoding", | ||
| "UTF-8", | ||
| &source_path, | ||
| ], | ||
| _ => vec![], | ||
|
||
| } | ||
| .iter() | ||
| .map(|s| s.to_string()) | ||
| .collect(); | ||
|
|
||
| CompileConfig { output_path, argv } | ||
| } | ||
| } | ||
|
|
||
| impl ExecuteConfig { | ||
| pub fn new(category: &str, filename: &str, language: Language) -> Self { | ||
| let executable = format!("{}/{}", category, filename); | ||
| let argv = match language { | ||
| Language::Cpp => vec!["--", &executable], | ||
| Language::Python => vec!["--", "/usr/bin/python3", &executable], | ||
| Language::Java => vec![ | ||
| "--processes=32", | ||
| "--", | ||
| "/usr/lib/jvm/java-8-openjdk-amd64/bin/java", | ||
| "-cp", | ||
| &category, | ||
| "-Xms1024m", | ||
| "-Xmx1920m", | ||
| "-Xss512m", | ||
| "Main", | ||
| ], | ||
| _ => vec![], | ||
|
||
| } | ||
| .iter() | ||
| .map(|s| s.to_string()) | ||
| .collect(); | ||
|
|
||
| Self { argv } | ||
| } | ||
|
|
||
| pub fn arg(mut self, arg: &str) -> Self { | ||
| self.argv.push(arg.to_string()); | ||
| self | ||
| } | ||
|
|
||
| pub fn stdin(mut self, input_path: &str) -> Self { | ||
| self.argv = vec!["-i", input_path] | ||
| .iter() | ||
| .map(|s| s.to_string()) | ||
| .chain(self.argv.iter().cloned()) | ||
| .collect(); | ||
| self | ||
|
Comment on lines
+92
to
+98
|
||
| } | ||
|
|
||
| pub fn stdout(mut self, output_path: &str) -> Self { | ||
| self.argv = vec!["-o", output_path] | ||
| .iter() | ||
| .map(|s| s.to_string()) | ||
| .chain(self.argv.iter().cloned()) | ||
| .collect(); | ||
| self | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
RUN git clone https://github.com/ioi/isolate.gitstep fetches and installs a third-party tool directly from a mutable Git branch without pinning to a specific commit or verifying integrity, which exposes the build to supply-chain compromise if the repository or network is tampered with. An attacker who gains control over that Git ref could inject arbitrary code into the image and thus the grading environment. Pin this dependency to an immutable commit or signed release and add integrity verification (or vendor it) instead of cloning the moving default branch at build time.