Files
Vencloud/main.go
Lewis Crichton 50b3013f53 feat: anonymous telemetry
anonymous in this case meaning we can't trace it back to a particular
user

THIS IS UNTESTED AND HEAVILY WORK IN PROGRESS.
2023-10-26 23:00:39 +01:00

237 lines
6.5 KiB
Go

// Vencord Cloud, the API for the Vencord client mod
// Copyright (C) 2023 Vendicated and contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package main
import (
"context"
"encoding/base64"
"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"
"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"
)
func requireAuth(c *fiber.Ctx) error {
authToken := c.Get("Authorization")
if authToken == "" {
return c.Status(401).JSON(&fiber.Map{
"error": "Missing authorization",
})
}
// decode base64 token and split by:
// token[0] = secret
// token[1] = user id
token, err := base64.StdEncoding.DecodeString(authToken)
if err != nil {
return c.Status(401).JSON(&fiber.Map{
"error": "Invalid authorization",
})
}
tokenStr := string(token)
tokenSplit := strings.Split(tokenStr, ":")
if len(tokenSplit) != 2 {
return c.Status(401).JSON(&fiber.Map{
"error": "Invalid authorization",
})
}
secret := tokenSplit[0]
userId := tokenSplit[1]
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 := g.RDB.Get(c.Context(), "secrets:"+util.Hash(g.PEPPER_SECRETS+userId)).Result()
if err == redis.Nil {
return c.Status(401).JSON(&fiber.Map{
"error": "Invalid authorization",
})
} else if err != nil {
panic(err)
}
if storedSecret != secret {
return c.Status(401).JSON(&fiber.Map{
"error": "Invalid authorization",
})
}
c.Context().SetUserValue("userId", userId)
return c.Next()
}
func main() {
// environment
slRaw, _ := strconv.ParseInt(os.Getenv("SIZE_LIMIT"), 10, 0)
g.SIZE_LIMIT = int(slRaw)
auRaw := os.Getenv("ALLOWED_USERS")
if auRaw != "" {
g.ALLOWED_USERS = make(map[string]bool)
for _, userId := range strings.Split(auRaw, ",") {
g.ALLOWED_USERS[userId] = true
}
}
app := fiber.New(fiber.Config{
ProxyHeader: os.Getenv("PROXY_HEADER"),
})
g.RDB = redis.NewClient(&redis.Options{
Addr: g.REDIS_URI,
})
if os.Getenv("PROMETHEUS") == "true" {
promauto.NewGaugeFunc(prometheus.GaugeOpts{
Name: "vencord_accounts_registered",
Help: "The total number of accounts registered",
}, func() float64 {
iter := g.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)
})
promUpdate := time.NewTicker(1 * time.Minute)
activeUsers := promauto.NewGaugeVec(prometheus.GaugeOpts{
Name: "vencord_active_users",
Help: "The total number of active Vencord users",
}, []string{"version", "operating_system"})
pluginsInUse := promauto.NewGaugeVec(prometheus.GaugeOpts{
Name: "vencord_plugins_in_use",
Help: "The total number of current plugins in use by Vencord users",
}, []string{"name"})
go func() {
for {
<-promUpdate.C
iter := g.RDB.Scan(context.Background(), 0, "telemetry:*", 0).Iterator()
userStats := make(map[string]map[string]int64)
pluginStats := make(map[string]int64)
for iter.Next(context.Background()) {
telemetryData := g.RDB.HMGet(context.Background(), iter.Val(), "version", "plugins", "operatingSystem").Val()
version := telemetryData[0].(string)
plugins := telemetryData[1].([]string)
operatingSystem := telemetryData[2].(string)
if userStats[version] == nil {
userStats[version] = make(map[string]int64)
}
userStats[version][operatingSystem]++
for _, plugin := range plugins {
pluginStats[plugin]++
}
}
if err := iter.Err(); err != nil {
panic(err)
}
for version, operatingSystems := range userStats {
for operatingSystem, count := range operatingSystems {
activeUsers.WithLabelValues(version, operatingSystem).Set(float64(count))
}
}
for plugin, count := range pluginStats {
pluginsInUse.WithLabelValues(plugin).Set(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,https://discordapp.com,https://ptb.discordapp.com,https://canary.discordapp.com",
}))
app.Use(logger.New())
// #region settings
app.All("/v1/settings", requireAuth)
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", routes.GETOAuthCallback)
app.Get("/v1/oauth/settings", routes.GETOAuthSettings)
// #endregion
// #region erase all
app.Delete("/v1", requireAuth, routes.DELETE)
// #endregion
// #region telemetry
if os.Getenv("TELEMETRY") == "true" {
app.Post("/v1/telemetry", routes.POSTTelemetry)
}
// #endregion
app.Get("/v1", routes.GET)
app.Get("/", func(c *fiber.Ctx) error {
return c.Redirect(g.ROOT_REDIRECT, 303)
})
app.Listen(g.HOST + ":" + g.PORT)
}