From c85af2641b55dcb91b07f7624aa118e4a54869bb Mon Sep 17 00:00:00 2001 From: Lewis Crichton Date: Mon, 7 Aug 2023 21:52:57 +0100 Subject: [PATCH] style: code maid is here! refactored the project significantly to reduce the single-file monolith --- globals/globals.go | 34 ++++++ go.mod | 2 +- main.go | 291 +++++++-------------------------------------- routes/discord.go | 115 ++++++++++++++++++ routes/root.go | 25 ++++ routes/settings.go | 93 +++++++++++++++ util/util.go | 10 ++ 7 files changed, 322 insertions(+), 248 deletions(-) create mode 100644 globals/globals.go create mode 100644 routes/discord.go create mode 100644 routes/root.go create mode 100644 routes/settings.go create mode 100644 util/util.go diff --git a/globals/globals.go b/globals/globals.go new file mode 100644 index 0000000..3fe9a0c --- /dev/null +++ b/globals/globals.go @@ -0,0 +1,34 @@ +package globals + +import ( + "os" + + "github.com/redis/go-redis/v9" +) + +// environment variables +var ( + HOST = os.Getenv("HOST") + PORT = os.Getenv("PORT") + + REDIS_URI = os.Getenv("REDIS_URI") + + ROOT_REDIRECT = os.Getenv("ROOT_REDIRECT") + + DISCORD_CLIENT_ID = os.Getenv("DISCORD_CLIENT_ID") + DISCORD_CLIENT_SECRET = os.Getenv("DISCORD_CLIENT_SECRET") + DISCORD_REDIRECT_URI = os.Getenv("DISCORD_REDIRECT_URI") + + PEPPER_SETTINGS = os.Getenv("PEPPER_SETTINGS") + PEPPER_SECRETS = os.Getenv("PEPPER_SECRETS") + + SIZE_LIMIT int // initialised in main + + ALLOWED_USERS map[string]bool // initialised in main +) + +// other app globals, initialised in main +var ( + // redis client + RDB *redis.Client +) diff --git a/go.mod b/go.mod index 4f18edf..9e32d79 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/Vencord/Backend +module github.com/vencord/backend go 1.20 diff --git a/main.go b/main.go index b132c11..eb46ee9 100644 --- a/main.go +++ b/main.go @@ -18,60 +18,25 @@ package main import ( "context" - "crypto/rand" - "crypto/sha1" "encoding/base64" - "encoding/hex" - "fmt" "os" "strconv" "strings" - "time" "github.com/ansrivas/fiberprometheus/v2" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/cors" "github.com/gofiber/fiber/v2/middleware/logger" - reqHttp "github.com/imroc/req/v3" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "github.com/redis/go-redis/v9" + + g "github.com/vencord/backend/globals" + "github.com/vencord/backend/routes" + "github.com/vencord/backend/util" ) -type DiscordAccessTokenResult struct { - AccessToken string `json:"access_token"` -} - -type DiscordUserResult struct { - Id string `json:"id"` -} - -var ALLOWED_USERS map[string]bool - var rdb *redis.Client -var ( - accountsRegistered = promauto.NewGaugeFunc(prometheus.GaugeOpts{ - Name: "vencord_accounts_registered", - Help: "The total number of accounts registered", - }, func() float64 { - iter := rdb.Scan(context.Background(), 0, "secrets:*", 0).Iterator() - var count int64 - - for iter.Next(context.Background()) { - count++ - } - - if err := iter.Err(); err != nil { - panic(err) - } - - return float64(count) - }) -) - -func hash(s string) string { - return fmt.Sprintf("%x", sha1.Sum([]byte(s))) -} func requireAuth(c *fiber.Ctx) error { authToken := c.Get("Authorization") @@ -105,13 +70,13 @@ func requireAuth(c *fiber.Ctx) error { secret := tokenSplit[0] userId := tokenSplit[1] - if ALLOWED_USERS != nil && c.Path() != "/v1" && c.Method() != "DELETE" && !ALLOWED_USERS[userId] { + if g.ALLOWED_USERS != nil && c.Path() != "/v1" && c.Method() != "DELETE" && !g.ALLOWED_USERS[userId] { return c.Status(403).JSON(&fiber.Map{ "error": "User is not whitelisted", }) } - storedSecret, err := rdb.Get(c.Context(), "secrets:"+hash(os.Getenv("PEPPER_SECRETS")+userId)).Result() + storedSecret, err := rdb.Get(c.Context(), "secrets:"+util.Hash(g.PEPPER_SECRETS+userId)).Result() if err == redis.Nil { return c.Status(401).JSON(&fiber.Map{ @@ -134,243 +99,75 @@ func requireAuth(c *fiber.Ctx) error { func main() { // environment - HOST := os.Getenv("HOST") - PORT := os.Getenv("PORT") - REDIS_URI := os.Getenv("REDIS_URI") - ROOT_REDIRECT := os.Getenv("ROOT_REDIRECT") - - DISCORD_CLIENT_ID := os.Getenv("DISCORD_CLIENT_ID") - DISCORD_CLIENT_SECRET := os.Getenv("DISCORD_CLIENT_SECRET") - DISCORD_REDIRECT_URI := os.Getenv("DISCORD_REDIRECT_URI") - - PEPPER_SECRETS := os.Getenv("PEPPER_SECRETS") - PEPPER_SETTINGS := os.Getenv("PEPPER_SETTINGS") - slRaw, _ := strconv.ParseInt(os.Getenv("SIZE_LIMIT"), 10, 0) - SIZE_LIMIT := int(slRaw) + g.SIZE_LIMIT = int(slRaw) auRaw := os.Getenv("ALLOWED_USERS") if auRaw != "" { - ALLOWED_USERS = make(map[string]bool) + g.ALLOWED_USERS = make(map[string]bool) for _, userId := range strings.Split(auRaw, ",") { - ALLOWED_USERS[userId] = true + g.ALLOWED_USERS[userId] = true } } app := fiber.New() rdb = redis.NewClient(&redis.Options{ - Addr: REDIS_URI, + Addr: g.REDIS_URI, }) - req := reqHttp.C() - if os.Getenv("PROMETHEUS") == "true" { - prometheus := fiberprometheus.New("vencord") - prometheus.RegisterAt(app, "/metrics") - app.Use(prometheus.Middleware) - } + if os.Getenv("PROMETHEUS") == "true" { + promauto.NewGaugeFunc(prometheus.GaugeOpts{ + Name: "vencord_accounts_registered", + Help: "The total number of accounts registered", + }, func() float64 { + iter := rdb.Scan(context.Background(), 0, "secrets:*", 0).Iterator() + var count int64 + + for iter.Next(context.Background()) { + count++ + } + + if err := iter.Err(); err != nil { + panic(err) + } + + return float64(count) + }) + + prometheus := fiberprometheus.New("vencord") + prometheus.RegisterAt(app, "/metrics") + app.Use(prometheus.Middleware) + } app.Use(cors.New(cors.Config{ ExposeHeaders: "ETag", - AllowOrigins: "https://discord.com,https://ptb.discord.com,https://canary.discord.com", + AllowOrigins: "https://discord.com,https://ptb.discord.com,https://canary.discord.com", })) app.Use(logger.New()) // #region settings app.All("/v1/settings", requireAuth) - app.Head("/v1/settings", func(c *fiber.Ctx) error { - userId := c.Context().UserValue("userId").(string) - - written, err := rdb.HGet(c.Context(), "settings:"+hash(PEPPER_SETTINGS+userId), "written").Result() - - if err == redis.Nil { - return c.Status(404).Send(nil) - } else if err != nil { - panic(err) - } - - c.Set("ETag", written) - return c.SendStatus(204) - }) - - app.Get("/v1/settings", func(c *fiber.Ctx) error { - userId := c.Context().UserValue("userId").(string) - - settings, err := rdb.HMGet(c.Context(), "settings:"+hash(PEPPER_SETTINGS+userId), "value", "written").Result() - - // we shouldn't expect an error here, HMGet doesn't return one - if err != nil { - panic(err) - } - - if settings[0] == nil { - return c.Status(404).Send(nil) - } - - // value is compressed data, written is a timestamp - value, written := []byte(settings[0].(string)), settings[1].(string) - - if ifm := c.Get("if-none-match"); ifm == written { - return c.SendStatus(304) - } - - c.Set("Content-Type", "application/octet-stream") - c.Set("ETag", written) - return c.Send(value) - }) - - app.Put("/v1/settings", func(c *fiber.Ctx) error { - if c.Get("Content-Type") != "application/octet-stream" { - return c.Status(415).JSON(&fiber.Map{ - "error": "Content type must be application/octet-stream", - }) - } - - if len(c.Body()) > SIZE_LIMIT { - return c.Status(413).JSON(&fiber.Map{ - "error": "Settings are too large", - }) - } - - userId := c.Context().UserValue("userId").(string) - - now := time.Now().UnixMilli() - - _, err := rdb.HSet(c.Context(), "settings:"+hash(PEPPER_SETTINGS+userId), map[string]interface{}{ - "value": c.Body(), - "written": now, - }).Result() - - if err != nil { - panic(err) - } - - return c.JSON(&fiber.Map{ - "written": now, - }) - }) - - app.Delete("/v1/settings", func(c *fiber.Ctx) error { - userId := c.Context().UserValue("userId").(string) - - rdb.Del(c.Context(), "settings:"+hash(PEPPER_SETTINGS+userId)) - - return c.SendStatus(204) - }) + app.Head("/v1/settings", routes.HEADSettings) + app.Get("/v1/settings", routes.GETSettings) + app.Put("/v1/settings", routes.PUTSettings) + app.Delete("/v1/settings", routes.DELETESettings) // #endregion // #region discord oauth - app.Get("/v1/oauth/callback", func(c *fiber.Ctx) error { - code := c.Query("code") - - if code == "" { - return c.Status(400).JSON(&fiber.Map{ - "error": "Missing code", - }) - } - - var accessTokenResult DiscordAccessTokenResult - - res, err := req.R().SetFormData(map[string]string{ - "client_id": DISCORD_CLIENT_ID, - "client_secret": DISCORD_CLIENT_SECRET, - "grant_type": "authorization_code", - "code": code, - "redirect_uri": DISCORD_REDIRECT_URI, - "scope": "identify", - }).SetSuccessResult(&accessTokenResult).Post("https://discord.com/api/oauth2/token") - - if err != nil { - return c.Status(500).JSON(&fiber.Map{ - "error": "Failed to request access token", - }) - } - - if res.IsErrorState() { - return c.Status(400).JSON(&fiber.Map{ - "error": "Invalid code", - }) - } - - accessToken := accessTokenResult.AccessToken - - var userResult DiscordUserResult - - res, err = req.R().SetHeaders(map[string]string{ - "Authorization": "Bearer " + accessToken, - }).SetSuccessResult(&userResult).Get("https://discord.com/api/users/@me") - - if err != nil { - return c.Status(500).JSON(&fiber.Map{ - "error": "Failed to request user", - }) - } - - if res.IsErrorState() { - return c.Status(500).JSON(&fiber.Map{ - "error": "Failed to request user", - }) - } - - userId := userResult.Id - - if ALLOWED_USERS != nil && !ALLOWED_USERS[userId] { - return c.Status(403).JSON(&fiber.Map{ - "error": "User is not whitelisted", - }) - } - - secret, err := rdb.Get(c.Context(), "secrets:"+hash(PEPPER_SECRETS+userId)).Result() - - if err == redis.Nil { - key := make([]byte, 48) - - _, err := rand.Read(key) - if err != nil { - return c.Status(500).JSON(&fiber.Map{ - "error": "Failed to generate secret", - }) - } - - secret = hex.EncodeToString(key) - rdb.Set(c.Context(), "secrets:"+hash(PEPPER_SECRETS+userId), secret, 0) - } else if err != nil { - panic(err) - } - - return c.JSON(&fiber.Map{ - "secret": secret, - }) - }) - - app.Get("/v1/oauth/settings", func(c *fiber.Ctx) error { - return c.JSON(&fiber.Map{ - "clientId": DISCORD_CLIENT_ID, - "redirectUri": DISCORD_REDIRECT_URI, - }) - }) + app.Get("/v1/oauth/callback", routes.GETOAuthCallback) + app.Get("/v1/oauth/settings", routes.GETOAuthSettings) // #endregion // #region erase all - app.Delete("/v1", requireAuth, func(c *fiber.Ctx) error { - userId := c.Context().UserValue("userId").(string) - - rdb.Del(c.Context(), "settings:"+hash(PEPPER_SETTINGS+userId)) - rdb.Del(c.Context(), "secrets:"+hash(PEPPER_SECRETS+userId)) - - return c.SendStatus(204) - }) + app.Delete("/v1", requireAuth, routes.DELETE) // #endregion - app.Get("/v1", func(c *fiber.Ctx) error { - return c.JSON(&fiber.Map{ - "ping": "pong", - }) - }) + app.Get("/v1", routes.GET) app.Get("/", func(c *fiber.Ctx) error { - return c.Redirect(ROOT_REDIRECT, 303) + return c.Redirect(g.ROOT_REDIRECT, 303) }) - app.Listen(HOST + ":" + PORT) + app.Listen(g.HOST + ":" + g.PORT) } diff --git a/routes/discord.go b/routes/discord.go new file mode 100644 index 0000000..608d206 --- /dev/null +++ b/routes/discord.go @@ -0,0 +1,115 @@ +package routes + +import ( + "crypto/rand" + "encoding/hex" + + "github.com/gofiber/fiber/v2" + "github.com/imroc/req/v3" + "github.com/redis/go-redis/v9" + + g "github.com/vencord/backend/globals" + "github.com/vencord/backend/util" +) + +// /v1/oauth + +type DiscordAccessTokenResult struct { + AccessToken string `json:"access_token"` +} + +type DiscordUserResult struct { + Id string `json:"id"` +} + +// /v1/oauth/callback +func GETOAuthCallback(c *fiber.Ctx) error { + code := c.Query("code") + + if code == "" { + return c.Status(400).JSON(&fiber.Map{ + "error": "Missing code", + }) + } + + var accessTokenResult DiscordAccessTokenResult + + res, err := req.R().SetFormData(map[string]string{ + "client_id": g.DISCORD_CLIENT_ID, + "client_secret": g.DISCORD_CLIENT_SECRET, + "grant_type": "authorization_code", + "code": code, + "redirect_uri": g.DISCORD_REDIRECT_URI, + "scope": "identify", + }).SetSuccessResult(&accessTokenResult).Post("https://discord.com/api/oauth2/token") + + if err != nil { + return c.Status(500).JSON(&fiber.Map{ + "error": "Failed to request access token", + }) + } + + if res.IsErrorState() { + return c.Status(400).JSON(&fiber.Map{ + "error": "Invalid code", + }) + } + + accessToken := accessTokenResult.AccessToken + + var userResult DiscordUserResult + + res, err = req.R().SetHeaders(map[string]string{ + "Authorization": "Bearer " + accessToken, + }).SetSuccessResult(&userResult).Get("https://discord.com/api/users/@me") + + if err != nil { + return c.Status(500).JSON(&fiber.Map{ + "error": "Failed to request user", + }) + } + + if res.IsErrorState() { + return c.Status(500).JSON(&fiber.Map{ + "error": "Failed to request user", + }) + } + + userId := userResult.Id + + if g.ALLOWED_USERS != nil && !g.ALLOWED_USERS[userId] { + return c.Status(403).JSON(&fiber.Map{ + "error": "User is not whitelisted", + }) + } + + secret, err := g.RDB.Get(c.Context(), "secrets:"+util.Hash(g.PEPPER_SECRETS+userId)).Result() + + if err == redis.Nil { + key := make([]byte, 48) + + _, err := rand.Read(key) + if err != nil { + return c.Status(500).JSON(&fiber.Map{ + "error": "Failed to generate secret", + }) + } + + secret = hex.EncodeToString(key) + g.RDB.Set(c.Context(), "secrets:"+util.Hash(g.PEPPER_SECRETS+userId), secret, 0) + } else if err != nil { + panic(err) + } + + return c.JSON(&fiber.Map{ + "secret": secret, + }) +} + +// /v1/oauth/settings +func GETOAuthSettings(c *fiber.Ctx) error { + return c.JSON(&fiber.Map{ + "clientId": g.DISCORD_CLIENT_ID, + "redirectUri": g.DISCORD_REDIRECT_URI, + }) +} diff --git a/routes/root.go b/routes/root.go new file mode 100644 index 0000000..fdae2a3 --- /dev/null +++ b/routes/root.go @@ -0,0 +1,25 @@ +package routes + +import ( + "github.com/gofiber/fiber/v2" + + g "github.com/vencord/backend/globals" + "github.com/vencord/backend/util" +) + +// /v1 + +func DELETE(c *fiber.Ctx) error { + userId := c.Context().UserValue("userId").(string) + + g.RDB.Del(c.Context(), "settings:"+util.Hash(g.PEPPER_SETTINGS+userId)) + g.RDB.Del(c.Context(), "secrets:"+util.Hash(g.PEPPER_SECRETS+userId)) + + return c.SendStatus(204) +} + +func GET(c *fiber.Ctx) error { + return c.JSON(&fiber.Map{ + "ping": "pong", + }) +} diff --git a/routes/settings.go b/routes/settings.go new file mode 100644 index 0000000..f2ca2a0 --- /dev/null +++ b/routes/settings.go @@ -0,0 +1,93 @@ +package routes + +import ( + "time" + + "github.com/gofiber/fiber/v2" + "github.com/redis/go-redis/v9" + + g "github.com/vencord/backend/globals" + "github.com/vencord/backend/util" +) + +// /v1/settings + +func HEADSettings(c *fiber.Ctx) error { + userId := c.Context().UserValue("userId").(string) + + written, err := g.RDB.HGet(c.Context(), "settings:"+util.Hash(g.PEPPER_SETTINGS+userId), "written").Result() + + if err == redis.Nil { + return c.Status(404).Send(nil) + } else if err != nil { + panic(err) + } + + c.Set("ETag", written) + return c.SendStatus(204) +} + +func GETSettings(c *fiber.Ctx) error { + userId := c.Context().UserValue("userId").(string) + + settings, err := g.RDB.HMGet(c.Context(), "settings:"+util.Hash(g.PEPPER_SETTINGS+userId), "value", "written").Result() + + // we shouldn't expect an error here, HMGet doesn't return one + if err != nil { + panic(err) + } + + if settings[0] == nil { + return c.Status(404).Send(nil) + } + + // value is compressed data, written is a timestamp + value, written := []byte(settings[0].(string)), settings[1].(string) + + if ifm := c.Get("if-none-match"); ifm == written { + return c.SendStatus(304) + } + + c.Set("Content-Type", "application/octet-stream") + c.Set("ETag", written) + return c.Send(value) +} + +func PUTSettings(c *fiber.Ctx) error { + if c.Get("Content-Type") != "application/octet-stream" { + return c.Status(415).JSON(&fiber.Map{ + "error": "Content type must be application/octet-stream", + }) + } + + if len(c.Body()) > g.SIZE_LIMIT { + return c.Status(413).JSON(&fiber.Map{ + "error": "Settings are too large", + }) + } + + userId := c.Context().UserValue("userId").(string) + + now := time.Now().UnixMilli() + + _, err := g.RDB.HSet(c.Context(), "settings:"+util.Hash(g.PEPPER_SETTINGS+userId), map[string]interface{}{ + "value": c.Body(), + "written": now, + }).Result() + + if err != nil { + panic(err) + } + + return c.JSON(&fiber.Map{ + "written": now, + }) +} + +func DELETESettings(c *fiber.Ctx) error { + userId := c.Context().UserValue("userId").(string) + + g.RDB.Del(c.Context(), "settings:"+util.Hash(g.PEPPER_SETTINGS+userId)) + + return c.SendStatus(204) +} diff --git a/util/util.go b/util/util.go new file mode 100644 index 0000000..90c48c7 --- /dev/null +++ b/util/util.go @@ -0,0 +1,10 @@ +package util + +import ( + "crypto/sha1" + "fmt" +) + +func Hash(s string) string { + return fmt.Sprintf("%x", sha1.Sum([]byte(s))) +}