diff --git a/.gitignore b/.gitignore index 67266ed6..676ed009 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ kjudge.db.bak kjudge.db-* /kjudge +/kjudge.exe /templates /keys /embed/templates diff --git a/README.md b/README.md index ad80bae2..ba456ea7 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,8 @@ Usage of ./kjudge: ## Build Instructions +Warning: Windows support for kjudge is a WIP (and by that we mean machine-wrecking WIP). Run at your own risk. + External Dependencies: ```yaml diff --git a/cmd/kjudge/main.go b/cmd/kjudge/main.go index 5e513530..a752af73 100644 --- a/cmd/kjudge/main.go +++ b/cmd/kjudge/main.go @@ -18,7 +18,7 @@ import ( var ( dbfile = flag.String("file", "kjudge.db", "Path to the database file.") - sandboxImpl = flag.String("sandbox", "isolate", "The sandbox implementation to be used (isolate, raw). If anything other than 'raw' is given, isolate is used.") + sandboxImpl = flag.String("sandbox", "isolate", "The sandbox implementation to be used (isolate, raw). Defaults to isolate.") port = flag.Int("port", 8088, "The port for the server to listen on.") httpsDir = flag.String("https", "", "Path to the directory where the HTTPS private key (kjudge.key) and certificate (kjudge.crt) is located. If omitted or empty, HTTPS is disabled.") @@ -34,11 +34,14 @@ func main() { defer db.Close() var sandbox worker.Sandbox - if *sandboxImpl == "raw" { + switch *sandboxImpl { + case "raw": log.Println("'raw' sandbox selected. WE ARE NOT RESPONSIBLE FOR ANY BREAKAGE CAUSED BY FOREIGN CODE.") sandbox = &raw.Sandbox{} - } else { + case "isolate": sandbox = isolate.New() + default: + log.Fatalf("Sandbox %s doesn't exists or not yet implemented.", *sandboxImpl) } // Start the queue @@ -58,9 +61,9 @@ func main() { go queue.Start() go startServer(server) - <-stop + received_signal := <-stop - log.Println("Shutting down") + log.Printf("Shutting down on receiving %s", received_signal) } func startServer(server *server.Server) { diff --git a/db/migrations.go b/db/migrations.go index 9fb3b7dc..6156d266 100644 --- a/db/migrations.go +++ b/db/migrations.go @@ -4,7 +4,7 @@ import ( "database/sql" "io/fs" "log" - "path" + "path/filepath" "regexp" "sort" @@ -44,7 +44,7 @@ func (db *DB) migrate() error { // Do migrations one by one for _, name := range versions { - sqlFile := path.Join(assetsSql, name+".sql") + sqlFile := filepath.Join(assetsSql, name+".sql") file, err := fs.ReadFile(embed.Content, sqlFile) if err != nil { return errors.Wrapf(err, "File %s", sqlFile) diff --git a/frontend/package.json b/frontend/package.json index 0fd89f21..218cf19d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -29,7 +29,8 @@ "test": "echo No tests... yet", "fmt": "prettier \"css/**/*\" \"ts/**/*\" \"*.json\" \"*.js\"", "dev": "parcel watch \"html/**/*.html\"", - "build": "mkdir -p ../embed/templates && rm -rf ../embed/templates/* && parcel build --no-source-maps --no-cache \"html/**/*.html\"" + "build": "mkdir -p ../embed/templates && rm -rf ../embed/templates/* && parcel build --no-source-maps --no-cache \"html/**/*.html\"", + "build:windows": "pwsh --command ../scripts/windows/frontend_build.ps1" }, "alias": { "react": "preact/compat", diff --git a/models/generate/main.go b/models/generate/main.go index 8d631413..81ff7ae8 100644 --- a/models/generate/main.go +++ b/models/generate/main.go @@ -7,6 +7,7 @@ import ( "log" "os" "os/exec" + "path/filepath" "sort" "strings" "text/template" @@ -331,14 +332,26 @@ func init() { template.Must(t.Parse(FileTemplate)) } +func removeLeftoverGeneratedFiles() { + files, err := filepath.Glob("models/*_generated.go") + if err != nil { + log.Fatal(err) + } + + for _, file := range files { + if err := os.Remove(file); err != nil { + log.Fatal(err) + } + } +} + func main() { var tables TomlTables if _, err := toml.DecodeFile("models/models.toml", &tables); err != nil { log.Fatal(err) } - if err := exec.Command("rm", "-fv", "models/*_generated.go").Run(); err != nil { - log.Fatal(err) - } + removeLeftoverGeneratedFiles() + files := []string{"-w"} // This is actually goimports's parameters. for name, fields := range tables { table := TableFromToml(tables, name, fields) diff --git a/models/rejudge.go b/models/rejudge.go index 19b7cd95..f49b1c88 100644 --- a/models/rejudge.go +++ b/models/rejudge.go @@ -20,7 +20,7 @@ func batchScoreJobs(subIDs ...int) []*Job { // Remove the submission's `score`, `penalty` and `verdict`. func resetScore(db db.DBContext, subIDs ...int) error { - query, params, err := sqlx.In(`UPDATE submissions SET score = NULL, penalty = NULL, verdict = "..." WHERE id IN (?)`, subIDs) + query, params, err := sqlx.In(`UPDATE submissions SET score = NULL, penalty = NULL, verdict = ? WHERE id IN (?)`, VerdictIsInQueue, subIDs) if err != nil { return errors.WithStack(err) } diff --git a/models/scoreboard.go b/models/scoreboard.go index a29d6d1c..0505b1e4 100644 --- a/models/scoreboard.go +++ b/models/scoreboard.go @@ -4,6 +4,7 @@ import ( "encoding/csv" "fmt" "io" + "log" "sort" "time" @@ -134,7 +135,8 @@ func (s *Scoreboard) JSON() JSONScoreboard { // Returns (comparison, is it just tie-breaking) func compareUserRanking(userResult []*UserResult, contestType ContestType, i, j int) (bool, bool) { a, b := userResult[i], userResult[j] - if contestType == ContestTypeWeighted { + switch contestType { + case ContestTypeWeighted: // sort based on totalScore if two users have same totalScore sort based on totalPenalty in an ascending order if a.TotalScore != b.TotalScore { return a.TotalScore > b.TotalScore, false @@ -143,7 +145,7 @@ func compareUserRanking(userResult []*UserResult, contestType ContestType, i, j return a.TotalPenalty < b.TotalPenalty, false } return a.User.ID < b.User.ID, true - } else { + case ContestTypeUnweighted: // sort based on solvedProblems if two users have same solvedProblems sort based on totalPenalty in an ascending order if a.SolvedProblems != b.SolvedProblems { return a.SolvedProblems > b.SolvedProblems, false @@ -153,6 +155,8 @@ func compareUserRanking(userResult []*UserResult, contestType ContestType, i, j } return a.User.ID < b.User.ID, true } + log.Panicf("unexpected contest type %s", contestType) + return true, true } // Get scoreboard given problems and contest diff --git a/models/submissions.go b/models/submissions.go index fcf09b2b..af52799c 100644 --- a/models/submissions.go +++ b/models/submissions.go @@ -24,6 +24,13 @@ const ( LanguageRust Language = "rustc" ) +const ( + VerdictCompileError = "Compile Error" + VerdictScored = "Scored" + VerdictAccepted = "Accepted" + VerdictIsInQueue = "..." +) + var availableLanguages []string // LanguageByExt returns a language based on the file extension. diff --git a/scripts/windows/frontend_build.ps1 b/scripts/windows/frontend_build.ps1 new file mode 100644 index 00000000..148c7049 --- /dev/null +++ b/scripts/windows/frontend_build.ps1 @@ -0,0 +1,5 @@ +$ErrorActionPreference = "Stop" + +New-Item -Type Directory -Force ../embed/templates | Out-Null +Remove-Item -Recurse -Force ../embed/templates/* +parcel build --no-source-maps --no-cache "html/**/*.html" diff --git a/scripts/windows/gen_cert.ps1 b/scripts/windows/gen_cert.ps1 new file mode 100644 index 00000000..0aacb22e --- /dev/null +++ b/scripts/windows/gen_cert.ps1 @@ -0,0 +1,100 @@ +<# +.SYNOPSIS + Generate self-signed SSL certificate +.DESCRIPTION + cert generation environment variables: + - OPENSSL_PATH [openssl] Path to openssl.exe + - RSA_BITS [4096] Strength of the RSA key. + - CERT_C [JP] Certificate country code + - CERT_ST [Moonland] Certificate State + - CERT_L [Kagamitown] Certificate Locality + - CERT_O [nki inc.] Certificate Organization Name + - CERT_CN [kjudge] Certificate Common name + - CERT_EMAIL [not@nkagami.me] Certificate Email address + - CERT_ALTNAMES [IP:127.0.0.1,DNS:localhost] A list of hosts that kjudge will be listening on, either by IP (as 'IP:1.2.3.4') or DNS (as 'DNS:google.com'), separated by ','" +.PARAMETER TargetDir + Target directory to export generated SSL certificate +#> + +Param ( + [Parameter( + Mandatory, + HelpMessage = "Target directory to export generated SSL certificate", + Position = 0 + )] [System.IO.FileInfo] $TargetDir +) + +# Break on first error +$ErrorActionPreference = "Stop" + +$OPENSSL_PATH = $Env:OPENSSL_PATH ?? "openssl" +Write-Host "OpenSSL Path: $OPENSSL_PATH" + +$CERT_C = $Env:CERT_C ?? "JP" # Country code +$CERT_ST = $Env:CERT_ST ?? "Moonland" # State +$CERT_L = $Env:CERT_L ?? "Kagamitown" # Locality +$CERT_O = $Env:CERT_O ?? "nki inc." # Organization Name +$CERT_CN = $Env:CERT_CN ?? "kjudge" # Common name +$CERT_EMAIL = $Env:CERT_EMAIL ?? "not@nkagami.me" # Email address +$CERT_ALTNAMES = $Env:CERT_ALTNAMES ?? "IP:127.0.0.1,DNS:localhost" # Alt hosts + +# All information +$CERT_SUBJ = "/C=$CERT_C/ST=$CERT_ST/L=$CERT_L/O=$CERT_O/CN=$CERT_CN/emailAddress=$CERT_EMAIL" +$CERT_EXT = "subjectAltName = $CERT_ALTNAMES" + +$RSA_BITS = $Env:RSA_BITS ?? 4096 # RSA bits + +# Paths +$ROOT_DIR = $TargetDir + +$CERT_GPATH = [IO.Path]::Combine($ROOT_DIR, '.certs_generated') +$ROOT_KEY = [IO.Path]::Combine($ROOT_DIR, "root.key") +$ROOT_CERT = [IO.Path]::Combine($ROOT_DIR, "root.pem") + +$KJUDGE_KEY = [IO.Path]::Combine($ROOT_DIR, "kjudge.key") +$KJUDGE_CERT = [IO.Path]::Combine($ROOT_DIR, "kjudge.crt") +$KJUDGE_CSR = [IO.Path]::Combine($ROOT_DIR, "kjudge.csr") + +Write-Host "Key info:" +Write-Host "- Country code = $CERT_C" +Write-Host "- State = $CERT_ST" +Write-Host "- Locality = $CERT_L" +Write-Host "- Organization Name = $CERT_O" +Write-Host "- Common name = $CERT_CN" +Write-Host "- Email address = $CERT_EMAIL" +Write-Host "- Alt hosts = $CERT_ALTNAMES" + +Function Build-Key { + If ([System.IO.File]::Exists([IO.Path]::Combine($ROOT_DIR, ".certs_generated"))){ + Write-Host "Certificate has already been generated." + return 0 + } + Write-Host "Generating root private key to $ROOT_KEY" + openssl genrsa -out "$ROOT_KEY" "$RSA_BITS" + + Write-Host "Generating a root certificate authority to $ROOT_CERT" + openssl req -x509 -new -key "$ROOT_KEY" -days 1285 -out "$ROOT_CERT" ` + -subj "$CERT_SUBJ" + + Write-Host "Generating a sub-key for kjudge to $KJUDGE_KEY" + openssl genrsa -out "$KJUDGE_KEY" "$RSA_BITS" + + Write-Host "Generating a certificate signing request to $KJUDGE_CSR" + openssl req -new -key "$KJUDGE_KEY" -out "$KJUDGE_CSR" ` + -subj "$CERT_SUBJ" -addext "$CERT_EXT" + + Write-Host "Generating a certificate signature to $KJUDGE_CERT" + Write-Output "[v3_ca]\n%s\n" "$CERT_EXT" | openssl x509 -req -days 730 ` + -in "$KJUDGE_CSR" ` + -CA "$ROOT_CERT" -CAkey "$ROOT_KEY" -CAcreateserial ` + -extensions v3_ca -extfile - ` + -out "$KJUDGE_CERT" + + Write-Host "Certificate generation complete." + Out-File -FilePath "$CERT_GPATH" +} +Build-Key + +Write-Host "To re-generate the keys, delete " "$CERT_GPATH" +Write-Host "Please keep $ROOT_KEY and $KJUDGE_KEY secret, while distributing" ` + "$ROOT_CERT as the certificate authority." diff --git a/scripts/windows/generate.ps1 b/scripts/windows/generate.ps1 new file mode 100644 index 00000000..a0575713 --- /dev/null +++ b/scripts/windows/generate.ps1 @@ -0,0 +1,8 @@ +$ErrorActionPreference = "Stop" + +Set-Location .\frontend +yarn +yarn run --prod build:windows +Set-Location .. + +go generate diff --git a/scripts/windows/production_build.ps1 b/scripts/windows/production_build.ps1 new file mode 100644 index 00000000..f6cb602e --- /dev/null +++ b/scripts/windows/production_build.ps1 @@ -0,0 +1,6 @@ +$ErrorActionPreference = "Stop" + +& "scripts\windows\generate.ps1" + +# Build +go build -tags "production" -o kjudge.exe cmd/kjudge/main.go diff --git a/server/admin/submission.go b/server/admin/submission.go index 8fa8a8e5..a690891f 100644 --- a/server/admin/submission.go +++ b/server/admin/submission.go @@ -11,7 +11,6 @@ import ( "github.com/natsukagami/kjudge/db" "github.com/natsukagami/kjudge/models" "github.com/natsukagami/kjudge/server/httperr" - "github.com/natsukagami/kjudge/worker" "github.com/pkg/errors" ) @@ -104,7 +103,7 @@ func (g *Group) SubmissionVerdictGet(c echo.Context) error { if err != nil { return err } - if ctx.Submission.Verdict == "..." || ctx.Submission.Verdict == worker.VerdictCompileError { + if ctx.Submission.Verdict == models.VerdictIsInQueue || ctx.Submission.Verdict == models.VerdictCompileError { return c.JSON(http.StatusOK, map[string]interface{}{ "verdict": ctx.Submission.Verdict, }) diff --git a/server/contests/problem.go b/server/contests/problem.go index 5014764d..7092f0e4 100644 --- a/server/contests/problem.go +++ b/server/contests/problem.go @@ -164,7 +164,7 @@ func (g *Group) SubmitPost(c echo.Context) error { Source: source, Language: lang, SubmittedAt: now, - Verdict: "...", + Verdict: models.VerdictIsInQueue, } if err := sub.Write(tx); err != nil { diff --git a/server/contests/submission.go b/server/contests/submission.go index 8f1dbf22..58fd3d87 100644 --- a/server/contests/submission.go +++ b/server/contests/submission.go @@ -9,7 +9,6 @@ import ( "github.com/natsukagami/kjudge/db" "github.com/natsukagami/kjudge/models" "github.com/natsukagami/kjudge/server/httperr" - "github.com/natsukagami/kjudge/worker" "github.com/pkg/errors" ) @@ -111,7 +110,7 @@ func (g *Group) SubmissionVerdictGet(c echo.Context) error { if err != nil { return err } - if ctx.Submission.Verdict == "..." || ctx.Submission.Verdict == worker.VerdictCompileError { + if ctx.Submission.Verdict == models.VerdictIsInQueue || ctx.Submission.Verdict == models.VerdictCompileError { return c.JSON(http.StatusOK, map[string]interface{}{ "verdict": ctx.Submission.Verdict, }) diff --git a/server/root_ca.go b/server/root_ca.go index 13f884e6..57a79bee 100644 --- a/server/root_ca.go +++ b/server/root_ca.go @@ -9,8 +9,8 @@ import ( "github.com/pkg/errors" ) -// ServeHTTPRootCA starts a HTTP server running on `address` -// serving the root CA from "/ca". It rejects all other requests. +// ServeHTTPRootCA starts a HTTP server running on `address` serving the .pem file at +// rootCA, serving the root CA from "/ca". It rejects all other requests. func (s *Server) ServeHTTPRootCA(address, rootCA string) error { if stat, err := os.Stat(rootCA); err != nil { return errors.WithStack(err) diff --git a/server/server.go b/server/server.go index eb2408a9..cc75abb4 100644 --- a/server/server.go +++ b/server/server.go @@ -24,7 +24,7 @@ import ( "github.com/pkg/errors" ) -// Server this the root entry of the server. +// Server is the root entry of the server. type Server struct { db *db.DB echo *echo.Echo diff --git a/worker/compile.go b/worker/compile.go index ee3c430d..7f087a6f 100644 --- a/worker/compile.go +++ b/worker/compile.go @@ -2,8 +2,8 @@ package worker // Compiling anything that's more compicated than single file: // -// - Prepare a "compile_%s.sh" file, with %s being the language (cc, go, rs, java, py2, py3, pas). -// - Prepare any more files as needed. They will all be put into the CWD of the script. +// - Prepare a "compile_%s.%ext" file, with %s being the language (cc, go, rs, java, py2, py3, pas) +// - Prepare any more files as needed. They will all be put into the CWD of the script // - The CWD also contains "code.%s" (%s being the language's respective extension) file, which is the contestant's source code. // - The script should do whatever it wants (unsandboxed, because it's not my job to do so) within 20 seconds. // - It should produce a single binary called "code" in the CWD. @@ -60,7 +60,7 @@ func Compile(c *CompileContext) (bool, error) { } else if !hasFile { // Batch compile mode enabled, but this language is not supported. c.Sub.CompiledSource = nil - c.Sub.Verdict = VerdictCompileError + c.Sub.Verdict = models.VerdictCompileError c.Sub.CompilerOutput = []byte("Custom Compilers are not enabled for this language.") return false, c.Sub.Write(c.DB) } @@ -94,15 +94,18 @@ func Compile(c *CompileContext) (bool, error) { c.Sub.CompiledSource = output } else { c.Sub.CompiledSource = nil - c.Sub.Verdict = VerdictCompileError + c.Sub.Verdict = models.VerdictCompileError } log.Printf("[WORKER] Compiling submission %v succeeded (result = %v).", c.Sub.ID, result) return result, c.Sub.Write(c.DB) } -// CompileAction is an action revolving writing the source into a file in "Source", -// compile it with "Command" and taking the "Output" as the result. +// CompileAction represents the following steps: +// 1. Write the source into a file in "Source". +// 2. Copy all files in Files into "Source" +// 3. Compile the source with "Command". +// 4. Produce "Output" as the result. type CompileAction struct { Source *models.File Files []*models.File diff --git a/worker/run.go b/worker/run.go index b13f545b..bb2e0b5c 100644 --- a/worker/run.go +++ b/worker/run.go @@ -126,7 +126,7 @@ func RunSingleCommand(sandbox Sandbox, r *RunContext, source []byte) (output *Sa return output, nil } -func RunMutipleCommands(sandbox Sandbox, r *RunContext, source []byte, stages []string) (output *SandboxOutput, err error) { +func RunMultipleCommands(sandbox Sandbox, r *RunContext, source []byte, stages []string) (output *SandboxOutput, err error) { command, args, err := RunCommand(r.Sub.Language) if err != nil { return nil, err @@ -193,7 +193,7 @@ func Run(sandbox Sandbox, r *RunContext) error { } else { // Problem Type is Chained Type, we need to run mutiple commands with arguments from .stages (file) stages := strings.Split(string(file.Content), "\n") - output, err = RunMutipleCommands(sandbox, r, source, stages) + output, err = RunMultipleCommands(sandbox, r, source, stages) if err != nil { return err } diff --git a/worker/score.go b/worker/score.go index 695b0260..36406c55 100644 --- a/worker/score.go +++ b/worker/score.go @@ -10,12 +10,6 @@ import ( "github.com/natsukagami/kjudge/models" ) -const ( - VerdictCompileError = "Compile Error" - VerdictScored = "Scored" - VerdictAccepted = "Accepted" -) - // ScoreContext is a context for calculating a submission's score // and update the user's problem scores. type ScoreContext struct { @@ -42,7 +36,7 @@ func Score(s *ScoreContext) error { return models.BatchInsertJobs(s.DB, models.NewJobCompile(s.Sub.ID), models.NewJobScore(s.Sub.ID)) } else if source == nil { log.Printf("[WORKER] Not running a submission that failed to compile.\n") - s.Sub.Verdict = VerdictCompileError + s.Sub.Verdict = models.VerdictCompileError if err := s.Sub.Write(s.DB); err != nil { return err } @@ -101,7 +95,7 @@ func Score(s *ScoreContext) error { func UpdateVerdict(tests []*models.TestGroupWithTests, sub *models.Submission) { score, _, counts := scoreOf(sub) if !counts { - sub.Verdict = VerdictCompileError + sub.Verdict = models.VerdictCompileError return } @@ -113,9 +107,9 @@ func UpdateVerdict(tests []*models.TestGroupWithTests, sub *models.Submission) { } if score == maxPossibleScore { - sub.Verdict = VerdictAccepted + sub.Verdict = models.VerdictAccepted } else { - sub.Verdict = VerdictScored + sub.Verdict = models.VerdictScored } } @@ -229,12 +223,12 @@ func (s *ScoreContext) CompareScores(subs []*models.Submission) *models.ProblemR } if s.Problem.ScoringMode == models.ScoringModeMin { if sub == which { - if sub.Verdict != VerdictAccepted { + if sub.Verdict != models.VerdictAccepted { failedAttempts++ } break } - } else if sub.Verdict == VerdictAccepted { + } else if sub.Verdict == models.VerdictAccepted { break } failedAttempts++ @@ -257,7 +251,7 @@ func (s *ScoreContext) CompareScores(subs []*models.Submission) *models.ProblemR contestType := s.Contest.ContestType if contestType == models.ContestTypeWeighted && maxScore == 0.0 { penalty = 0 - } else if contestType == models.ContestTypeUnweighted && which.Verdict != VerdictAccepted { + } else if contestType == models.ContestTypeUnweighted && which.Verdict != models.VerdictAccepted { penalty = 0 } return &models.ProblemResult{ @@ -265,7 +259,7 @@ func (s *ScoreContext) CompareScores(subs []*models.Submission) *models.ProblemR FailedAttempts: failedAttempts, Penalty: penalty, Score: maxScore, - Solved: which.Verdict == VerdictAccepted, + Solved: which.Verdict == models.VerdictAccepted, ProblemID: s.Problem.ID, UserID: s.Sub.UserID, }