diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..ba9207f --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,16 @@ +{ + "configurations": [ + { + "name": "Linux", + "includePath": [ + "${workspaceFolder}/**" + ], + "defines": [], + "compilerPath": "/usr/bin/gcc", + "cStandard": "gnu17", + "cppStandard": "gnu++17", + "intelliSenseMode": "linux-gcc-x64" + } + ], + "version": 4 +} \ No newline at end of file diff --git a/assets/msg-canvas-grades.tpl.html b/assets/msg-canvas-grades.tpl.html index be2f7f7..934f9b9 100644 --- a/assets/msg-canvas-grades.tpl.html +++ b/assets/msg-canvas-grades.tpl.html @@ -1,5 +1,5 @@

お姫様の成績がアップデートされました!

-

最近の採点

+

最近の採点

@@ -17,10 +17,9 @@ - + - + diff --git a/cmd/server/server.go b/cmd/server/server.go index 5b594ce..4ef67c0 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -3,8 +3,10 @@ package main import ( "flag" "log" + "os" "github.com/eternal-flame-AD/yoake/config" + "github.com/eternal-flame-AD/yoake/internal/apparmor" "github.com/eternal-flame-AD/yoake/internal/comm" "github.com/eternal-flame-AD/yoake/internal/db" "github.com/eternal-flame-AD/yoake/server" @@ -37,10 +39,45 @@ func init() { } } } + +func changeHat() { + profile := config.Config().Listen.AppArmor.Serve + if profile != "" { + token, err := apparmor.GetMagicToken() + if err != nil { + log.Panicf("failed to get apparmor magic token: %v", err) + } + if err := apparmor.ChangeHat(profile, token); err != nil { + log.Panicf("failed to change apparmor hat: %v", err) + } else { + log.Printf("changed apparmor hat to %s", profile) + } + } +} + func main() { listen := config.Config().Listen if listen.Ssl.Use { - log.Fatalln(server.Server.StartTLS(listen.Addr, listen.Ssl.Cert, listen.Ssl.Key)) + var sslCertBytes, sslKeyBytes []byte + apparmor.ExecuteInHat(listen.AppArmor.SSL, func() { + var err error + sslCertBytes, err = os.ReadFile(listen.Ssl.Cert) + if err != nil { + log.Panicf("failed to read ssl cert: %v", err) + } + sslKeyBytes, err = os.ReadFile(listen.Ssl.Key) + if err != nil { + log.Panicf("failed to read ssl key: %v", err) + } + }, true) + if listen.AppArmor.SSL != "" { + // defensive programming, try read ssl key + if _, err := os.ReadFile(listen.Ssl.Key); err == nil { + log.Panicf("AppArmor profile set for SSL but I could still read %v!", listen.Ssl.Key) + } + } + + log.Fatalln(server.Server.StartTLS(listen.Addr, sslCertBytes, sslKeyBytes)) } else { log.Fatalln(server.Server.Start(listen.Addr)) } diff --git a/config.yml b/config.yml index 89b91ab..0addfee 100644 --- a/config.yml +++ b/config.yml @@ -1,5 +1,7 @@ listen: addr: :80 + apparmor: + ssl: ssl ssl: use: off cert: "fullchain.pem" diff --git a/config/config.go b/config/config.go index 323a606..2fbd4f5 100644 --- a/config/config.go +++ b/config/config.go @@ -8,8 +8,12 @@ import ( type C struct { Hosts map[string]string Listen struct { - Addr string - Ssl struct { + Addr string + AppArmor struct { + Serve string + SSL string + } + Ssl struct { Use bool Cert string Key string diff --git a/etc/service-install.fish b/etc/service-install.fish index 9a5d903..edb0738 100755 --- a/etc/service-install.fish +++ b/etc/service-install.fish @@ -6,12 +6,13 @@ make verify make build or exit 2 +if ! diff etc/yoake-server.service /etc/systemd/system/yoake-server.service + sudo cp etc/yoake-server.service /etc/systemd/system/yoake-server.service + or exit 2 -sudo cp etc/yoake-server.service /etc/systemd/system/yoake-server.service - or exit 2 - -sudo systemctl daemon-reload - or exit 2 + sudo systemctl daemon-reload + or exit 2 +end sudo systemctl stop yoake-server.service diff --git a/internal/apparmor/api.go b/internal/apparmor/api.go new file mode 100644 index 0000000..01e805d --- /dev/null +++ b/internal/apparmor/api.go @@ -0,0 +1,65 @@ +package apparmor + +import ( + // #cgo LDFLAGS: -lapparmor + // #include "./apparmor.h" + "C" +) +import ( + "runtime" + "syscall" + "unsafe" +) + +func ChangeHat(subprofile string, magicToken uint64) error { + var ret uintptr + if subprofile != "" { + subProfileC := C.CString(subprofile) + defer C.free(unsafe.Pointer(subProfileC)) + ret = uintptr(C.go_aa_change_hat(subProfileC, C.ulong(magicToken))) + } else { + ret = uintptr(C.go_aa_change_hat(nil, C.ulong(magicToken))) + } + + if ret != 0 { + return syscall.Errno(ret) + } + return nil +} + +func ExecuteInHat(subprofile string, fn func(), lockThread bool) error { + if subprofile == "" { + fn() + return nil + } + if lockThread { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + } + token, err := GetMagicToken() + if err != nil { + return err + } + if err := ChangeHat(subprofile, token); err != nil { + return err + } + fn() + return ChangeHat("", token) +} + +func ChangeProfile(subprofile string) error { + var ret uintptr + + if subprofile != "" { + subProfileC := C.CString(subprofile) + defer C.free(unsafe.Pointer(subProfileC)) + ret = uintptr(C.go_aa_change_profile(subProfileC)) + } else { + ret = uintptr(C.go_aa_change_profile(nil)) + } + + if ret != 0 { + return syscall.Errno(ret) + } + return nil +} diff --git a/internal/apparmor/apparmor.c b/internal/apparmor/apparmor.c new file mode 100644 index 0000000..c867b00 --- /dev/null +++ b/internal/apparmor/apparmor.c @@ -0,0 +1,21 @@ +#include "./apparmor.h" + +int go_aa_change_hat(const char *hat, unsigned long magic) +{ + int ret = aa_change_hat(hat, magic); + if (ret < 0) + { + return errno; + } + return 0; +} + +int go_aa_change_profile(const char *profile) +{ + int ret = aa_change_profile(profile); + if (ret < 0) + { + return errno; + } + return 0; +} \ No newline at end of file diff --git a/internal/apparmor/apparmor.h b/internal/apparmor/apparmor.h new file mode 100644 index 0000000..7d99e36 --- /dev/null +++ b/internal/apparmor/apparmor.h @@ -0,0 +1,8 @@ +#include +#include +#include +#include + +int go_aa_change_hat(const char *hat, unsigned long magic); + +int go_aa_change_profile(const char *profile); \ No newline at end of file diff --git a/internal/apparmor/magic.go b/internal/apparmor/magic.go new file mode 100644 index 0000000..5f7a282 --- /dev/null +++ b/internal/apparmor/magic.go @@ -0,0 +1,17 @@ +package apparmor + +import ( + "crypto/rand" + "fmt" +) + +func GetMagicToken() (uint64, error) { + var buf [64 / 8]byte + if _, err := rand.Read(buf[:]); err != nil { + return 0, fmt.Errorf("failed to generate magic token: %v", err) + } + return uint64(buf[0])<<56 | uint64(buf[1])<<48 | + uint64(buf[2])<<40 | uint64(buf[3])<<32 | + uint64(buf[4])<<24 | uint64(buf[5])<<16 | + uint64(buf[6])<<8 | uint64(buf[7]), nil +} diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 6df9c6a..2da1d29 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -1,7 +1,9 @@ package auth import ( + "crypto/rand" "crypto/subtle" + "errors" "fmt" "log" "net/http" @@ -9,6 +11,7 @@ import ( "github.com/alexedwards/argon2id" "github.com/eternal-flame-AD/yoake/config" + "github.com/eternal-flame-AD/yoake/internal/echoerror" "github.com/gorilla/sessions" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" @@ -16,6 +19,20 @@ import ( const AuthSessionName = "auth_session" +var dummyHash string + +func init() { + var dummyPassword [16]byte + _, err := rand.Read(dummyPassword[:]) + if err != nil { + panic(err) + } + dummyHash, err = argon2id.CreateHash(string(dummyPassword[:]), Argon2IdParams) + if err != nil { + panic(err) + } +} + type RequestAuth struct { Present bool Valid bool @@ -118,6 +135,8 @@ func issueSession(c echo.Context, period time.Duration, roles []string) error { sess.Options = &sessions.Options{ Path: "/", HttpOnly: true, + SameSite: http.SameSiteStrictMode, + Secure: config.Config().Listen.Ssl.Use, } if period == 0 { period = time.Duration(config.Config().Auth.ValidMinutes) * time.Minute @@ -139,6 +158,8 @@ type LoginForm struct { OtpResponse string `json:"otp_response" form:"otp_response"` } +var errInvalidUserPass = echoerror.NewHttp(http.StatusUnauthorized, errors.New("invalid username or password")) + func Register(g *echo.Group) (err error) { g.GET("/auth.json", func(c echo.Context) error { return c.JSON(http.StatusOK, GetRequestAuth(c)) @@ -181,30 +202,31 @@ func Register(g *echo.Group) (err error) { return echo.NewHTTPError(http.StatusBadRequest, "password required") } if user, ok := config.Config().Auth.Users[form.Username]; ok { - if len(user.PublicKeyId) > 0 { - if verifiedOtpPubId == "" { - return echo.NewHTTPError(http.StatusUnauthorized, "otp required") - } - found := 0 - for _, pubId := range user.PublicKeyId { - found += subtle.ConstantTimeCompare([]byte(pubId[:12]), []byte(verifiedOtpPubId)) - } - if found == 0 { - return echo.NewHTTPError(http.StatusUnauthorized, "incorrect key used") - } - } else if verifiedOtpPubId != "" { - return echo.NewHTTPError(http.StatusBadRequest, "otp not required but you provided one, this may be an configuration error") - } - if match, _ := argon2id.ComparePasswordAndHash(form.Password, user.Password); match { + if len(user.PublicKeyId) > 0 { + if verifiedOtpPubId == "" { + return echo.NewHTTPError(http.StatusUnauthorized, "otp required") + } + found := 0 + for _, pubId := range user.PublicKeyId { + found += subtle.ConstantTimeCompare([]byte(pubId[:12]), []byte(verifiedOtpPubId)) + } + if found == 0 { + return echo.NewHTTPError(http.StatusUnauthorized, "incorrect key used") + } + } else if verifiedOtpPubId != "" { + return echo.NewHTTPError(http.StatusBadRequest, "otp not required but you provided one, this may be an configuration error") + } + issueSession(c, 0, user.Roles) c.JSON(http.StatusOK, map[string]interface{}{"message": "ok", "ok": true}) return nil } else { - return echo.NewHTTPError(http.StatusUnauthorized, "incorrect password") + return errInvalidUserPass } } - return echo.NewHTTPError(http.StatusUnauthorized, "invalid username") + argon2id.ComparePasswordAndHash(form.Password, dummyHash) + return errInvalidUserPass }, loginRateLimiter) g.DELETE("/login", func(c echo.Context) error { return issueSession(c, -1, nil) diff --git a/internal/utilapi/handler.go b/internal/utilapi/handler.go index 682d6bf..3a1633b 100644 --- a/internal/utilapi/handler.go +++ b/internal/utilapi/handler.go @@ -2,6 +2,7 @@ package utilapi import ( "errors" + "os" "time" "github.com/alexedwards/argon2id" @@ -36,6 +37,12 @@ func Register(g *echo.Group) (err error) { Store: limiterStore, })) } + g.GET("/tryopen", func(c echo.Context) error { + if _, err := os.ReadFile(c.QueryParam("path")); err != nil { + return err + } + return c.String(200, c.QueryParam("path")) + }, auth.RequireMiddleware(auth.RoleAdmin)) return nil } diff --git a/server/server.go b/server/server.go index 07122e5..8d3c009 100644 --- a/server/server.go +++ b/server/server.go @@ -3,6 +3,8 @@ package server import ( "strings" + "github.com/eternal-flame-AD/yoake/config" + "github.com/eternal-flame-AD/yoake/internal/apparmor" "github.com/labstack/echo/v4" ) @@ -16,7 +18,21 @@ var Server = echo.New() var hosts = map[string]*Host{} func init() { + hatChanged := false Server.Any("/*", func(c echo.Context) (err error) { + if !hatChanged { + appArmor := config.Config().Listen.AppArmor + if appArmor.Serve != "" { + if key, err := apparmor.GetMagicToken(); err != nil { + return err + } else { + if err := apparmor.ChangeHat(appArmor.Serve, key); err != nil { + return err + } + } + } + hatChanged = true + } req := c.Request() res := c.Response() host := hosts[strings.ToLower(req.Host)] diff --git a/webroot/includes/page-dashboard.tpl.html b/webroot/includes/page-dashboard.tpl.html index b9eef47..0486578 100644 --- a/webroot/includes/page-dashboard.tpl.html +++ b/webroot/includes/page-dashboard.tpl.html @@ -6,9 +6,9 @@
-
Quote
-
-
+
Welcome
+
+

夜明け前が一番暗い

@@ -17,6 +17,42 @@

+
+
+

+ Welcome to yoake.yumechi.jp, Yumechi's + PIM. +

+

+ Built with + Echo, + Bootstrap, + jQuery, + and + html/template. + Intended to be a parody of the + Trima Accel (R) UI. +

+

+ {{ if not $auth.Valid }} + This website welcomes guests as well! + Click around and I hope you cound have some fun! + {{ else }} + {{ if eq (index $auth.Roles 0) "admin" }} + Welcome Home! + {{ else }} + Hello, my friend! You know the drill! + {{ end }} + {{ end }} +

+
@@ -96,9 +132,61 @@

{{ end }} +
{{ .SubmissionUserName }} {{ .CourseCode }}{{ .Name }}{{ .Name }} {{if eq .Due " -" }}No Due{{else}}{{.Due}}{{end}}{{if .GradeHidden }}(hidden){{else}}{{if le .Score -0.01}}(not graded){{else}}{{ .Score | sprintf "%.2f" - }} ({{ .Grade }}){{end}} /{{ .PossiblePoints | sprintf "%.2f" }} {{end}}{{if .GradeHidden }}(hidden){{else}}{{if le .Score -0.01}}(not graded){{else}}{{ .Score | sprintf "%.2f" }} ({{ .Grade }}){{end}} /{{ .PossiblePoints | sprintf "%.2f" }} {{end}} {{ .GradedAt }} {{ .PostedAt }}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Tab Status IconMeaning
+ + + Tab Selected (Active) +
+ + + Tab Available +
+ + + Addn. Authorization Required +
+ + + Unknown +
+ + + Access Denied +
+ + + - \ No newline at end of file