diff --git a/src/routes.go b/src/routes.go index d302e3a..3a19eec 100644 --- a/src/routes.go +++ b/src/routes.go @@ -6,9 +6,11 @@ import ( "main/src/assets" "net/http" "strconv" + "time" "github.com/gofiber/fiber/v2" "github.com/mcstatus-io/mcutil" + "github.com/mcstatus-io/mcutil/options" ) func init() { @@ -18,6 +20,7 @@ func init() { app.Get("/status/bedrock/:address", BedrockStatusHandler) app.Get("/icon", DefaultIconHandler) app.Get("/icon/:address", IconHandler) + app.Post("/vote", SendVoteHandler) app.Get("/debug/java/:address", DebugJavaStatusHandler) app.Get("/debug/legacy/:address", DebugLegacyStatusHandler) app.Get("/debug/bedrock/:address", DebugBedrockStatusHandler) @@ -113,6 +116,35 @@ func IconHandler(ctx *fiber.Ctx) error { return ctx.Type("png").Send(icon) } +// SendVoteHandler allows sending of Votifier votes to the specified server. +func SendVoteHandler(ctx *fiber.Ctx) error { + opts, err := ParseVoteOptions(ctx) + + if err != nil { + return ctx.Status(http.StatusBadRequest).SendString(err.Error()) + } + + switch opts.Version { + case 1: + return ctx.Status(http.StatusNotImplemented).SendString("Votifier version 1 is currently not supported") + case 2: + { + if err = mcutil.SendVote(opts.Host, opts.Port, options.Vote{ + ServiceName: opts.ServiceName, + Username: opts.Username, + Token: opts.Token, + UUID: opts.UUID, + Timestamp: opts.Timestamp, + Timeout: time.Second * 5, + }); err != nil { + return ctx.Status(http.StatusBadRequest).SendString(err.Error()) + } + } + } + + return ctx.Status(http.StatusOK).SendString("The vote was successfully sent to the server") +} + // DefaultIconHandler returns the default server icon. func DefaultIconHandler(ctx *fiber.Ctx) error { return ctx.Type("png").Send(assets.DefaultIcon) diff --git a/src/util.go b/src/util.go index ca89a71..5816543 100644 --- a/src/util.go +++ b/src/util.go @@ -4,6 +4,7 @@ import ( "crypto/sha1" _ "embed" "encoding/hex" + "errors" "fmt" "io" "log" @@ -14,6 +15,9 @@ import ( "strconv" "strings" "sync" + "time" + + "github.com/gofiber/fiber/v2" ) var ( @@ -22,6 +26,18 @@ var ( ipAddressRegEx *regexp.Regexp = regexp.MustCompile(`^\d{1,3}(\.\d{1,3}){3}$`) ) +// VoteOptions is the options provided as query parameters to the vote route. +type VoteOptions struct { + Version int + Host string + Port uint16 + ServiceName string + Username string + UUID string + Token string + Timestamp time.Time +} + // MutexArray is a thread-safe array for storing and retrieving values. type MutexArray[T comparable] struct { List []T @@ -122,6 +138,87 @@ func ParseAddress(address string, defaultPort uint16) (string, uint16, error) { return host, uint16(port), nil } +// ParseVoteOptions parses the vote options from the provided query parameters. +func ParseVoteOptions(ctx *fiber.Ctx) (*VoteOptions, error) { + result := VoteOptions{} + + // Version + { + result.Version = ctx.QueryInt("version", 2) + + if result.Version < 0 || result.Version > 2 { + return nil, fmt.Errorf("invalid 'version' query parameter: %d", result.Version) + } + } + + // Host + { + result.Host = ctx.Query("host") + + if len(result.Host) < 1 { + return nil, errors.New("missing 'host' query parameter") + } + } + + // Port + { + result.Port = uint16(ctx.QueryInt("host", 8192)) + } + + // Service Name + { + result.ServiceName = ctx.Query("serviceName", "mcstatus.io") + + if len(result.ServiceName) < 1 { + return nil, fmt.Errorf("invalid 'serviceName' query parameter: %s", result.ServiceName) + } + } + + // Username + { + result.Username = ctx.Query("username") + + if len(result.Username) < 1 || len(result.Username) > 16 { + return nil, fmt.Errorf("invalid 'username' query parameter: %s", result.Username) + } + } + + // UUID + { + result.UUID = ctx.Query("uuid") + + // TODO check for properly formatted UUID + } + + // Token + { + result.Token = ctx.Query("token") + + if len(result.Token) < 1 { + return nil, fmt.Errorf("invalid 'token' query parameter: %s", result.Token) + } + } + + // Timestamp + { + value := ctx.Query("timestamp") + + if len(value) > 0 { + parsedTime, err := time.Parse(time.RFC3339, value) + + if err != nil { + return nil, fmt.Errorf("invalid 'timestamp' query parameter: %s", result.Token) + } + + result.Timestamp = parsedTime + } else { + result.Timestamp = time.Now() + } + } + + return &result, nil +} + // GetInstanceID returns the INSTANCE_ID environment variable parsed as an unsigned 16-bit integer. func GetInstanceID() (uint16, error) { if instanceID := os.Getenv("INSTANCE_ID"); len(instanceID) > 0 {