diff --git a/README.md b/README.md index 2543689..7a79270 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ The REST server that powers the API for mcstatus.io. This repository is open sou - [Git](https://git-scm.com/) - [Go](https://go.dev/) -- [Redis](https://redis.io/) (*optional with caching disabled*) +- [Redis](https://redis.io/) - [GNU Make](https://www.gnu.org/software/make/) (*optional*) ## Installation diff --git a/config.example.yml b/config.example.yml index 9b4e66e..cabf2d0 100644 --- a/config.example.yml +++ b/config.example.yml @@ -1,9 +1,12 @@ environment: production host: 127.0.0.1 port: 3001 -redis: redis://127.0.0.1:6379/0 -cache: - enable: false +redis: + host: 127.0.0.1 + port: 6379 + # username: myuser + # password: mypassword + database: 0 java_cache_duration: 1m bedrock_cache_duration: 1m icon_cache_duration: 15m \ No newline at end of file diff --git a/src/blocked.go b/src/blocked.go new file mode 100644 index 0000000..897a166 --- /dev/null +++ b/src/blocked.go @@ -0,0 +1,48 @@ +package main + +import ( + "fmt" + "io" + "log" + "net/http" + "strings" + "time" +) + +func StartBlockedServersGoroutine() { + for { + if err := GetBlockedServerList(); err != nil { + log.Println(err) + } + + time.Sleep(time.Hour) + } +} + +func GetBlockedServerList() error { + resp, err := http.Get("https://sessionserver.mojang.com/blockedservers") + + if err != nil { + return err + } + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + + if err != nil { + return err + } + + for _, hash := range strings.Split(string(body), "\n") { + if err = r.Set(fmt.Sprintf("blocked:%s", hash), "true", time.Hour*24); err != nil { + return err + } + } + + return nil +} diff --git a/src/config.go b/src/config.go index 52502e2..a2d7cb6 100644 --- a/src/config.go +++ b/src/config.go @@ -11,13 +11,16 @@ type Config struct { Environment string `yaml:"environment"` Host string `yaml:"host"` Port uint16 `yaml:"port"` - Redis string `yaml:"redis"` - Cache struct { - Enable bool `yaml:"enable"` + Redis struct { + Host string `yaml:"host"` + Port uint16 `yaml:"port"` + Username string `yaml:"username"` + Password string `yaml:"password"` + Database int `yaml:"database"` JavaCacheDuration time.Duration `yaml:"java_cache_duration"` BedrockCacheDuration time.Duration `yaml:"bedrock_cache_duration"` IconCacheDuration time.Duration `yaml:"icon_cache_duration"` - } `yaml:"cache"` + } `yaml:"redis"` } func (c *Config) ReadFile(file string) error { diff --git a/src/main.go b/src/main.go index 33a8740..8d2a10e 100644 --- a/src/main.go +++ b/src/main.go @@ -29,19 +29,11 @@ func init() { log.Fatal(err) } - if config.Cache.Enable { - if err := r.Connect(config.Redis); err != nil { - log.Fatal(err) - } - - log.Println("Successfully connected to Redis") - } - - if err := GetBlockedServerList(); err != nil { + if err := r.Connect(); err != nil { log.Fatal(err) } - log.Println("Successfully retrieved EULA blocked servers") + log.Println("Successfully connected to Redis") app.Use(recover.New()) @@ -62,6 +54,8 @@ func init() { func main() { defer r.Close() + go StartBlockedServersGoroutine() + log.Printf("Listening on %s:%d\n", config.Host, config.Port) log.Fatal(app.Listen(fmt.Sprintf("%s:%d", config.Host, config.Port))) } diff --git a/src/redis.go b/src/redis.go index 7a51fb0..1fdd66e 100644 --- a/src/redis.go +++ b/src/redis.go @@ -2,6 +2,7 @@ package main import ( "context" + "fmt" "time" "github.com/go-redis/redis/v8" @@ -11,14 +12,13 @@ type Redis struct { Client *redis.Client } -func (r *Redis) Connect(uri string) error { - opts, err := redis.ParseURL(uri) - - if err != nil { - return err - } - - r.Client = redis.NewClient(opts) +func (r *Redis) Connect() error { + r.Client = redis.NewClient(&redis.Options{ + Addr: fmt.Sprintf("%s:%d", config.Redis.Host, config.Redis.Port), + Username: config.Redis.Username, + Password: config.Redis.Password, + DB: config.Redis.Database, + }) ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) diff --git a/src/routes.go b/src/routes.go index a6abf91..3cd34ac 100644 --- a/src/routes.go +++ b/src/routes.go @@ -90,7 +90,7 @@ func IconHandler(ctx *fiber.Ctx) error { } func DefaultIconHandler(ctx *fiber.Ctx) error { - return ctx.Type("png").Send(defaultIconBytes) + return ctx.Type("png").Send(defaultIcon) } func NotFoundHandler(ctx *fiber.Ctx) error { diff --git a/src/status.go b/src/status.go index 5fb78b3..9052455 100644 --- a/src/status.go +++ b/src/status.go @@ -114,7 +114,11 @@ func GetJavaStatus(host string, port uint16) (*JavaStatusResponse, *time.Duratio return &response, &ttl, err } - response := FetchJavaStatus(host, port, config.Cache.JavaCacheDuration) + response, err := FetchJavaStatus(host, port) + + if err != nil { + return nil, nil, err + } data, err := json.Marshal(response) @@ -122,7 +126,7 @@ func GetJavaStatus(host string, port uint16) (*JavaStatusResponse, *time.Duratio return nil, nil, err } - if err := r.Set(cacheKey, data, config.Cache.JavaCacheDuration); err != nil { + if err := r.Set(cacheKey, data, config.Redis.JavaCacheDuration); err != nil { return nil, nil, err } @@ -156,7 +160,11 @@ func GetBedrockStatus(host string, port uint16) (*BedrockStatusResponse, *time.D return &response, &ttl, err } - response := FetchBedrockStatus(host, port, config.Cache.BedrockCacheDuration) + response, err := FetchBedrockStatus(host, port) + + if err != nil { + return nil, nil, err + } data, err := json.Marshal(response) @@ -164,7 +172,7 @@ func GetBedrockStatus(host string, port uint16) (*BedrockStatusResponse, *time.D return nil, nil, err } - if err := r.Set(cacheKey, data, config.Cache.BedrockCacheDuration); err != nil { + if err := r.Set(cacheKey, data, config.Redis.BedrockCacheDuration); err != nil { return nil, nil, err } @@ -192,7 +200,7 @@ func GetServerIcon(host string, port uint16) ([]byte, *time.Duration, error) { return data, &ttl, err } - icon := defaultIconBytes + icon := defaultIcon status, err := mcutil.Status(host, port) @@ -206,14 +214,20 @@ func GetServerIcon(host string, port uint16) ([]byte, *time.Duration, error) { icon = data } - if err := r.Set(cacheKey, icon, config.Cache.IconCacheDuration); err != nil { + if err := r.Set(cacheKey, icon, config.Redis.IconCacheDuration); err != nil { return nil, nil, err } return icon, nil, nil } -func FetchJavaStatus(host string, port uint16, ttl time.Duration) *JavaStatusResponse { +func FetchJavaStatus(host string, port uint16) (*JavaStatusResponse, error) { + isEULABlocked, err := IsBlockedAddress(host) + + if err != nil { + return nil, err + } + status, err := mcutil.Status(host, port) if err != nil { @@ -225,11 +239,11 @@ func FetchJavaStatus(host string, port uint16, ttl time.Duration) *JavaStatusRes Online: false, Host: host, Port: port, - EULABlocked: IsBlockedAddress(host), + EULABlocked: isEULABlocked, RetrievedAt: time.Now().UnixMilli(), - ExpiresAt: time.Now().Add(ttl).UnixMilli(), + ExpiresAt: time.Now().Add(config.Redis.JavaCacheDuration).UnixMilli(), }, - } + }, nil } response := &JavaStatusResponse{ @@ -237,9 +251,9 @@ func FetchJavaStatus(host string, port uint16, ttl time.Duration) *JavaStatusRes Online: true, Host: host, Port: port, - EULABlocked: IsBlockedAddress(host), + EULABlocked: isEULABlocked, RetrievedAt: time.Now().UnixMilli(), - ExpiresAt: time.Now().Add(ttl).UnixMilli(), + ExpiresAt: time.Now().Add(config.Redis.JavaCacheDuration).UnixMilli(), }, JavaStatus: &JavaStatus{ Version: nil, @@ -267,7 +281,7 @@ func FetchJavaStatus(host string, port uint16, ttl time.Duration) *JavaStatusRes } } - return response + return response, nil } playerList := make([]Player, 0) @@ -299,9 +313,9 @@ func FetchJavaStatus(host string, port uint16, ttl time.Duration) *JavaStatusRes Online: true, Host: host, Port: port, - EULABlocked: IsBlockedAddress(host), + EULABlocked: isEULABlocked, RetrievedAt: time.Now().UnixMilli(), - ExpiresAt: time.Now().Add(ttl).UnixMilli(), + ExpiresAt: time.Now().Add(config.Redis.JavaCacheDuration).UnixMilli(), }, JavaStatus: &JavaStatus{ Version: &JavaVersion{ @@ -323,10 +337,16 @@ func FetchJavaStatus(host string, port uint16, ttl time.Duration) *JavaStatusRes Icon: status.Favicon, Mods: modList, }, - } + }, nil } -func FetchBedrockStatus(host string, port uint16, ttl time.Duration) *BedrockStatusResponse { +func FetchBedrockStatus(host string, port uint16) (*BedrockStatusResponse, error) { + isEULABlocked, err := IsBlockedAddress(host) + + if err != nil { + return nil, err + } + status, err := mcutil.StatusBedrock(host, port) if err != nil { @@ -335,11 +355,11 @@ func FetchBedrockStatus(host string, port uint16, ttl time.Duration) *BedrockSta Online: false, Host: host, Port: port, - EULABlocked: IsBlockedAddress(host), + EULABlocked: isEULABlocked, RetrievedAt: time.Now().UnixMilli(), - ExpiresAt: time.Now().Add(ttl).UnixMilli(), + ExpiresAt: time.Now().Add(config.Redis.BedrockCacheDuration).UnixMilli(), }, - } + }, nil } response := &BedrockStatusResponse{ @@ -347,9 +367,9 @@ func FetchBedrockStatus(host string, port uint16, ttl time.Duration) *BedrockSta Online: true, Host: host, Port: port, - EULABlocked: IsBlockedAddress(host), + EULABlocked: isEULABlocked, RetrievedAt: time.Now().UnixMilli(), - ExpiresAt: time.Now().Add(ttl).UnixMilli(), + ExpiresAt: time.Now().Add(config.Redis.BedrockCacheDuration).UnixMilli(), }, BedrockStatus: &BedrockStatus{ Version: nil, @@ -413,5 +433,5 @@ func FetchBedrockStatus(host string, port uint16, ttl time.Duration) *BedrockSta } } - return response + return response, nil } diff --git a/src/util.go b/src/util.go index bdecec3..db816c6 100644 --- a/src/util.go +++ b/src/util.go @@ -5,68 +5,18 @@ import ( _ "embed" "encoding/hex" "fmt" - "io" - "net/http" "regexp" "strconv" "strings" - "sync" ) var ( //go:embed icon.png - defaultIconBytes []byte - blockedServers *MutexArray[string] = nil - ipAddressRegExp *regexp.Regexp = regexp.MustCompile(`^\d{1,3}(\.\d{1,3}){3}$`) + defaultIcon []byte + ipAddressRegExp *regexp.Regexp = regexp.MustCompile(`^\d{1,3}(\.\d{1,3}){3}$`) ) -type MutexArray[K comparable] struct { - List []K - Mutex *sync.Mutex -} - -func (m *MutexArray[K]) Has(value K) bool { - m.Mutex.Lock() - - defer m.Mutex.Unlock() - - for _, v := range m.List { - if v == value { - return true - } - } - - return false -} - -func GetBlockedServerList() error { - resp, err := http.Get("https://sessionserver.mojang.com/blockedservers") - - if err != nil { - return err - } - - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("unexpected status code: %d", resp.StatusCode) - } - - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - - if err != nil { - return err - } - - blockedServers = &MutexArray[string]{ - List: strings.Split(string(body), "\n"), - Mutex: &sync.Mutex{}, - } - - return nil -} - -func IsBlockedAddress(address string) bool { +func IsBlockedAddress(address string) (bool, error) { split := strings.Split(strings.ToLower(address), ".") isIPAddress := ipAddressRegExp.MatchString(address) @@ -94,14 +44,19 @@ func IsBlockedAddress(address string) bool { } newAddressBytes := sha1.Sum([]byte(newAddress)) - newAddressHash := hex.EncodeToString(newAddressBytes[:]) - if blockedServers.Has(newAddressHash) { - return true + exists, err := r.Exists(fmt.Sprintf("blocked:%s", hex.EncodeToString(newAddressBytes[:]))) + + if err != nil { + return false, err + } + + if exists { + return true, nil } } - return false + return false, nil } func ParseAddress(address string, defaultPort uint16) (string, uint16, error) {