Merge branch 'develop' version 1.2.2

This commit is contained in:
Paul Pan 2024-01-06 20:27:44 +08:00
commit ca10608d64
Signed by: Paul
GPG Key ID: D639BDF5BA578AF4
528 changed files with 710082 additions and 620 deletions

View File

@ -1,8 +1,11 @@
# top
/woj
# runner
# resource
resource/deploy
resource/frontend
resource/runner/.mark.image
resource/runner/problem/*
resource/runner/tmp/*
resource/runner/user/*

View File

@ -33,4 +33,8 @@ COPY --from=builder /builder/config.docker.yaml /app
COPY --from=builder /builder/docker-entrypoint.sh /app
COPY --from=builder /builder/woj /app
# switch user
RUN chown -R podman:podman /app
USER podman
ENTRYPOINT ["/app/docker-entrypoint.sh"]

View File

@ -17,6 +17,7 @@ RUN --mount=type=cache,id=golang,target=/go/pkg make build
# UI Builder
FROM git.0x7f.app/woj/woj-ui:1.0.0 AS ui-builder
RUN find /app -type f -name "*.map" -delete
# main image
FROM docker.io/library/alpine
@ -26,7 +27,6 @@ RUN apk --no-cache add tzdata ca-certificates libc6-compat bash
COPY --from=go-builder /builder/config.docker.yaml /app
COPY --from=go-builder /builder/docker-entrypoint.sh /app
COPY --from=go-builder /builder/resource/frontend /app/resource/frontend
COPY --from=go-builder /builder/woj /app
COPY --from=ui-builder /app /app/resource/frontend

View File

@ -1 +1 @@
1.2.1
1.2.2

View File

@ -76,8 +76,7 @@ func getBuildTime() time.Time {
func setupSentry() {
err := sentry.Init(sentry.ClientOptions{
Dsn: SentryDSN,
EnableTracing: true,
TracesSampleRate: 0.5,
EnableTracing: false,
SendDefaultPII: true,
Release: GitCommit,
})

View File

@ -105,6 +105,11 @@ func wrap(f func(i *do.Injector) error) func(*cli.Context) error {
}()
injector := prepareServices(c)
defer func() {
if err := injector.Shutdown(); err != nil {
slog.Printf("shutdown injector failed: %v", err)
}
}()
logger := do.MustInvoke[log.Service](injector)
defer func() { _ = logger.GetRawLogger().Sync() }()

View File

@ -1,8 +1,15 @@
WebServer:
Address: ${WEB_SERVER_ADDRESS}
Port: ${WEB_SERVER_PORT}
JwtSigningKey: ${WEB_SERVER_JWT_SIGNING_KEY}
JwtExpireHour: ${WEB_SERVER_JWT_EXPIRE_HOUR}
PublicBase: ${WEB_SERVER_PUBLIC_BASE}
TrustedPlatform: ${WEB_SERVER_TRUSTED_PLATFORM}
JWT:
SigningKey: ${WEB_SERVER_JWT_SIGNING_KEY}
ExpireHour: ${WEB_SERVER_JWT_EXPIRE_HOUR}
OAuth:
Domain: ${WEB_SERVER_OAUTH_DOMAIN}
ClientID: ${WEB_SERVER_OAUTH_CLIENT_ID}
ClientSecret: ${WEB_SERVER_OAUTH_CLIENT_SECRET}
Redis:
Db: ${REDIS_DB}

View File

@ -1,6 +1,6 @@
services:
server:
image: git.0x7f.app/woj/woj-server:1.2.1
image: git.0x7f.app/woj/woj-server:1.2.2
restart: unless-stopped
healthcheck:
test: [ "CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8000/health" ]
@ -33,7 +33,7 @@ services:
- "8000:8000"
runner:
image: git.0x7f.app/woj/woj-runner:1.2.1
image: git.0x7f.app/woj/woj-runner:1.2.2
restart: unless-stopped
command: runner
security_opt:

View File

@ -36,8 +36,13 @@ function check_env() {
check_env "WEB_SERVER_ADDRESS" "0.0.0.0" true
check_env "WEB_SERVER_PORT" 8000 false
check_env "WEB_SERVER_PUBLIC_BASE" "http://127.0.0.1:8000" true
check_env "WEB_SERVER_TRUSTED_PLATFORM" "" true
check_env "WEB_SERVER_JWT_SIGNING_KEY" "$(head -n 10 /dev/urandom | md5sum | cut -c 1-32)" true
check_env "WEB_SERVER_JWT_EXPIRE_HOUR" 12 false
check_env "WEB_SERVER_OAUTH_DOMAIN" "" true
check_env "WEB_SERVER_OAUTH_CLIENT_ID" "" true
check_env "WEB_SERVER_OAUTH_CLIENT_SECRET" "" true
check_env "REDIS_DB" 0 false
check_env "REDIS_QUEUE_DB" 1 false

26
go.mod
View File

@ -1,9 +1,10 @@
module git.0x7f.app/WOJ/woj-server
go 1.20
go 1.21.5
require (
github.com/TheZeroSlave/zapsentry v1.20.0
github.com/TheZeroSlave/zapsentry v1.20.2
github.com/coreos/go-oidc/v3 v3.9.0
github.com/getsentry/sentry-go v0.25.0
github.com/gin-contrib/cors v1.5.0
github.com/gin-contrib/pprof v1.4.0
@ -14,15 +15,16 @@ require (
github.com/hibiken/asynq v0.24.1
github.com/jackc/pgtype v1.14.0
github.com/minio/minio-go/v7 v7.0.66
github.com/prometheus/client_golang v1.17.0
github.com/prometheus/client_golang v1.18.0
github.com/redis/go-redis/v9 v9.3.1
github.com/samber/do v1.6.0
github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.0
github.com/swaggo/swag v1.16.2
github.com/urfave/cli/v2 v2.26.0
github.com/urfave/cli/v2 v2.27.1
go.uber.org/zap v1.26.0
golang.org/x/crypto v0.17.0
golang.org/x/oauth2 v0.15.0
golang.org/x/text v0.14.0
gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/postgres v1.5.4
@ -42,10 +44,11 @@ require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-jose/go-jose/v3 v3.0.1 // indirect
github.com/go-openapi/jsonpointer v0.20.2 // indirect
github.com/go-openapi/jsonreference v0.20.3 // indirect
github.com/go-openapi/spec v0.20.12 // indirect
github.com/go-openapi/swag v0.22.5 // indirect
github.com/go-openapi/jsonreference v0.20.4 // indirect
github.com/go-openapi/spec v0.20.13 // indirect
github.com/go-openapi/swag v0.22.7 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.16.0 // indirect
@ -86,12 +89,13 @@ require (
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/arch v0.6.0 // indirect
golang.org/x/arch v0.7.0 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/sync v0.5.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.16.1 // indirect
google.golang.org/protobuf v1.31.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/protobuf v1.32.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
)

63
go.sum
View File

@ -2,15 +2,17 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/TheZeroSlave/zapsentry v1.20.0 h1:PP7Qb2OPue+E87NqvEq4xcT50Y7BXKYHdGwb0GmsrpE=
github.com/TheZeroSlave/zapsentry v1.20.0/go.mod h1:D1YMfSuu6xnkhwFXxrronesmsiyDhIqo+86I3Ok+r64=
github.com/TheZeroSlave/zapsentry v1.20.2 h1:llgC91ZJdoU/OzGxYpUlEhKinf65mw9hJ2KkZ7+cGIk=
github.com/TheZeroSlave/zapsentry v1.20.2/go.mod h1:D1YMfSuu6xnkhwFXxrronesmsiyDhIqo+86I3Ok+r64=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE=
@ -25,6 +27,8 @@ github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLI
github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0=
github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/coreos/go-oidc/v3 v3.9.0 h1:0J/ogVOd4y8P0f0xUh8l9t07xRP/d8tccvjHl2dcsSo=
github.com/coreos/go-oidc/v3 v3.9.0/go.mod h1:rTKz2PYwftcrtoCzV5g5kvfJoWcm0Mk8AF8y1iAQro4=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM=
@ -39,6 +43,7 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cu
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/getsentry/sentry-go v0.25.0 h1:q6Eo+hS+yoJlTO3uu/azhQadsD8V+jQn2D8VvX1eOyI=
@ -46,6 +51,7 @@ github.com/getsentry/sentry-go v0.25.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w
github.com/gin-contrib/cors v1.5.0 h1:DgGKV7DDoOn36DFkNtbHrjoRiT5ExCe+PC9/xp7aKvk=
github.com/gin-contrib/cors v1.5.0/go.mod h1:TvU7MAZ3EwrPLI2ztzTt3tqgvBCq+wn8WpZmfADjupI=
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
github.com/gin-contrib/pprof v1.4.0 h1:XxiBSf5jWZ5i16lNOPbMTVdgHBdhfGRD5PZ1LWazzvg=
github.com/gin-contrib/pprof v1.4.0/go.mod h1:RrehPJasUVBPK6yTUwOl8/NP6i0vbUgmxtis+Z5KE90=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
@ -58,18 +64,22 @@ github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA=
github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q=
github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs=
github.com/go-openapi/jsonreference v0.20.3 h1:EjGcjTW8pD1mRis6+w/gmoBdqv5+RbE9B85D1NgDOVQ=
github.com/go-openapi/jsonreference v0.20.3/go.mod h1:FviDZ46i9ivh810gqzFLl5NttD5q3tSlMLqLr6okedM=
github.com/go-openapi/spec v0.20.12 h1:cgSLbrsmziAP2iais+Vz7kSazwZ8rsUZd6TUzdDgkVI=
github.com/go-openapi/spec v0.20.12/go.mod h1:iSCgnBcwbMW9SfzJb8iYynXvcY6C/QFrI7otzF7xGM4=
github.com/go-openapi/swag v0.22.5 h1:fVS63IE3M0lsuWRzuom3RLwUMVI2peDH01s6M70ugys=
github.com/go-openapi/swag v0.22.5/go.mod h1:Gl91UqO+btAM0plGGxHqJcQZ1ZTy6jbmridBTsDy8A0=
github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU=
github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4=
github.com/go-openapi/spec v0.20.13 h1:XJDIN+dLH6vqXgafnl5SUIMnzaChQ6QTo0/UPMbkIaE=
github.com/go-openapi/spec v0.20.13/go.mod h1:8EOhTpBoFiask8rrgwbLC3zmJfz4zsCUueRuPM6GNkw=
github.com/go-openapi/swag v0.22.7 h1:JWrc1uc/P9cSomxfnsFSVWoE1FW6bNbrVPmpQYpCcR8=
github.com/go-openapi/swag v0.22.7/go.mod h1:Gl91UqO+btAM0plGGxHqJcQZ1ZTy6jbmridBTsDy8A0=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
@ -90,6 +100,7 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
@ -178,6 +189,7 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
@ -218,13 +230,15 @@ github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZO
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
@ -240,6 +254,7 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
@ -288,8 +303,8 @@ github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/urfave/cli/v2 v2.26.0 h1:3f3AMg3HpThFNT4I++TKOejZO8yU55t3JnnSr4S4QEI=
github.com/urfave/cli/v2 v2.26.0/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho=
github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e h1:+SOyEddqYF09QP7vr7CgJ1eti3pY9Fn3LHO1M1r/0sI=
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
@ -303,6 +318,7 @@ go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
@ -318,12 +334,13 @@ go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.6.0 h1:S0JTfE48HbRj80+4tbvZDYsJ3tGv6BUU3XxyZ7CirAc=
golang.org/x/arch v0.6.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc=
golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
@ -339,6 +356,7 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -350,11 +368,13 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ=
golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -377,8 +397,8 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@ -389,6 +409,7 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
@ -414,11 +435,13 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@ -19,7 +19,6 @@ func (h *handler) ProblemUpdate(_ context.Context, t *asynq.Task) error {
if p.Status != e.Success {
h.log.Warn("RunnerError", zap.Any("payload", p))
return nil
}
status := h.problemService.UpdateVersion(
@ -29,7 +28,7 @@ func (h *handler) ProblemUpdate(_ context.Context, t *asynq.Task) error {
Bytes: []byte(p.Context),
Status: pgtype.Present,
},
"IsEnabled": true,
"IsEnabled": p.Status == e.Success,
},
)

View File

@ -19,7 +19,6 @@ func (h *handler) SubmitUpdate(_ context.Context, t *asynq.Task) error {
if p.Status != e.Success {
h.log.Warn("RunnerError", zap.Any("payload", p))
return nil
}
createData := &status.CreateData{

View File

@ -0,0 +1,116 @@
package oauth
import (
"context"
"fmt"
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/internal/service/user"
"github.com/gin-gonic/gin"
"net/http"
)
// CallbackHandler
// @Summary Callback with OAuth2
// @Description Callback endpoint from OAuth2
// @Tags oauth
// @Produce json
// @Router /oauth/callback [get]
func (h *handler) CallbackHandler() gin.HandlerFunc {
// TODO: we are returning e.Response directly here, we should redirect to a trampoline page, passing the response as query string
return func(c *gin.Context) {
// Extract key from cookie
key, err := c.Cookie(oauthStateCookieName)
if err != nil {
e.Pong[any](c, e.InvalidParameter, nil)
return
}
// Get state from redis
key = fmt.Sprintf(oauthStateKey, key)
expected, err := h.cache.Get().Get(context.Background(), key).Result()
if err != nil {
e.Pong[any](c, e.RedisError, nil)
return
}
// Whether state is valid, delete it
h.cache.Get().Unlink(context.Background(), key)
c.SetCookie(oauthStateCookieName, "", -1, "/", "", false, true)
// Verify state
if c.Query("state") != expected {
e.Pong[any](c, e.OAuthStateMismatch, nil)
return
}
// Exchange code for token
token, err := h.conf.Exchange(context.Background(), c.Query("code"))
if err != nil {
e.Pong[any](c, e.OAuthExchangeFailed, nil)
return
}
// Extract the ID Token from OAuth2 token.
raw, ok := token.Extra("id_token").(string)
if !ok {
e.Pong[any](c, e.OAuthExchangeFailed, nil)
return
}
// Parse and verify ID Token payload.
idToken, err := h.verifier.Verify(context.Background(), raw)
if err != nil {
e.Pong[any](c, e.OAuthVerifyFailed, nil)
return
}
// Extract custom claims
// TODO: extract role from claims
var claims struct {
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
Nickname string `json:"preferred_username"`
Role string `json:"role"`
}
if err := idToken.Claims(&claims); err != nil {
e.Pong[any](c, e.OAuthGetClaimsFailed, nil)
return
}
if !claims.EmailVerified || claims.Email == "" || claims.Nickname == "" {
e.Pong[any](c, e.UserInvalid, nil)
return
}
// Check user existence
u, status := h.user.ProfileOrCreate(&user.CreateData{Email: claims.Email, NickName: claims.Nickname})
if status != e.Success {
e.Pong[any](c, status, nil)
return
}
// Increment user version
version, status := h.user.IncrVersion(u.ID)
if status != e.Success {
e.Pong[any](c, status, nil)
return
}
// Sign JWT token
claim := &model.Claim{
UID: u.ID,
Role: u.Role,
Version: version,
}
jwt, status := h.jwt.SignClaim(claim)
if status != e.Success {
e.Pong[any](c, status, nil)
return
}
// TODO: Figure out a better way to cooperate with frontend
c.Redirect(http.StatusFound, "/login?redirect_token="+jwt)
// e.Pong(c, status, userApi.LoginResponse{Token: jwt, NickName: u.NickName})
}
}

View File

@ -0,0 +1,71 @@
package oauth
import (
"context"
"git.0x7f.app/WOJ/woj-server/internal/misc/config"
"git.0x7f.app/WOJ/woj-server/internal/misc/log"
"git.0x7f.app/WOJ/woj-server/internal/repo/cache"
"git.0x7f.app/WOJ/woj-server/internal/service/user"
"git.0x7f.app/WOJ/woj-server/internal/web/jwt"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/gin-gonic/gin"
"github.com/samber/do"
"go.uber.org/zap"
"golang.org/x/oauth2"
"time"
)
type Handler interface {
LoginHandler() gin.HandlerFunc
CallbackHandler() gin.HandlerFunc
}
const (
oauthStateCookieName = "oauth_state"
oauthStateKey = "OAuthState:%s"
oauthStateLiveness = 15 * time.Minute
)
func RouteRegister(rg *gin.RouterGroup, i *do.Injector) {
conf := do.MustInvoke[config.Service](i).GetConfig()
if conf.WebServer.OAuth.Domain == "" {
return
}
app := &handler{}
app.log = do.MustInvoke[log.Service](i).GetLogger("oauth")
app.jwt = do.MustInvoke[jwt.Service](i)
app.user = do.MustInvoke[user.Service](i)
app.cache = do.MustInvoke[cache.Service](i)
var err error
app.provider, err = oidc.NewProvider(context.Background(), conf.WebServer.OAuth.Domain)
if err != nil {
app.log.Error("failed to create oauth provider", zap.Error(err), zap.String("domain", conf.WebServer.OAuth.Domain))
return
}
app.verifier = app.provider.Verifier(&oidc.Config{ClientID: conf.WebServer.OAuth.ClientID})
app.conf = oauth2.Config{
ClientID: conf.WebServer.OAuth.ClientID,
ClientSecret: conf.WebServer.OAuth.ClientSecret,
RedirectURL: conf.WebServer.PublicBase + rg.BasePath() + "/callback",
Endpoint: app.provider.Endpoint(),
Scopes: []string{oidc.ScopeOpenID, "profile", "email", "roles"},
}
rg.POST("/login", app.LoginHandler())
rg.GET("/callback", app.CallbackHandler())
}
type handler struct {
log *zap.Logger
jwt jwt.Service
user user.Service
cache cache.Service
provider *oidc.Provider
conf oauth2.Config
verifier *oidc.IDTokenVerifier
}

View File

@ -0,0 +1,36 @@
package oauth
import (
"context"
"fmt"
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/pkg/utils"
"github.com/gin-gonic/gin"
"net/http"
)
// LoginHandler
// @Summary Login with OAuth2
// @Description Get OAuth2 Login URL
// @Tags oauth
// @Produce json
// @Response 200 {object} e.Response[string] "random string"
// @Router /oauth/login [post]
func (h *handler) LoginHandler() gin.HandlerFunc {
return func(c *gin.Context) {
state := utils.RandomString(64)
key := utils.RandomString(16)
err := h.cache.Get().Set(context.Background(), fmt.Sprintf(oauthStateKey, key), state, oauthStateLiveness).Err()
if err != nil {
e.Pong[any](c, e.RedisError, nil)
return
}
c.SetSameSite(http.SameSiteStrictMode)
c.SetCookie(oauthStateCookieName, key, int(oauthStateLiveness.Seconds()), "/", "", false, true)
url := h.conf.AuthCodeURL(state)
e.Pong(c, e.Success, url)
}
}

View File

@ -37,6 +37,8 @@ func (h *handler) Search(c *gin.Context) {
param := problem.QueryData{
Keyword: req.Keyword,
Tag: req.Tag,
Associations: true,
ShouldEnable: true,
Offset: req.Offset,
Limit: req.Limit,
Count: &count,

View File

@ -22,12 +22,12 @@ func (h *handler) Build(_ context.Context, t *asynq.Task) error {
status, ctx := func() (e.Status, string) {
url, status := h.storageService.Get(p.StorageKey, time.Second*60*5)
if status != e.Success {
return e.InternalError, "{}"
return e.InternalError, "{\"Message\": \"storage error\"}"
}
config, status := h.runnerService.NewProblem(p.ProblemVersionID, url, true)
if status != e.Success {
return e.InternalError, "{}"
return e.InternalError, "{\"Message\": \"build error: " + status.String() + "\"}"
}
for i := range config.Languages {

View File

@ -7,6 +7,7 @@ import (
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/internal/service/runner"
"git.0x7f.app/WOJ/woj-server/pkg/file"
"git.0x7f.app/WOJ/woj-server/pkg/utils"
"github.com/hibiken/asynq"
"go.uber.org/zap"
@ -24,14 +25,17 @@ func (h *handler) Judge(_ context.Context, t *asynq.Task) error {
h.log.Info("judge", zap.Any("payload", p), zap.String("user", user))
status, point, ctx := func() (e.Status, int32, runner.JudgeStatus) {
systemError := runner.JudgeStatus{Message: "System Error"}
systemError := runner.JudgeStatus{
Message: "System Error",
Tasks: []runner.TaskStatus{{Verdict: runner.VerdictSystemError, Message: "API Error"}},
}
// 1. write user code
userCode := filepath.Join(runner.UserDir, user, fmt.Sprintf("%s.%s", user, p.Submission.Language))
if !utils.FileTouch(userCode) {
if !file.Touch(userCode) {
return e.InternalError, 0, systemError
}
err := utils.FileWrite(userCode, []byte(p.Submission.Code))
err := file.Write(userCode, []byte(p.Submission.Code))
if err != nil {
return e.InternalError, 0, systemError
}
@ -52,17 +56,11 @@ func (h *handler) Judge(_ context.Context, t *asynq.Task) error {
// 3. compile
compileResult, status := h.runnerService.Compile(p.ProblemVersionID, user, p.Submission.Language)
if status != e.Success {
return e.Success, 0, compileResult
return e.InternalError, 0, compileResult
}
// 4. config
config, err := h.runnerService.ParseConfig(p.ProblemVersionID, true)
if err != nil {
return e.InternalError, 0, systemError
}
// 5. run and judge
result, point, status := h.runnerService.RunAndJudge(p.ProblemVersionID, user, p.Submission.Language, &config)
// 4. run and judge
result, point, status := h.runnerService.RunAndJudge(p.ProblemVersionID, user, p.Submission.Language)
return utils.If(status != e.Success, e.InternalError, e.Success), point, result
}()

View File

@ -3,7 +3,6 @@ package status
import (
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/pkg/utils"
"github.com/gin-gonic/gin"
)
@ -58,7 +57,12 @@ func (h *handler) Query(c *gin.Context) {
var response []*submissionWithScore
for _, submission := range submissions {
cur, _ := h.statusService.Query(submission.ID, false)
point := utils.If(cur == nil, -1, cur.Point)
point := int32(-1)
if cur != nil {
point = cur.Point
}
resp := &submissionWithScore{
Submission: *submission,
Point: point,

View File

@ -5,12 +5,13 @@ import (
"git.0x7f.app/WOJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/internal/service/user"
"github.com/gin-gonic/gin"
"net/mail"
)
type createRequest struct {
UserName string `form:"username" json:"username" binding:"required"`
Password string `form:"password" json:"password" binding:"required"`
Email string `form:"email" json:"email" binding:"required"`
NickName string `form:"nickname" json:"nickname" binding:"required"`
Password string `form:"password" json:"password" binding:"required"`
}
// Create
@ -19,7 +20,7 @@ type createRequest struct {
// @Tags user
// @Accept application/x-www-form-urlencoded
// @Produce json
// @Param username formData string true "username"
// @Param email formData string true "email"
// @Param nickname formData string true "nickname"
// @Param password formData string true "password"
// @Response 200 {object} e.Response[string] "jwt token"
@ -31,11 +32,18 @@ func (h *handler) Create(c *gin.Context) {
return
}
// verify email is valid
_, err := mail.ParseAddress(req.Email)
if err != nil {
e.Pong[any](c, e.InvalidParameter, nil)
return
}
// create user
createData := &user.CreateData{
UserName: req.UserName,
Password: req.Password,
Email: req.Email,
NickName: req.NickName,
Password: req.Password,
}
u, status := h.userService.Create(createData)
if status != e.Success {

View File

@ -8,11 +8,11 @@ import (
)
type loginRequest struct {
UserName string `form:"username" json:"username" binding:"required"`
Email string `form:"email" json:"email" binding:"required"`
Password string `form:"password" json:"password" binding:"required"`
}
type loginResponse struct {
type LoginResponse struct {
Token string `json:"token"`
NickName string `json:"nickname"`
}
@ -23,9 +23,9 @@ type loginResponse struct {
// @Tags user
// @Accept application/x-www-form-urlencoded
// @Produce json
// @Param username formData string true "username"
// @Param email formData string true "email"
// @Param password formData string true "password"
// @Response 200 {object} e.Response[loginResponse] "jwt token and user's nickname"
// @Response 200 {object} e.Response[LoginResponse] "jwt token and user's nickname"
// @Router /v1/user/login [post]
func (h *handler) Login(c *gin.Context) {
req := new(loginRequest)
@ -36,7 +36,7 @@ func (h *handler) Login(c *gin.Context) {
// check password
loginData := &user.LoginData{
UserName: req.UserName,
Email: req.Email,
Password: req.Password,
}
u, status := h.userService.Login(loginData)
@ -57,5 +57,5 @@ func (h *handler) Login(c *gin.Context) {
Version: version,
}
token, status := h.jwtService.SignClaim(claim)
e.Pong(c, status, loginResponse{Token: token, NickName: u.NickName})
e.Pong(c, status, LoginResponse{Token: token, NickName: u.NickName})
}

View File

@ -6,12 +6,10 @@ import (
"git.0x7f.app/WOJ/woj-server/internal/misc/config"
"git.0x7f.app/WOJ/woj-server/internal/misc/log"
"git.0x7f.app/WOJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/pkg/utils"
"git.0x7f.app/WOJ/woj-server/pkg/zapasynq"
"github.com/hibiken/asynq"
"github.com/samber/do"
"go.uber.org/zap"
"runtime"
)
func RunRunner(i *do.Injector) error {
@ -34,7 +32,7 @@ func RunRunner(i *do.Injector) error {
DB: conf.Redis.QueueDb,
},
asynq.Config{
Concurrency: utils.If(runtime.NumCPU() > 1, runtime.NumCPU()-1, 1),
Concurrency: 1, // there's a worker pool in runner service
Logger: zapasynq.New(rlog),
Queues: map[string]int{model.QueueRunner: 1},
},

View File

@ -24,15 +24,8 @@ import (
)
func RunServerMigrate(i *do.Injector) error {
slog := do.MustInvoke[log.Service](i).GetLogger("app.server")
// Migrate and shutdown database
err := do.MustInvoke[db.Service](i).Close()
if err != nil {
slog.Warn("Database Close Failed", zap.Error(err))
}
return err
do.MustInvoke[db.Service](i).Migrate()
return nil
}
func RunServer(i *do.Injector) error {
@ -98,11 +91,5 @@ func RunServer(i *do.Injector) error {
// Graceful Shutdown Queue
queueSrv.Shutdown()
// Graceful Shutdown Database
err = do.MustInvoke[db.Service](i).Close()
if err != nil {
slog.Warn("Database Close Failed", zap.Error(err))
}
return err
}

View File

@ -21,6 +21,10 @@ const (
TokenInvalid
TokenSignError
TokenRevoked
OAuthStateMismatch
OAuthExchangeFailed
OAuthVerifyFailed
OAuthGetClaimsFailed
)
const (
@ -30,6 +34,8 @@ const (
UserUnauthenticated
UserUnauthorized
UserDisabled
UserWithoutPassword
UserInvalid
)
const (
@ -81,6 +87,10 @@ var msgText = map[Status]string{
TokenInvalid: "Token Invalid",
TokenSignError: "Token Sign Error",
TokenRevoked: "Token Revoked",
OAuthStateMismatch: "OAuth State Mismatch",
OAuthExchangeFailed: "OAuth Exchange Failed",
OAuthVerifyFailed: "OAuth Verify Failed",
OAuthGetClaimsFailed: "OAuth Get Claims Failed",
UserNotFound: "User Not Found",
UserWrongPassword: "User Wrong Password",
@ -88,6 +98,8 @@ var msgText = map[Status]string{
UserUnauthenticated: "User Unauthenticated",
UserUnauthorized: "User Unauthorized",
UserDisabled: "User Disabled",
UserWithoutPassword: "User Without Password",
UserInvalid: "User Invalid",
ProblemNotFound: "Problem Not Found",
ProblemNotAvailable: "Problem Not Available",

View File

@ -1,5 +1,7 @@
package e
import "errors"
type Status int
func (code Status) String() string {
@ -9,3 +11,7 @@ func (code Status) String() string {
}
return msgText[InternalError]
}
func (code Status) AsError() error {
return errors.New(code.String())
}

View File

@ -2,7 +2,7 @@ package config
import (
"git.0x7f.app/WOJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/pkg/utils"
"git.0x7f.app/WOJ/woj-server/pkg/file"
"github.com/samber/do"
"github.com/urfave/cli/v2"
"gopkg.in/yaml.v3"
@ -19,7 +19,7 @@ type Service interface {
func NewService(i *do.Injector) (Service, error) {
cliCtx := do.MustInvoke[*cli.Context](i)
data, err := utils.FileRead(cliCtx.String("config"))
data, err := file.Read(cliCtx.String("config"))
if err != nil {
log.Printf("Failed to setup config: %s\n", err.Error())
return nil, err

View File

@ -6,9 +6,9 @@ import (
type User struct {
gorm.Model `json:"meta"`
UserName string `json:"user_name" gorm:"not null;uniqueIndex"`
NickName string `json:"nick_name" gorm:"not null"`
Email string `json:"email" gorm:"not null;uniqueIndex"`
NickName string `json:"nick_name" gorm:"not null;uniqueIndex"`
Role Role `json:"role" gorm:"not null"`
Password []byte `json:"-" gorm:"not null"`
Password []byte `json:"-"`
IsEnabled bool `json:"is_enabled" gorm:"not null;index"`
}

View File

@ -3,8 +3,21 @@ package model
type ConfigWebServer struct {
Address string `yaml:"Address"`
Port int `yaml:"Port"`
JwtSigningKey string `yaml:"JwtSigningKey"`
JwtExpireHour int `yaml:"JwtExpireHour"`
PublicBase string `yaml:"PublicBase"`
TrustedPlatform string `yaml:"TrustedPlatform"`
JWT ConfigJWT `yaml:"JWT"`
OAuth ConfigOAuth `yaml:"OAuth"`
}
type ConfigJWT struct {
SigningKey string `yaml:"SigningKey"`
ExpireHour int `yaml:"ExpireHour"`
}
type ConfigOAuth struct {
Domain string `yaml:"Domain"`
ClientID string `yaml:"ClientID"`
ClientSecret string `yaml:"ClientSecret"`
}
type ConfigRedis struct {

View File

@ -22,8 +22,9 @@ var _ Service = (*service)(nil)
type Service interface {
Get() *gorm.DB
Close() error
Migrate()
HealthCheck() error
Shutdown() error
}
func NewService(i *do.Injector) (Service, error) {
@ -45,7 +46,44 @@ func (s *service) Get() *gorm.DB {
return s.db
}
func (s *service) Close() error {
func (s *service) Migrate() {
s.log.Info("Auto Migrating database...")
// Running AutoMigrate concurrently on the same model fails with various race conditions
// https://github.com/go-gorm/gorm/pull/6680
// https://github.com/go-gorm/postgres/pull/224
// Obtain a lock to prevent concurrent AutoMigrate
lockID := func(s string) int64 {
h := fnv.New64a()
_, err := h.Write([]byte(s))
return utils.If(err != nil, int64(0x4242AA55), int64(h.Sum64()))
}("gorm:migrator")
s.err = s.db.Exec("SELECT pg_advisory_lock(?)", lockID).Error
if s.err != nil {
s.log.Error("Failed to obtain lock", zap.Error(s.err))
return
}
_ = s.db.AutoMigrate(&model.User{})
_ = s.db.AutoMigrate(&model.Problem{})
_ = s.db.AutoMigrate(&model.ProblemVersion{})
_ = s.db.AutoMigrate(&model.Submission{})
_ = s.db.AutoMigrate(&model.Status{})
s.err = s.db.Exec("SELECT pg_advisory_unlock(?)", lockID).Error
if s.err != nil {
s.log.Error("Failed to release lock", zap.Error(s.err))
}
}
func (s *service) HealthCheck() error {
return s.err
}
func (s *service) Shutdown() error {
var db *sql.DB
db, s.err = s.db.DB()
if s.err != nil {
@ -56,10 +94,6 @@ func (s *service) Close() error {
return s.err
}
func (s *service) HealthCheck() error {
return s.err
}
func (s *service) setup(conf *model.Config) {
s.log.Info("Connecting to database...")
@ -104,41 +138,6 @@ func (s *service) setup(conf *model.Config) {
db.SetMaxOpenConns(conf.Database.MaxOpenConns)
db.SetMaxIdleConns(conf.Database.MaxIdleConns)
db.SetConnMaxLifetime(time.Duration(conf.Database.ConnMaxLifetime) * time.Minute)
s.migrateDatabase()
}
func (s *service) migrateDatabase() {
s.log.Info("Auto Migrating database...")
// Running AutoMigrate concurrently on the same model fails with various race conditions
// https://github.com/go-gorm/gorm/pull/6680
// https://github.com/go-gorm/postgres/pull/224
// Obtain a lock to prevent concurrent AutoMigrate
lockID := func(s string) int64 {
h := fnv.New64a()
_, err := h.Write([]byte(s))
return utils.If(err != nil, int64(0x4242AA55), int64(h.Sum64()))
}("gorm:migrator")
s.err = s.db.Exec("SELECT pg_advisory_lock(?)", lockID).Error
if s.err != nil {
s.log.Error("Failed to obtain lock", zap.Error(s.err))
return
}
_ = s.db.AutoMigrate(&model.User{})
_ = s.db.AutoMigrate(&model.Problem{})
_ = s.db.AutoMigrate(&model.ProblemVersion{})
_ = s.db.AutoMigrate(&model.Submission{})
_ = s.db.AutoMigrate(&model.Status{})
s.err = s.db.Exec("SELECT pg_advisory_unlock(?)", lockID).Error
if s.err != nil {
s.log.Error("Failed to release lock", zap.Error(s.err))
}
}
func (s *service) checkAlive(retry int) (*sql.DB, error) {

View File

@ -3,10 +3,9 @@ package runner
import (
"fmt"
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/pkg/utils"
"git.0x7f.app/WOJ/woj-server/pkg/file"
"go.uber.org/zap"
"os"
"os/exec"
"path"
"path/filepath"
)
@ -27,53 +26,43 @@ func init() {
Prefix = path.Join(wd, Prefix)
ProblemDir = path.Join(Prefix, ProblemDir)
ScriptsDir = path.Join(Prefix, ScriptsDir)
UserDir = path.Join(Prefix, UserDir)
TmpDir = path.Join(Prefix, TmpDir)
}
func (s *service) execute(script string, args ...string) error {
p := filepath.Join(ScriptsDir, script)
cmd := exec.Command(p, args...)
cmd.Dir = ScriptsDir
if s.verbose {
cmd.Stdout = os.Stderr
cmd.Stderr = os.Stderr
}
return cmd.Run()
func (s *service) ProblemExists(version uint) bool {
problemPath := filepath.Join(ProblemDir, fmt.Sprintf("%d", version))
return file.Exist(problemPath)
}
func (s *service) checkAndExecute(version uint, user string, lang string, script string, fail e.Status) e.Status {
func (s *service) check(version uint, user string, lang string) e.Status {
if !s.ProblemExists(version) {
s.log.Info("problem not exists", zap.Uint("version", version))
return e.RunnerProblemNotExist
}
if !s.userExists(user, fmt.Sprintf("%s.%s", user, lang)) {
userPath := filepath.Join(UserDir, user, fmt.Sprintf("%s.%s", user, lang))
if !file.Exist(userPath) {
s.log.Info("user program not exists", zap.String("user", user), zap.String("lang", lang))
return e.RunnerUserNotExist
}
err := s.execute(script, fmt.Sprintf("%d", version), user, lang)
if err != nil {
s.log.Info("execute failed",
zap.Error(err),
zap.Uint("version", version),
zap.String("user", user),
zap.String("lang", lang))
return fail
}
return e.Success
}
func (s *service) ProblemExists(version uint) bool {
problemPath := filepath.Join(ProblemDir, fmt.Sprintf("%d", version))
return utils.FileExist(problemPath)
func (s *service) getLangInfo(config *Config, lang string) (configLanguage, bool) {
for _, l := range config.Languages {
if l.Lang == lang {
return l, true
}
}
return configLanguage{}, false
}
func (s *service) userExists(user string, file string) bool {
userPath := filepath.Join(UserDir, user, file)
return utils.FileExist(userPath)
func (s *service) getLangScript(l *configLanguage, lang string) string {
if l.Type == "default" {
return "/woj/framework/template/default/" + lang + ".Makefile"
} else {
return "/woj/problem/judge/" + l.Script
}
}

View File

@ -3,23 +3,102 @@ package runner
import (
"fmt"
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/pkg/file"
"git.0x7f.app/WOJ/woj-server/pkg/utils"
"go.uber.org/zap"
"io"
"os"
"path/filepath"
"time"
)
func (s *service) Compile(version uint, user string, lang string) (JudgeStatus, e.Status) {
target := filepath.Join(UserDir, user, fmt.Sprintf("%s.out", user))
// 1. ensure problem/user exists
status := s.check(version, user, lang)
if status != e.Success {
return JudgeStatus{Message: "check failed"}, status
}
_ = os.Remove(target)
status := s.checkAndExecute(version, user, lang, "problem_compile.sh", e.RunnerUserCompileFailed)
config, err := s.ParseConfig(version, true)
if err != nil {
s.log.Error("[compile] parse config failed", zap.Error(err))
return JudgeStatus{
Message: "parse config failed",
Tasks: []TaskStatus{{Verdict: VerdictSystemError, Message: "parse config failed"}},
}, e.RunnerProblemParseFailed
}
log := filepath.Join(UserDir, user, fmt.Sprintf("%s.compile.log", user))
msg, err := utils.FileRead(log)
// 2. prepare judge environment
workDir := filepath.Join(UserDir, user)
judgeDir := filepath.Join(ProblemDir, fmt.Sprintf("%d", version), "judge")
sourceFile := filepath.Join(workDir, fmt.Sprintf("%s.%s", user, lang))
targetFile := filepath.Join(workDir, fmt.Sprintf("%s.out", user))
logFile := filepath.Join(workDir, fmt.Sprintf("%s.compile.log", user))
log, err := os.Create(logFile)
if err != nil {
s.log.Error("[compile] create log file failed", zap.Error(err))
return JudgeStatus{
Message: "create log file failed",
Tasks: []TaskStatus{{Verdict: VerdictSystemError, Message: "create log file failed"}},
}, e.RunnerUserCompileFailed
}
defer func(log *os.File) {
_ = log.Close()
}(log)
// 3. compile
err = utils.NewMust().
DoAny(func() error { return os.Remove(targetFile) }).
Do(func() error { return file.TouchErr(targetFile) }).
Do(func() error {
l, ok := s.getLangInfo(&config, lang)
script := s.getLangScript(&l, lang)
if !ok {
return e.RunnerProblemParseFailed.AsError()
}
args := []string{
"-v", fmt.Sprintf("%s:/woj/problem/judge:ro", judgeDir),
"-v", fmt.Sprintf("%s:/woj/user/%s.%s:ro", sourceFile, user, lang),
"-v", fmt.Sprintf("%s:/woj/user/%s.out", targetFile, user),
"-e", fmt.Sprintf("USER_PROG=%s", user),
"-e", fmt.Sprintf("LANG=%s", lang),
"git.0x7f.app/woj/ubuntu-full:latest",
"sh", "-c", fmt.Sprintf("cd /woj/user && make -f %s compile", script),
}
runArgs := &podmanArgs{
executeArgs: executeArgs{
args: args,
timeout: 60 * time.Second,
output: log,
},
memory: "256m",
}
return s.podmanRun(runArgs)
}).
Done()
if err != nil {
s.log.Info("[compile] compile failed",
zap.Error(err),
zap.Uint("version", version),
zap.String("user", user),
zap.String("lang", lang),
)
status = e.RunnerUserCompileFailed
}
// 4. read log
_, _ = log.Seek(0, io.SeekStart)
msg, err := io.ReadAll(log)
msg = utils.If(err == nil, msg, nil)
msgText := string(msg)
if !utils.FileExist(target) || utils.FileEmpty(target) {
if !file.Exist(targetFile) || file.Empty(targetFile) {
return JudgeStatus{
Message: "compile failed",
Tasks: []TaskStatus{{Verdict: VerdictCompileError, Message: msgText}}},

View File

@ -8,18 +8,20 @@ import (
"path/filepath"
)
type configLanguage struct {
Lang string `json:"Lang"`
Type string `json:"Type,omitempty"`
Script string `json:"Script,omitempty"`
Cmp string `json:"Cmp,omitempty"`
}
type Config struct {
Runtime struct {
TimeLimit int `json:"TimeLimit"`
MemoryLimit int `json:"MemoryLimit"`
NProcLimit int `json:"NProcLimit"`
} `json:"Runtime"`
Languages []struct {
Lang string `json:"Lang"`
Type string `json:"Type,omitempty"`
Script string `json:"Script,omitempty"`
Cmp string `json:"Cmp,omitempty"`
} `json:"Languages"`
Languages []configLanguage `json:"Languages"`
Tasks []struct {
Id int `json:"Id"`
Points int32 `json:"Points"`

View File

@ -1,35 +1,84 @@
package runner
import (
"errors"
"fmt"
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/pkg/file"
"git.0x7f.app/WOJ/woj-server/pkg/utils"
"go.uber.org/zap"
"os"
"os/exec"
"path/filepath"
)
func (s *service) EnsureDeps(force bool) e.Status {
mark := filepath.Join(Prefix, ".mark.image")
type depConfig struct {
tarball string
image string
dockerfile string
}
if force {
_ = os.Remove(mark)
} else if utils.FileExist(mark) {
return e.Success
func (s *service) loadImage(cfg *depConfig) e.Status {
err := utils.NewTryErr().
Try(func() error {
// import from tarball
if !file.Exist(cfg.tarball) {
return errors.New("tarball not exists")
}
return s.execute("bash", "-c", fmt.Sprintf("gzip -d -c %s | podman load", cfg.tarball))
}).
Or(func() error {
// pull from docker hub
return s.execute("podman", "pull", cfg.image)
}).
Or(func() error {
// build from dockerfile
if !file.Exist(cfg.dockerfile) {
return errors.New("dockerfile not exists")
}
return s.execute("podman", "build", "-f", cfg.dockerfile, "-t", cfg.image, ".")
}).
Done()
script := filepath.Join(ScriptsDir, "prepare_images.sh")
cmd := exec.Command(script)
cmd.Dir = ScriptsDir
if s.verbose {
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
}
err := cmd.Run()
if err != nil {
s.log.Warn("prebuild docker images failed", zap.Error(err))
s.log.Warn("[deps] load image failed", zap.Error(err))
return e.RunnerDepsBuildFailed
}
return e.Success
}
func (s *service) EnsureDeps(force bool) e.Status {
mark := filepath.Join(Prefix, ".mark.image")
// check mark
if force {
_ = os.Remove(mark)
} else if file.Exist(mark) {
return e.Success
}
// full
fullImage := &depConfig{
tarball: filepath.Join(TmpDir, "ubuntu-full.tar.gz"),
image: "git.0x7f.app/woj/ubuntu-full:latest",
dockerfile: filepath.Join(ScriptsDir, "ubuntu-full.Dockerfile"),
}
if s.loadImage(fullImage) != e.Success {
return e.RunnerDepsBuildFailed
}
// tiny
tinyImage := &depConfig{
tarball: filepath.Join(TmpDir, "ubuntu-tiny.tar.gz"),
image: "git.0x7f.app/woj/ubuntu-run:latest",
dockerfile: filepath.Join(ScriptsDir, "ubuntu-run.Dockerfile"),
}
if s.loadImage(tinyImage) != e.Success {
return e.RunnerDepsBuildFailed
}
// mark
_, _ = os.Create(mark)
return e.Success
}

View File

@ -0,0 +1,109 @@
package runner
import (
"context"
"errors"
"fmt"
"git.0x7f.app/WOJ/woj-server/pkg/file"
"git.0x7f.app/WOJ/woj-server/pkg/utils"
"os"
"os/exec"
"time"
)
func (s *service) execute(exe string, args ...string) error {
cmd := exec.Command(exe, args...)
cmd.Dir = Prefix
if s.verbose {
cmd.Stdout = os.Stderr
cmd.Stderr = os.Stderr
}
return cmd.Run()
}
type executeArgs struct {
exe string
args []string
timeout time.Duration
kill func() error
output *os.File
limit int64
}
func (s *service) executeTimeout(arg *executeArgs) error {
ctx, cancel := context.WithTimeout(context.Background(), arg.timeout)
defer cancel()
cmd := exec.CommandContext(ctx, arg.exe, arg.args...)
cmd.Dir = Prefix
if arg.kill != nil {
cmd.Cancel = arg.kill
}
if s.verbose && arg.output == nil {
cmd.Stdout = os.Stderr
cmd.Stderr = os.Stderr
} else if arg.output != nil {
if arg.limit == 0 {
cmd.Stdout = arg.output
cmd.Stderr = arg.output
} else {
lw := &file.LimitedWriter{
File: arg.output,
Limit: arg.limit,
}
cmd.Stdout = lw
cmd.Stderr = lw
}
}
err := cmd.Start()
if err != nil {
return err
}
err = cmd.Wait()
// make sure the process is killed
_ = cmd.Process.Kill()
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
return fmt.Errorf("command timed out")
}
return err
}
type podmanArgs struct {
executeArgs
memory string
}
func (s *service) podmanRun(arg *podmanArgs) error {
name := fmt.Sprintf("woj-%d-%s", time.Now().UnixNano(), utils.RandomString(8))
execArgs := &executeArgs{
exe: "podman",
output: utils.If(arg.output == nil, os.Stderr, arg.output),
limit: utils.If(arg.limit == 0, 4*1024, arg.limit),
timeout: utils.If(arg.timeout == 0, 10*time.Second, arg.timeout),
kill: func() error {
if arg.kill != nil {
_ = arg.kill()
}
return s.execute("podman", "kill", name)
},
}
args := []string{
"run",
"--rm",
"--name", name,
"--memory", utils.If(arg.memory == "", "256m", arg.memory),
}
args = append(args, arg.args...)
execArgs.args = args
return s.executeTimeout(execArgs)
}

View File

@ -4,11 +4,12 @@ import (
"fmt"
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/pkg/down"
"git.0x7f.app/WOJ/woj-server/pkg/file"
"git.0x7f.app/WOJ/woj-server/pkg/unzip"
"git.0x7f.app/WOJ/woj-server/pkg/utils"
"go.uber.org/zap"
"os"
"path/filepath"
"time"
)
func (s *service) download(version uint, url string) e.Status {
@ -17,13 +18,13 @@ func (s *service) download(version uint, url string) e.Status {
err := down.Down(zipPath, url)
if err != nil {
s.log.Error("download problem failed", zap.Error(err))
s.log.Error("[new] download problem failed", zap.Error(err))
return e.RunnerDownloadFailed
}
err = unzip.Unzip(zipPath, problemPath)
if err != nil {
s.log.Warn("unzip problem failed", zap.Error(err))
s.log.Warn("[new] unzip problem failed", zap.Error(err))
return e.RunnerUnzipFailed
}
@ -38,14 +39,37 @@ func (s *service) prebuild(version uint, force bool) e.Status {
mark := filepath.Join(ProblemDir, fmt.Sprintf("%d", version), ".mark.prebuild")
if force {
_ = os.Remove(mark)
} else if utils.FileExist(mark) {
} else if file.Exist(mark) {
return e.Success
}
err := s.execute("problem_prebuild.sh", fmt.Sprintf("%d", version))
prebuildScript := filepath.Join(ProblemDir, fmt.Sprintf("%d", version), "judge", "prebuild.Makefile")
if !file.Exist(prebuildScript) {
s.log.Info("[new] prebuild script not found", zap.String("path", prebuildScript), zap.Uint("version", version))
return e.Success
}
dataDir := filepath.Join(ProblemDir, fmt.Sprintf("%d", version), "data")
judgeDir := filepath.Join(ProblemDir, fmt.Sprintf("%d", version), "judge")
args := []string{
"-v", fmt.Sprintf("%s:/woj/problem/data", dataDir),
"-v", fmt.Sprintf("%s:/woj/problem/judge", judgeDir),
"-e", "PREFIX=/woj/problem",
"git.0x7f.app/woj/ubuntu-full:latest",
"sh", "-c", "cd /woj/problem/judge && make -f prebuild.Makefile prebuild && touch .mark.prebuild",
}
runArgs := &podmanArgs{
executeArgs: executeArgs{
args: args,
timeout: 300 * time.Second,
},
memory: "1g",
}
err := s.podmanRun(runArgs)
if err != nil {
s.log.Warn("prebuild problem failed", zap.Error(err), zap.Uint("version", version))
s.log.Warn("[new] prebuild problem failed", zap.Error(err), zap.Uint("version", version))
return e.RunnerProblemPrebuildFailed
}
@ -67,6 +91,7 @@ func (s *service) NewProblem(version uint, url string, force bool) (Config, e.St
cfg, err := s.ParseConfig(version, false)
if err != nil {
s.log.Info("[new] parse problem failed", zap.Error(err), zap.Uint("version", version))
return Config{}, e.RunnerProblemParseFailed
}

View File

@ -1,22 +0,0 @@
package runner
import "git.0x7f.app/WOJ/woj-server/internal/e"
func (s *service) RunAndJudge(version uint, user string, lang string, config *Config) (JudgeStatus, int32, e.Status) {
// run user program
status := s.checkAndExecute(version, user, lang, "problem_run.sh", e.RunnerRunFailed)
if status != e.Success {
return JudgeStatus{Message: "run failed"}, 0, status
}
// run judger
status = s.checkAndExecute(version, user, lang, "problem_judge.sh", e.RunnerJudgeFailed)
if status != e.Success {
return JudgeStatus{Message: "judge failed"}, 0, status
}
// check result
result, pts := s.checkResults(user, config)
return result, pts, e.Success
}

View File

@ -0,0 +1,183 @@
package runner
import (
"fmt"
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/pkg/file"
"git.0x7f.app/WOJ/woj-server/pkg/utils"
"go.uber.org/zap"
"os"
"path/filepath"
"time"
)
func (s *service) problemRun(version uint, user string, lang string, config *Config) {
workDir := filepath.Join(UserDir, user)
dataDir := filepath.Join(ProblemDir, fmt.Sprintf("%d", version), "data", "input")
// woj-sandbox killer will add 2 more seconds, here we add 2 more seconds
timeout := time.Duration((config.Runtime.TimeLimit+1000)/1000+2+2) * time.Second
ids := make([]int, 0)
for _, task := range config.Tasks {
f := func(id int) func() {
return func() {
testCase := filepath.Join(dataDir, fmt.Sprintf("%d.input", id))
targetFile := filepath.Join(workDir, fmt.Sprintf("%s.out", user))
ansFile := filepath.Join(workDir, fmt.Sprintf("%d.out.usr", id))
ifoFile := filepath.Join(workDir, fmt.Sprintf("%d.info", id))
err := utils.NewMust().
DoAny(func() error { return os.Remove(ansFile) }).
DoAny(func() error { return os.Remove(ifoFile) }).
Do(func() error { return file.TouchErr(ansFile) }).
Do(func() error { return file.TouchErr(ifoFile) }).
Do(func() error {
args := []string{
"--cpus", "1",
"--network", "none",
"-v", fmt.Sprintf("%s:/woj/problem/data/input/%d.input:ro", testCase, id),
"-v", fmt.Sprintf("%s:/woj/user/%s.out:ro", targetFile, user),
"-v", fmt.Sprintf("%s:/woj/user/%d.out.usr", ansFile, id),
"-v", fmt.Sprintf("%s:/woj/user/%d.info", ifoFile, id),
"git.0x7f.app/woj/ubuntu-run:latest",
"sh", "-c",
fmt.Sprintf("cd /woj/user && /woj/framework/scripts/woj_launcher "+
"--memory_limit=%d "+
"--nproc_limit=%d "+
"--time_limit=%d "+
"--sandbox_template=%s "+
"--sandbox_action=ret "+
"--uid=1000 "+
"--gid=1000 "+
"--file_input=/woj/problem/data/input/%d.input "+
"--file_output=/woj/user/%d.out.usr "+
"--file_info=/woj/user/%d.info "+
"-program=/woj/user/%s.out",
config.Runtime.MemoryLimit,
config.Runtime.NProcLimit,
config.Runtime.TimeLimit,
lang,
id, id, id,
user,
),
}
runArgs := &podmanArgs{
executeArgs: executeArgs{
args: args,
timeout: timeout,
},
}
return s.podmanRun(runArgs)
}).
Done()
if err != nil {
s.log.Info("[run] run failed",
zap.Error(err),
zap.Uint("version", version),
zap.String("user", user),
zap.String("lang", lang),
)
}
}
}(task.Id)
id := s.pool.AddTask(f)
ids = append(ids, id)
}
for _, id := range ids {
s.pool.WaitForTask(id)
}
}
func (s *service) problemJudge(version uint, user string, lang string, config *Config) {
workDir := filepath.Join(UserDir, user)
dataDir := filepath.Join(ProblemDir, fmt.Sprintf("%d", version), "data")
judgeDir := filepath.Join(ProblemDir, fmt.Sprintf("%d", version), "judge")
ids := make([]int, 0)
for _, task := range config.Tasks {
f := func(id int) func() {
return func() {
ansFile := filepath.Join(workDir, fmt.Sprintf("%d.out.usr", id))
jdgFile := filepath.Join(workDir, fmt.Sprintf("%d.judge", id))
c, ok := s.getLangInfo(config, lang)
if !ok {
return
}
err := utils.NewMust().
DoAny(func() error { return os.Remove(jdgFile) }).
Do(func() error { return file.TouchErr(jdgFile) }).
Do(func() error {
args := []string{
"-v", fmt.Sprintf("%s:/woj/problem/judge:ro", judgeDir),
"-v", fmt.Sprintf("%s:/woj/problem/data:ro", dataDir),
"-v", fmt.Sprintf("%s:/woj/user/%d.out.usr", ansFile, id),
"-v", fmt.Sprintf("%s:/woj/user/%d.judge", jdgFile, id),
"-e", fmt.Sprintf("TEST_NUM=%d", id),
"-e", fmt.Sprintf("CMP=%s", c.Cmp),
"git.0x7f.app/woj/ubuntu-full:latest",
"sh", "-c",
fmt.Sprintf("cd /woj/user && make -f %s judge", s.getLangScript(&c, lang)),
}
runArgs := &podmanArgs{
executeArgs: executeArgs{
args: args,
},
}
return s.podmanRun(runArgs)
}).
Done()
if err != nil {
s.log.Info("[judge] judge failed",
zap.Error(err),
zap.Uint("version", version),
zap.String("user", user),
zap.String("lang", lang),
)
}
}
}(task.Id)
id := s.pool.AddTask(f)
ids = append(ids, id)
}
for _, id := range ids {
s.pool.WaitForTask(id)
}
}
func (s *service) RunAndJudge(version uint, user string, lang string) (JudgeStatus, int32, e.Status) {
// 1. ensure problem/user exists
status := s.check(version, user, lang)
if status != e.Success {
return JudgeStatus{Message: "check failed"}, 0, status
}
// 2. config
config, err := s.ParseConfig(version, false)
if err != nil {
return JudgeStatus{Message: "parse config failed"}, 0, e.RunnerProblemParseFailed
}
// 3. run user program
s.problemRun(version, user, lang, &config)
// 4. run judger
s.problemJudge(version, user, lang, &config)
// 5. check result
result, pts := s.checkResults(user, &config)
return result, pts, e.Success
}

View File

@ -4,8 +4,11 @@ import (
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/misc/config"
"git.0x7f.app/WOJ/woj-server/internal/misc/log"
"git.0x7f.app/WOJ/woj-server/pkg/pool"
"git.0x7f.app/WOJ/woj-server/pkg/utils"
"github.com/samber/do"
"go.uber.org/zap"
"runtime"
)
var _ Service = (*service)(nil)
@ -19,7 +22,7 @@ type Service interface {
// Compile compile user submission
Compile(version uint, user string, lang string) (JudgeStatus, e.Status)
// RunAndJudge execute user program
RunAndJudge(version uint, user string, lang string, config *Config) (JudgeStatus, int32, e.Status)
RunAndJudge(version uint, user string, lang string) (JudgeStatus, int32, e.Status)
// ParseConfig parse config file
ParseConfig(version uint, skipCheck bool) (Config, error)
@ -27,20 +30,33 @@ type Service interface {
ProblemExists(version uint) bool
HealthCheck() error
Shutdown() error
}
func NewService(i *do.Injector) (Service, error) {
return &service{
concurrency := utils.If(runtime.NumCPU() > 1, runtime.NumCPU()-1, 1)
srv := &service{
log: do.MustInvoke[log.Service](i).GetLogger("runner"),
pool: pool.NewTaskPool(concurrency, concurrency),
verbose: do.MustInvoke[config.Service](i).GetConfig().Development,
}, nil
}
srv.pool.Start()
return srv, nil
}
type service struct {
log *zap.Logger
pool *pool.TaskPool
verbose bool
}
func (s *service) HealthCheck() error {
return nil
}
func (s *service) Shutdown() error {
s.pool.Stop()
return nil
}

View File

@ -5,7 +5,7 @@ import (
"encoding/json"
"encoding/xml"
"fmt"
"git.0x7f.app/WOJ/woj-server/pkg/utils"
"git.0x7f.app/WOJ/woj-server/pkg/file"
"golang.org/x/text/encoding/charmap"
"io"
"path/filepath"
@ -57,7 +57,7 @@ func (t *TaskStatus) getInfoText(infoFile string) *TaskStatus {
}
var err error
t.infoText, err = utils.FileRead(infoFile)
t.infoText, err = file.Read(infoFile)
if err != nil {
t.Verdict = VerdictSystemError
t.Message = "cannot read info file"
@ -128,7 +128,7 @@ func (t *TaskStatus) getJudgeText(judgeFile string) *TaskStatus {
return t
}
j, err := utils.FileRead(judgeFile)
j, err := file.Read(judgeFile)
if err != nil {
t.Verdict = VerdictSystemError
t.Message = "cannot read judge file"

View File

@ -9,9 +9,9 @@ import (
)
type CreateData struct {
UserName string
Password string
Email string
NickName string
Password string
}
func (s *service) Create(data *CreateData) (*model.User, e.Status) {
@ -22,9 +22,9 @@ func (s *service) Create(data *CreateData) (*model.User, e.Status) {
}
user := &model.User{
UserName: data.UserName,
Password: hashed,
Email: data.Email,
NickName: data.NickName,
Password: hashed,
Role: model.RoleGeneral,
IsEnabled: true,
}

View File

@ -10,12 +10,12 @@ import (
)
type LoginData struct {
UserName string
Email string
Password string
}
func (s *service) Login(data *LoginData) (*model.User, e.Status) {
user := &model.User{UserName: data.UserName}
user := &model.User{Email: data.Email}
err := s.db.Get().Where(user).First(&user).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
@ -29,6 +29,10 @@ func (s *service) Login(data *LoginData) (*model.User, e.Status) {
if !user.IsEnabled {
return nil, e.UserDisabled
}
if len(user.Password) == 0 {
// created by oauth
return nil, e.UserWithoutPassword
}
err = bcrypt.CompareHashAndPassword(user.Password, []byte(data.Password))
if err != nil {

View File

@ -22,3 +22,22 @@ func (s *service) Profile(uid uint) (*model.User, e.Status) {
return user, e.Success
}
func (s *service) ProfileOrCreate(data *CreateData) (*model.User, e.Status) {
user := &model.User{
Email: data.Email,
NickName: data.NickName,
Role: model.RoleGeneral,
IsEnabled: true,
}
// Notice: FirstOrCreate will not update the record if it exists, and also we should not update the record
// Notice: OAuth2 created user will not have password
err := s.db.Get().Where(model.User{Email: data.Email}).FirstOrCreate(&user, data).Error
if err != nil {
s.log.Warn("DatabaseError", zap.Error(err), zap.Any("user", user))
return nil, e.DatabaseError
}
return user, e.Success
}

View File

@ -17,6 +17,7 @@ type Service interface {
Login(data *LoginData) (*model.User, e.Status)
IncrVersion(uid uint) (int64, e.Status)
Profile(uid uint) (*model.User, e.Status)
ProfileOrCreate(data *CreateData) (*model.User, e.Status)
HealthCheck() error
}

View File

@ -29,8 +29,8 @@ func NewService(i *do.Injector) (Service, error) {
srv.cacheService = do.MustInvoke[cache.Service](i) // .Get().(*redis.Client)
conf := do.MustInvoke[config.Service](i).GetConfig()
srv.SigningKey = []byte(conf.WebServer.JwtSigningKey)
srv.ExpireHour = conf.WebServer.JwtExpireHour
srv.SigningKey = []byte(conf.WebServer.JWT.SigningKey)
srv.ExpireHour = conf.WebServer.JWT.ExpireHour
return srv, srv.err
}

View File

@ -3,7 +3,7 @@ package metrics
import (
"git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/misc/config"
"git.0x7f.app/WOJ/woj-server/internal/pkg/cast"
"git.0x7f.app/WOJ/woj-server/pkg/cast"
"github.com/gin-gonic/gin"
"github.com/prometheus/client_golang/prometheus"
"github.com/samber/do"
@ -61,7 +61,7 @@ func (s *service) setup(namespace string, subsystem string) {
Subsystem: subsystem,
Name: "requests_details",
Help: "Details of each request",
Buckets: []float64{0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 25, 50, 100, 250, 500, 1000},
Buckets: []float64{0.01, 0.05, 0.1, 0.25, 0.5, 1, 1.5, 2, 2.5, 3, 5, 10, 15, 20, 25, 50, 75, 100, 150, 200, 500, 750, 1000, 1500, 2000},
},
[]string{"method", "url", "success", "http_code", "err_code"},
)

View File

@ -2,6 +2,7 @@ package router
import (
"git.0x7f.app/WOJ/woj-server/internal/api/debug"
"git.0x7f.app/WOJ/woj-server/internal/api/oauth"
"git.0x7f.app/WOJ/woj-server/internal/api/problem"
"git.0x7f.app/WOJ/woj-server/internal/api/status"
"git.0x7f.app/WOJ/woj-server/internal/api/submission"
@ -30,4 +31,5 @@ var endpoints = []model.EndpointInfo{
{Version: "/v1", Path: "/problem", Register: problem.RouteRegister},
{Version: "/v1", Path: "/submission", Register: submission.RouteRegister},
{Version: "/v1", Path: "/status", Register: status.RouteRegister},
{Version: "/v1", Path: "/oauth", Register: oauth.RouteRegister},
}

View File

@ -40,9 +40,9 @@ func NewService(i *do.Injector) (Service, error) {
}
type service struct {
metric metrics.Service
logger log.Service
engine *gin.Engine
metric metrics.Service
err error
}
@ -58,15 +58,20 @@ func (s *service) initRouters(conf *model.Config, injector *do.Injector) *gin.En
gin.SetMode(utils.If[string](conf.Development, gin.DebugMode, gin.ReleaseMode))
r := gin.New()
r.MaxMultipartMemory = 8 << 20 // 8MB
// +--------------+
// |Configurations|
// +--------------+
if conf.WebServer.TrustedPlatform != "" {
// Extract Origin IP
r.TrustedPlatform = conf.WebServer.TrustedPlatform
}
// +-----------+
// |Middlewares|
// +-----------+
// static files - must before sentry
r.Use(static.Serve("/", static.LocalFile("./resource/frontend", true)))
// Sentry middleware
r.Use(sentrygin.New(sentrygin.Options{Repanic: true}))
@ -129,6 +134,9 @@ func (s *service) initRouters(conf *model.Config, injector *do.Injector) *gin.En
api := r.Group("/api/")
s.setupApi(api, injector)
// static files
r.Use(static.Serve("/", static.LocalFile("./resource/frontend", true)))
// fallback to frontend
r.NoRoute(func(c *gin.Context) {
c.File("./resource/frontend/index.html")

View File

@ -1 +0,0 @@
package web

45
pkg/file/file.go Normal file
View File

@ -0,0 +1,45 @@
package file
import (
"git.0x7f.app/WOJ/woj-server/pkg/utils"
"io"
"os"
"path/filepath"
)
func Read(filePath string) ([]byte, error) {
f, err := os.Open(filePath)
if err != nil {
return nil, err
}
return io.ReadAll(f)
}
func Write(filePath string, content []byte) error {
return os.WriteFile(filePath, content, 0644)
}
func Exist(filePath string) bool {
_, err := os.Stat(filePath)
return utils.If(err == nil || os.IsExist(err), true, false)
}
func Empty(filePath string) bool {
stat, err := os.Stat(filePath)
if err != nil {
return true
}
return stat.Size() == 0
}
func Touch(filePath string) bool {
err := TouchErr(filePath)
return utils.If(err == nil, true, false)
}
func TouchErr(filePath string) error {
base := filepath.Dir(filePath)
_ = os.MkdirAll(base, 0755)
_, err := os.OpenFile(filePath, os.O_RDONLY|os.O_CREATE, 0644)
return err
}

21
pkg/file/writer.go Normal file
View File

@ -0,0 +1,21 @@
package file
import (
"fmt"
"os"
)
type LimitedWriter struct {
File *os.File
Limit int64
n int64
}
func (lw *LimitedWriter) Write(p []byte) (n int, err error) {
if lw.n+int64(len(p)) > lw.Limit {
return 0, fmt.Errorf("output limit exceeded")
}
n, err = lw.File.Write(p)
lw.n += int64(n)
return
}

78
pkg/pool/pool.go Normal file
View File

@ -0,0 +1,78 @@
package pool
import (
"sync"
)
type TaskPool struct {
workers int
queue chan Task
wg sync.WaitGroup
lck sync.Mutex
curTaskID int
waitMap map[int]chan struct{}
}
func NewTaskPool(maxWorkers, bufferSize int) *TaskPool {
return &TaskPool{
workers: maxWorkers,
queue: make(chan Task, bufferSize),
waitMap: make(map[int]chan struct{}),
curTaskID: 1, // task id starts from 1
}
}
func (tp *TaskPool) Start() {
for i := 1; i <= tp.workers; i++ { // worker id starts from 1
worker := NewWorker(i, tp.queue, tp)
tp.wg.Add(1)
go worker.Start(&tp.wg)
}
}
func (tp *TaskPool) AddTask(f func()) int {
tp.lck.Lock()
defer tp.lck.Unlock()
id := tp.curTaskID
tp.curTaskID++
task := Task{id: id, f: f}
tp.queue <- task
waitChan := make(chan struct{})
tp.waitMap[id] = waitChan
return id
}
func (tp *TaskPool) WaitForTask(taskID int) {
tp.lck.Lock()
waitChan, ok := tp.waitMap[taskID]
if !ok {
tp.lck.Unlock()
return
}
tp.lck.Unlock()
<-waitChan
}
func (tp *TaskPool) markTaskComplete(taskID int) {
tp.lck.Lock()
defer tp.lck.Unlock()
waitChan, ok := tp.waitMap[taskID]
if !ok {
return
}
close(waitChan)
delete(tp.waitMap, taskID)
}
func (tp *TaskPool) Stop() {
close(tp.queue)
tp.wg.Wait()
}

62
pkg/pool/pool_test.go Normal file
View File

@ -0,0 +1,62 @@
package pool
import (
"sync"
"testing"
"time"
)
func TestTaskPool_Stop(t *testing.T) {
pool := NewTaskPool(5, 10)
pool.Start()
lck := sync.Mutex{}
counter := 0
for i := 1; i <= 10; i++ {
f := func(i int) func() {
return func() {
lck.Lock()
t.Log("task", i, "locked")
counter += i
t.Log("task", i, "unlocked")
lck.Unlock()
time.Sleep(time.Duration(i*100) * time.Millisecond)
t.Log("task", i, "finished")
}
}(i)
pool.AddTask(f)
}
pool.Stop()
if counter != 55 {
t.Error("some tasks were not executed")
}
}
func TestTaskPool_WaitForTask(t *testing.T) {
pool := NewTaskPool(10, 10)
pool.Start()
counter := 0
for i := 1; i <= 10; i++ {
f := func(i int) func() {
return func() {
counter += 1
t.Log("task", i, "finished")
}
}(i)
id := pool.AddTask(f)
pool.WaitForTask(id)
if counter != 1 {
t.Errorf("Counter mismatch: expected %d, got %d, task %d", 1, counter, id)
}
counter -= 1
}
pool.Stop()
}

6
pkg/pool/task.go Normal file
View File

@ -0,0 +1,6 @@
package pool
type Task struct {
id int
f func()
}

24
pkg/pool/worker.go Normal file
View File

@ -0,0 +1,24 @@
package pool
import (
"sync"
)
type Worker struct {
id int
queue chan Task
pool *TaskPool // back reference to the pool
}
func NewWorker(id int, queue chan Task, pool *TaskPool) *Worker {
return &Worker{id: id, queue: queue, pool: pool}
}
func (w *Worker) Start(wg *sync.WaitGroup) {
defer wg.Done()
for task := range w.queue {
task.f()
w.pool.markTaskComplete(task.id)
}
}

View File

@ -1,39 +0,0 @@
package utils
import (
"io"
"os"
"path/filepath"
)
func FileRead(filePath string) ([]byte, error) {
f, err := os.Open(filePath)
if err != nil {
return nil, err
}
return io.ReadAll(f)
}
func FileWrite(filePath string, content []byte) error {
return os.WriteFile(filePath, content, 0644)
}
func FileExist(filePath string) bool {
_, err := os.Stat(filePath)
return If(err == nil || os.IsExist(err), true, false)
}
func FileEmpty(filePath string) bool {
stat, err := os.Stat(filePath)
if err != nil {
return true
}
return stat.Size() == 0
}
func FileTouch(filePath string) bool {
base := filepath.Dir(filePath)
_ = os.MkdirAll(base, 0755)
_, err := os.OpenFile(filePath, os.O_RDONLY|os.O_CREATE, 0644)
return If(err == nil, true, false)
}

25
pkg/utils/must.go Normal file
View File

@ -0,0 +1,25 @@
package utils
type MustChain struct {
err error
}
func NewMust() *MustChain {
return &MustChain{}
}
func (c *MustChain) Do(callback func() error) *MustChain {
if c.err == nil {
c.err = callback()
}
return c
}
func (c *MustChain) DoAny(callback func() error) *MustChain {
_ = callback()
return c
}
func (c *MustChain) Done() error {
return c.err
}

View File

@ -1,6 +1,9 @@
package utils
import (
"crypto/hmac"
"crypto/sha512"
"encoding/base64"
"math/rand"
)
@ -14,3 +17,22 @@ func RandomString(n int) string {
return string(s)
}
func SignString(s string, key []byte) string {
mac := hmac.New(sha512.New, key)
mac.Write([]byte(s))
return base64.StdEncoding.EncodeToString(mac.Sum(nil))
}
func SignAndCompare(s string, exp string, key []byte) bool {
mac := hmac.New(sha512.New, key)
mac.Write([]byte(s))
decoded, err := base64.StdEncoding.DecodeString(exp)
if err != nil {
return false
}
return hmac.Equal(mac.Sum(nil), decoded)
}

51
pkg/utils/try.go Normal file
View File

@ -0,0 +1,51 @@
package utils
type TryChain[T any, V comparable] struct {
result T
error V
success V
}
func NewTry[T any, V comparable](success V) *TryChain[T, V] {
return &TryChain[T, V]{success: success}
}
func (c *TryChain[T, V]) Try(callback func() (T, V)) *TryChain[T, V] {
c.result, c.error = callback()
return c
}
func (c *TryChain[T, V]) Or(callback func() (T, V)) *TryChain[T, V] {
if c.error == c.success {
return c
}
return c.Try(callback)
}
func (c *TryChain[T, V]) Done() (T, V) {
return c.result, c.error
}
type TryChainErr struct {
err error
}
func NewTryErr() *TryChainErr {
return &TryChainErr{}
}
func (c *TryChainErr) Try(callback func() error) *TryChainErr {
c.err = callback()
return c
}
func (c *TryChainErr) Or(callback func() error) *TryChainErr {
if c.err == nil {
return c
}
return c.Try(callback)
}
func (c *TryChainErr) Done() error {
return c.err
}

57
pkg/utils/try_test.go Normal file
View File

@ -0,0 +1,57 @@
package utils
import "testing"
func TestTry(t *testing.T) {
t.Run("First Try", func(t *testing.T) {
val, err := NewTry[int, bool](true).
Try(func() (int, bool) { return 1, true }).
Or(func() (int, bool) { return 2, false }).
Or(func() (int, bool) { return 3, true }).
Or(func() (int, bool) { return 4, false }).
Done()
if val != 1 && err != true {
t.Error("Try failed")
}
})
t.Run("Middle Or", func(t *testing.T) {
val, err := NewTry[int, bool](true).
Try(func() (int, bool) { return 1, false }).
Or(func() (int, bool) { return 2, false }).
Or(func() (int, bool) { return 3, true }).
Or(func() (int, bool) { return 4, false }).
Done()
if val != 3 && err != true {
t.Error("Try failed")
}
})
t.Run("Last Or", func(t *testing.T) {
val, err := NewTry[int, bool](true).
Try(func() (int, bool) { return 1, false }).
Or(func() (int, bool) { return 2, false }).
Or(func() (int, bool) { return 3, false }).
Or(func() (int, bool) { return 4, true }).
Done()
if val != 4 && err != true {
t.Error("Try failed")
}
})
t.Run("Nothing", func(t *testing.T) {
val, err := NewTry[int, bool](true).
Try(func() (int, bool) { return 1, false }).
Or(func() (int, bool) { return 2, false }).
Or(func() (int, bool) { return 3, false }).
Or(func() (int, bool) { return 4, false }).
Done()
if val != 4 && err != false {
t.Error("Try failed")
}
})
}

View File

@ -60,7 +60,8 @@ metadata:
labels:
app: cache
spec:
type: ClusterIP
# for production use ClusterIP
type: LoadBalancer
selector:
app: cache
ports:

View File

@ -70,7 +70,8 @@ metadata:
labels:
app: db
spec:
type: ClusterIP
# for production use ClusterIP
type: LoadBalancer
selector:
app: db
ports:

View File

@ -35,7 +35,7 @@ spec:
spec:
containers:
- name: runner
image: git.0x7f.app/woj/woj-runner:1.2.1
image: git.0x7f.app/woj/woj-runner:1.2.2
imagePullPolicy: IfNotPresent
args:
- runner

View File

@ -20,7 +20,7 @@ spec:
spec:
containers:
- name: server
image: git.0x7f.app/woj/woj-server:1.2.1
image: git.0x7f.app/woj/woj-server:1.2.2
imagePullPolicy: IfNotPresent
args:
- server

View File

@ -10,6 +10,5 @@ cmake .. -DCMAKE_BUILD_TYPE=Release || exit 1
make -j || exit 1
cd ../..
cp woj-sandbox/build/libwoj_sandbox.so . || exit 1
cp woj-sandbox/build/woj_launcher . || exit 1
rm -rf woj-sandbox || exit 1

View File

@ -4,4 +4,4 @@ compile:
@$(CC) $(CFLAGS) -o $(PREFIX)/user/$(USER_PROG).out $(PREFIX)/user/$(USER_PROG).$(LANG)
judge:
$($(CMP)) $(PREFIX)/data/input/$(TEST_NUM).input $(PREFIX)/user/$(TEST_NUM).out.usr $(PREFIX)/data/output/$(TEST_NUM).output $(PREFIX)/user/$(TEST_NUM).judge -appes
$($(CMP)) $(PREFIX)/problem/data/input/$(TEST_NUM).input $(PREFIX)/user/$(TEST_NUM).out.usr $(PREFIX)/problem/data/output/$(TEST_NUM).output $(PREFIX)/user/$(TEST_NUM).judge -appes

View File

@ -4,4 +4,4 @@ compile:
@$(CXX) $(CFLAGS) -o $(PREFIX)/user/$(USER_PROG).out $(PREFIX)/user/$(USER_PROG).$(LANG)
judge:
$($(CMP)) $(PREFIX)/data/input/$(TEST_NUM).input $(PREFIX)/user/$(TEST_NUM).out.usr $(PREFIX)/data/output/$(TEST_NUM).output $(PREFIX)/user/$(TEST_NUM).judge -appes
$($(CMP)) $(PREFIX)/problem/data/input/$(TEST_NUM).input $(PREFIX)/user/$(TEST_NUM).out.usr $(PREFIX)/problem/data/output/$(TEST_NUM).output $(PREFIX)/user/$(TEST_NUM).judge -appes

View File

@ -0,0 +1,7 @@
include ${TEMPLATE}/go.mk ${TEMPLATE}/Judger.mk
compile:
@$(GO) build $(GO_BUILD_FLAGS) -o $(PREFIX)/user/$(USER_PROG).out $(PREFIX)/user/$(USER_PROG).$(LANG)
judge:
$($(CMP)) $(PREFIX)/problem/data/input/$(TEST_NUM).input $(PREFIX)/user/$(TEST_NUM).out.usr $(PREFIX)/problem/data/output/$(TEST_NUM).output $(PREFIX)/user/$(TEST_NUM).judge -appes

View File

@ -0,0 +1,2 @@
GO=/usr/local/go/bin/go
GO_BUILD_FLAGS=-trimpath

View File

@ -5,4 +5,4 @@ git clone --depth=1 https://github.com/MikeMirzayanov/testlib.git >/dev/null 2>&
rm -rf testlib/.git
rm -rf testlib/tests
cd testlib/checkers || exit 1
parallel clang++ -Ofast -march=native -Wall -pipe -I.. {}.cpp -o {} ::: fcmp hcmp lcmp ncmp nyesno rcmp4 rcmp6 rcmp9 wcmp yesno
parallel clang++ -O2 -Wall -pipe -I.. {}.cpp -o {} ::: fcmp hcmp lcmp ncmp nyesno rcmp4 rcmp6 rcmp9 wcmp yesno

View File

@ -2,3 +2,5 @@
!.gitignore
!example
!example/*
!book
!book/**/*

View File

@ -0,0 +1,43 @@
{
"Runtime": {
"TimeLimit": 1000,
"MemoryLimit": 16,
"NProcLimit": 1
},
"Languages": [
{
"Lang": "c",
"Type": "default",
"Script": "",
"Cmp": "HCMP"
},
{
"Lang": "cpp",
"Type": "default",
"Script": "",
"Cmp": "HCMP"
}
],
"Tasks": [
{
"Id": 1,
"Points": 20
},
{
"Id": 2,
"Points": 20
},
{
"Id": 3,
"Points": 20
},
{
"Id": 4,
"Points": 20
},
{
"Id": 5,
"Points": 20
}
]
}

View File

@ -0,0 +1 @@
20

View File

@ -0,0 +1 @@
55

View File

@ -0,0 +1 @@
88

View File

@ -0,0 +1 @@
133

View File

@ -0,0 +1 @@
345

View File

@ -0,0 +1 @@
2432902008176640000

View File

@ -0,0 +1 @@
12696403353658275925965100847566516959580321051449436762275840000000000000

View File

@ -0,0 +1 @@
185482642257398439114796845645546284380220968949399346684421580986889562184028199319100141244804501828416633516851200000000000000000000

View File

@ -0,0 +1 @@
14872707060906857289084508911813048098675809251055070300508818286592035566485075754388082124671571841702793317081960037166525246368924700537538282948117301741317436012998958826217903503076596121600000000000000000000000000000000

View File

@ -0,0 +1 @@
24215638650792346558700053691985855570120556040258652734839783267039961720178323593174739047913617079695531502689473012213820889134885853992818438056445080201482863675240494802269823110125881000284687377104376400792200165127855908498047507347955446603093964326987087311394274684237308398502911304969719715098068025497504900730580217016573270011698467378924291550780873605154736879542602554635558428265690302091342359471863508627516511203478353542187151045838267239168928747525890559708487655213488727530884968558716385000436989129479527833010340517760688345368715729020015336862534353876914871201776699205878662858555857265544230999178449256448000000000000000000000000000000000000000000000000000000000000000000000000000000000000

View File

@ -0,0 +1,26 @@
# 求 N! 的值
## Tags
- C++一本通
- 高精度
## Description
用高精度方法,求$N!$的精确值($N$以一般整数输入)
## Example Cases
### Case 1
#### Input
```
10
```
#### Output
```
3628800
```

View File

@ -0,0 +1,47 @@
{
"Runtime": {
"TimeLimit": 1000,
"MemoryLimit": 16,
"NProcLimit": 1
},
"Languages": [
{
"Lang": "c",
"Type": "default",
"Script": "",
"Cmp": "FCMP"
},
{
"Lang": "cpp",
"Type": "default",
"Script": "",
"Cmp": "FCMP"
}
],
"Tasks": [
{
"Id": 1,
"Points": 10
},
{
"Id": 2,
"Points": 10
},
{
"Id": 3,
"Points": 10
},
{
"Id": 4,
"Points": 23
},
{
"Id": 5,
"Points": 22
},
{
"Id": 6,
"Points": 25
}
]
}

View File

@ -0,0 +1 @@
6 5

View File

@ -0,0 +1 @@
30 6

View File

@ -0,0 +1 @@
18 5

View File

@ -0,0 +1 @@
12 7

View File

@ -0,0 +1 @@
8 3

View File

@ -0,0 +1 @@
1034 1033

View File

@ -0,0 +1 @@
6/5=1.2

View File

@ -0,0 +1 @@
30/6=5.0

View File

@ -0,0 +1 @@
18/5=3.6

View File

@ -0,0 +1 @@
12/7=1.71428571428571428571

View File

@ -0,0 +1 @@
8/3=2.66666666666666666666

View File

@ -0,0 +1 @@
1034/1033=1.000968054211035818

View File

@ -0,0 +1,42 @@
# 求A/B高精度值
## Tags
- C++一本通
- 高精度
## Description
计算$\frac AB$的精确值,设$A$$B$是以一般整数输入,计算结果精确小数后$20$位
(若不足$20$位,末尾不用补$0$)
## Example Cases
### Case 1
#### Input
```
4 3
```
#### Output
```
4/3=1.33333333333333333333
```
### Case 2
#### Input
```
6 5
```
#### Output
```
6/5=1.2
```

View File

@ -0,0 +1,43 @@
{
"Runtime": {
"TimeLimit": 1000,
"MemoryLimit": 16,
"NProcLimit": 1
},
"Languages": [
{
"Lang": "c",
"Type": "default",
"Script": "",
"Cmp": "NCMP"
},
{
"Lang": "cpp",
"Type": "default",
"Script": "",
"Cmp": "NCMP"
}
],
"Tasks": [
{
"Id": 1,
"Points": 20
},
{
"Id": 2,
"Points": 20
},
{
"Id": 3,
"Points": 20
},
{
"Id": 4,
"Points": 20
},
{
"Id": 5,
"Points": 20
}
]
}

View File

@ -0,0 +1 @@
123

Some files were not shown because too many files have changed in this diff Show More