From dae1b36bfa188643d66e93d65b53f12a2e44f3f7 Mon Sep 17 00:00:00 2001 From: Jacob Gunther Date: Sun, 17 Mar 2024 23:17:38 -0500 Subject: [PATCH] Connect MongoDB and require authentication --- config.example.yml | 1 + go.mod | 10 +++ go.sum | 51 +++++++++++- src/config.go | 24 +++--- src/main.go | 16 +++- src/mongo.go | 198 +++++++++++++++++++++++++++++++++++++++++++++ src/routes.go | 15 ++-- src/util.go | 83 +++++++++++++++++++ 8 files changed, 376 insertions(+), 22 deletions(-) create mode 100644 src/mongo.go diff --git a/config.example.yml b/config.example.yml index 4b64841..55c8b95 100644 --- a/config.example.yml +++ b/config.example.yml @@ -1,6 +1,7 @@ environment: development host: 0.0.0.0 port: 3001 +mongodb: ${MONGO_URL} # Use an environment variable to define the Redis URL redis: ${REDIS_URL} # Use an environment variable to define the Redis URL cache: enable_locks: true diff --git a/go.mod b/go.mod index 03fbb0e..c207f63 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/gofiber/fiber/v2 v2.52.2 github.com/mcstatus-io/mcutil/v3 v3.3.0 github.com/redis/go-redis/v9 v9.5.1 + go.mongodb.org/mongo-driver v1.14.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -15,6 +16,7 @@ require ( github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/go-redis/redis/v8 v8.11.5 // indirect + github.com/golang/snappy v0.0.4 // indirect github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect @@ -22,9 +24,17 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/montanaflynn/stats v0.7.1 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.52.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect + golang.org/x/crypto v0.21.0 // indirect + golang.org/x/sync v0.6.0 // indirect golang.org/x/sys v0.18.0 // indirect + golang.org/x/text v0.14.0 // indirect ) diff --git a/go.sum b/go.sum index 067c5a3..8c4e815 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,7 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= @@ -14,7 +15,10 @@ github.com/go-redsync/redsync/v4 v4.12.1 h1:hCtdZ45DJxMxNdPiby5GlQwOKQmcka2587Y4 github.com/go-redsync/redsync/v4 v4.12.1/go.mod h1:sn72ojgeEhxUuRjrliK0NRrB0Zl6kOZ3BDvNN3P2jAY= github.com/gofiber/fiber/v2 v2.52.2 h1:b0rYH6b06Df+4NyrbdptQL8ifuxw/Tf2DgfkZkDaxEo= github.com/gofiber/fiber/v2 v2.52.2/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws= +github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -33,6 +37,8 @@ github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZ github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mcstatus-io/mcutil/v3 v3.3.0 h1:FnI0hzmCtYGOlYARcdVTqNS+5iYqyo/t/U6YDpBmc8c= github.com/mcstatus-io/mcutil/v3 v3.3.0/go.mod h1:f1hgiUD3WoNmeZdN1AXYASSEO7yPxVEsLCGXnPkK6p4= +github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= +github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8= github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= github.com/redis/rueidis v1.0.19 h1:s65oWtotzlIFN8eMPhyYwxlwLR1lUdhza2KtWprKYSo= @@ -46,11 +52,54 @@ github.com/valyala/fasthttp v1.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7g github.com/valyala/fasthttp v1.52.0/go.mod h1:hf5C4QnVMkNXMspnsUlfM3WitlgYflyhHYoKol/szxQ= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= -golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk= +github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80= +go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +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= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/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.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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +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= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/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.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/src/config.go b/src/config.go index 8eda8f7..4c208e7 100644 --- a/src/config.go +++ b/src/config.go @@ -15,6 +15,7 @@ var ( Environment: "production", Host: "127.0.0.1", Port: 3001, + MongoDB: nil, Redis: nil, Cache: ConfigCache{ EnableLocks: true, @@ -22,18 +23,17 @@ var ( BedrockStatusDuration: time.Minute, IconDuration: time.Minute * 15, }, - AccessControl: ConfigAccessControl{}, } ) // Config represents the application configuration. type Config struct { - Environment string `yaml:"environment"` - Host string `yaml:"host"` - Port uint16 `yaml:"port"` - Redis *string `yaml:"redis"` - Cache ConfigCache `yaml:"cache"` - AccessControl ConfigAccessControl `yaml:"access_control"` + Environment string `yaml:"environment"` + Host string `yaml:"host"` + Port uint16 `yaml:"port"` + MongoDB *string `yaml:"mongodb"` + Redis *string `yaml:"redis"` + Cache ConfigCache `yaml:"cache"` } // ConfigCache represents the caching durations of various responses. @@ -44,12 +44,6 @@ type ConfigCache struct { IconDuration time.Duration `yaml:"icon_duration"` } -// ConfigAccessControl is the configuration for the CORS headers -type ConfigAccessControl struct { - Enable bool `yaml:"enable"` - AllowedOrigins []string `yaml:"allowed_origins"` -} - // ReadFile reads the configuration from the given file and overrides values using environment variables. func (c *Config) ReadFile(file string) error { data, err := os.ReadFile(file) @@ -99,5 +93,9 @@ func (c *Config) overrideWithEnvVars() error { c.Redis = &value } + if value := os.Getenv("MONGO_URL"); value != "" { + c.MongoDB = &value + } + return nil } diff --git a/src/main.go b/src/main.go index b3b055d..22cbf7e 100644 --- a/src/main.go +++ b/src/main.go @@ -25,9 +25,10 @@ var ( return ctx.SendStatus(http.StatusInternalServerError) }, }) - r *Redis = &Redis{} - config *Config = DefaultConfig - instanceID uint16 = 0 + r *Redis = &Redis{} + db *MongoDB = &MongoDB{} + config *Config = DefaultConfig + instanceID uint16 = 0 ) func init() { @@ -51,6 +52,14 @@ func init() { log.Println("Successfully retrieved EULA blocked servers") + if config.MongoDB != nil { + if err = db.Connect(); err != nil { + log.Fatalf("Failed to connect to MongoDB: %v", err) + } + + log.Println("Successfully connected to MongoDB") + } + if config.Redis != nil { if err = r.Connect(); err != nil { log.Fatalf("Failed to connect to Redis: %v", err) @@ -72,6 +81,7 @@ func init() { func main() { defer r.Close() + defer db.Close() if err := app.Listen(fmt.Sprintf("%s:%d", config.Host, config.Port+instanceID)); err != nil { panic(err) diff --git a/src/mongo.go b/src/mongo.go new file mode 100644 index 0000000..020e149 --- /dev/null +++ b/src/mongo.go @@ -0,0 +1,198 @@ +package main + +import ( + "context" + "errors" + "net/url" + "strings" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var ( + CollectionApplications string = "applications" + CollectionTokens string = "tokens" + CollectionRequestLog string = "request_log" + + ErrMongoNotConnected error = errors.New("cannot use method as MongoDB is not connected") +) + +type MongoDB struct { + Client *mongo.Client + Database *mongo.Database +} + +type Application struct { + ID string `bson:"_id" json:"id"` + Name string `bson:"name" json:"name"` + ShortDescription string `bson:"shortDescription" json:"shortDescription"` + User string `bson:"user" json:"user"` + Token string `bson:"token" json:"token"` + TotalRequests uint64 `bson:"totalRequests" json:"totalRequests"` + CreatedAt time.Time `bson:"createdAt" json:"createdAt"` +} + +type Token struct { + ID string `bson:"_id" json:"id"` + Name string `bson:"name" json:"name"` + Token string `bson:"token" json:"token"` + TotalRequests uint64 `bson:"totalRequests" json:"totalRequests"` + Application string `bson:"application" json:"application"` + CreatedAt time.Time `bson:"createdAt" json:"createdAt"` + LastUsedAt time.Time `bson:"lastUsedAt" json:"lastUsedAt"` +} + +func (c *MongoDB) Connect() error { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + + defer cancel() + + parsedURI, err := url.Parse(*config.MongoDB) + + if err != nil { + return err + } + + client, err := mongo.Connect(ctx, options.Client().ApplyURI(*config.MongoDB)) + + if err != nil { + return err + } + + c.Client = client + c.Database = client.Database(strings.TrimPrefix(parsedURI.Path, "/")) + + return nil +} + +func (c *MongoDB) GetTokenByToken(token string) (*Token, error) { + if c.Client == nil { + return nil, ErrMongoNotConnected + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + + defer cancel() + + cur := c.Database.Collection(CollectionTokens).FindOne(ctx, bson.M{"token": token}) + + if err := cur.Err(); err != nil { + if errors.Is(err, mongo.ErrNoDocuments) { + return nil, nil + } + + return nil, err + } + + var result Token + + if err := cur.Decode(&result); err != nil { + return nil, err + } + + return &result, nil +} + +func (c *MongoDB) GetApplicationByID(id string) (*Application, error) { + if c.Client == nil { + return nil, ErrMongoNotConnected + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + + defer cancel() + + cur := c.Database.Collection(CollectionApplications).FindOne(ctx, bson.M{"_id": id}) + + if err := cur.Err(); err != nil { + if errors.Is(err, mongo.ErrNoDocuments) { + return nil, nil + } + + return nil, err + } + + var result Application + + if err := cur.Decode(&result); err != nil { + return nil, err + } + + return &result, nil +} + +func (c *MongoDB) UpdateToken(id string, update bson.M) error { + if c.Client == nil { + return ErrMongoNotConnected + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + + defer cancel() + + _, err := c.Database.Collection(CollectionTokens).UpdateOne( + ctx, + bson.M{"_id": id}, + update, + ) + + return err +} + +func (c *MongoDB) IncrementApplicationRequestCount(id string) error { + if c.Client == nil { + return ErrMongoNotConnected + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + + defer cancel() + + _, err := c.Database.Collection(CollectionApplications).UpdateOne( + ctx, + bson.M{"_id": id}, + bson.M{ + "$inc": bson.M{ + "totalRequests": 1, + }, + }, + ) + + return err +} + +func (c *MongoDB) UpsertRequestLog(query, update bson.M) error { + if c.Client == nil { + return ErrMongoNotConnected + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + + defer cancel() + + _, err := c.Database.Collection(CollectionRequestLog).UpdateOne( + ctx, + query, + update, + &options.UpdateOptions{ + Upsert: PointerOf(true), + }, + ) + + return err +} + +func (c *MongoDB) Close() error { + if c.Client == nil { + return nil + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + + defer cancel() + + return c.Client.Disconnect(ctx) +} diff --git a/src/routes.go b/src/routes.go index 21df249..6c6cb1c 100644 --- a/src/routes.go +++ b/src/routes.go @@ -6,7 +6,6 @@ import ( "main/src/assets" "net/http" "strconv" - "strings" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/cors" @@ -26,15 +25,13 @@ func init() { Data: assets.Favicon, })) - if config.AccessControl.Enable { + if config.Environment == "development" { app.Use(cors.New(cors.Config{ - AllowOrigins: strings.Join(config.AccessControl.AllowedOrigins, ","), + AllowOrigins: "*", AllowMethods: "HEAD,OPTIONS,GET,POST", ExposeHeaders: "X-Cache-Hit,X-Cache-Time-Remaining", })) - } - if config.Environment == "development" { app.Use(logger.New(logger.Config{ Format: "${time} ${ip}:${port} -> ${status}: ${method} ${path} (${latency})\n", TimeFormat: "2006/01/02 15:04:05", @@ -68,6 +65,14 @@ func JavaStatusHandler(ctx *fiber.Ctx) error { return ctx.Status(http.StatusBadRequest).SendString("Invalid address value") } + authorized, err := Authenticate(ctx) + + // This check should work for both scenarios, because nil should be returned if the user + // is unauthorized, and err will be nil in that case. + if err != nil || !authorized { + return err + } + if err = r.Increment(fmt.Sprintf("java-hits:%s-%d", host, port)); err != nil { return err } diff --git a/src/util.go b/src/util.go index e26f76d..99f6a30 100644 --- a/src/util.go +++ b/src/util.go @@ -1,6 +1,7 @@ package main import ( + "crypto/rand" "crypto/sha1" _ "embed" "encoding/hex" @@ -19,6 +20,7 @@ import ( "time" "github.com/gofiber/fiber/v2" + "go.mongodb.org/mongo-driver/bson" ) var ( @@ -248,6 +250,7 @@ func GetVoteOptions(ctx *fiber.Ctx) (*VoteOptions, error) { return &result, nil } +// GetStatusOptions returns the options for status routes, with the default values filled in. func GetStatusOptions(ctx *fiber.Ctx) (*StatusOptions, error) { result := &StatusOptions{} @@ -292,6 +295,70 @@ func GetCacheKey(host string, port uint16, opts *StatusOptions) string { return SHA256(values.Encode()) } +// Authenticate checks and requires authentication for the current request, by finding the token. +func Authenticate(ctx *fiber.Ctx) (bool, error) { + if config.MongoDB == nil { + return true, nil + } + + authToken := ctx.Get("Authorization") + + if len(authToken) < 1 { + if err := ctx.Status(http.StatusUnauthorized).SendString("Missing 'Authorization' header in request"); err != nil { + return false, err + } + + return false, nil + } + + token, err := db.GetTokenByToken(authToken) + + if err != nil { + return false, err + } + + if token == nil { + if err := ctx.Status(http.StatusUnauthorized).SendString("Invalid or expired authorization token, please generate another one in the dashboard"); err != nil { + return false, err + } + + return false, nil + } + + if err = db.IncrementApplicationRequestCount(token.Application); err != nil { + return false, err + } + + if err = db.UpdateToken( + token.ID, + bson.M{ + "$inc": bson.M{"requestCount": 1}, + "$set": bson.M{"lastUsedAt": time.Now().UTC()}, + }, + ); err != nil { + return false, err + } + + if err = db.UpsertRequestLog( + bson.M{ + "application": token.Application, + "timestamp": GetStartOfHour(), + }, + bson.M{ + "$setOnInsert": bson.M{ + "_id": RandomHexString(16), + }, + "$inc": bson.M{ + "requestCount": 1, + }, + }, + ); err != nil { + return false, err + } + + return true, nil +} + // SHA256 returns the result of hashing the input value using SHA256 algorithm. func SHA256(input string) string { result := sha1.Sum([]byte(input)) @@ -325,3 +392,19 @@ func Map[I, O any](arr []I, f func(I) O) []O { return result } + +// GetStartOfHour returns the current date and time rounded down to the start of the hour. +func GetStartOfHour() time.Time { + return time.Now().UTC().Truncate(time.Hour) +} + +// RandomHexString returns a random hexadecimal string with the specified byte length. +func RandomHexString(byteLength int) string { + data := make([]byte, byteLength) + + if _, err := rand.Read(data); err != nil { + panic(err) + } + + return hex.EncodeToString(data) +}