Use Redis for blocked servers list
This commit is contained in:
@@ -5,7 +5,7 @@ The REST server that powers the API for mcstatus.io. This repository is open sou
|
|||||||
|
|
||||||
- [Git](https://git-scm.com/)
|
- [Git](https://git-scm.com/)
|
||||||
- [Go](https://go.dev/)
|
- [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*)
|
- [GNU Make](https://www.gnu.org/software/make/) (*optional*)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
environment: production
|
environment: production
|
||||||
host: 127.0.0.1
|
host: 127.0.0.1
|
||||||
port: 3001
|
port: 3001
|
||||||
redis: redis://127.0.0.1:6379/0
|
redis:
|
||||||
cache:
|
host: 127.0.0.1
|
||||||
enable: false
|
port: 6379
|
||||||
|
# username: myuser
|
||||||
|
# password: mypassword
|
||||||
|
database: 0
|
||||||
java_cache_duration: 1m
|
java_cache_duration: 1m
|
||||||
bedrock_cache_duration: 1m
|
bedrock_cache_duration: 1m
|
||||||
icon_cache_duration: 15m
|
icon_cache_duration: 15m
|
||||||
48
src/blocked.go
Normal file
48
src/blocked.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -11,13 +11,16 @@ type Config struct {
|
|||||||
Environment string `yaml:"environment"`
|
Environment string `yaml:"environment"`
|
||||||
Host string `yaml:"host"`
|
Host string `yaml:"host"`
|
||||||
Port uint16 `yaml:"port"`
|
Port uint16 `yaml:"port"`
|
||||||
Redis string `yaml:"redis"`
|
Redis struct {
|
||||||
Cache struct {
|
Host string `yaml:"host"`
|
||||||
Enable bool `yaml:"enable"`
|
Port uint16 `yaml:"port"`
|
||||||
|
Username string `yaml:"username"`
|
||||||
|
Password string `yaml:"password"`
|
||||||
|
Database int `yaml:"database"`
|
||||||
JavaCacheDuration time.Duration `yaml:"java_cache_duration"`
|
JavaCacheDuration time.Duration `yaml:"java_cache_duration"`
|
||||||
BedrockCacheDuration time.Duration `yaml:"bedrock_cache_duration"`
|
BedrockCacheDuration time.Duration `yaml:"bedrock_cache_duration"`
|
||||||
IconCacheDuration time.Duration `yaml:"icon_cache_duration"`
|
IconCacheDuration time.Duration `yaml:"icon_cache_duration"`
|
||||||
} `yaml:"cache"`
|
} `yaml:"redis"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) ReadFile(file string) error {
|
func (c *Config) ReadFile(file string) error {
|
||||||
|
|||||||
12
src/main.go
12
src/main.go
@@ -29,19 +29,11 @@ func init() {
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Cache.Enable {
|
if err := r.Connect(); err != nil {
|
||||||
if err := r.Connect(config.Redis); err != nil {
|
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("Successfully connected to Redis")
|
log.Println("Successfully connected to Redis")
|
||||||
}
|
|
||||||
|
|
||||||
if err := GetBlockedServerList(); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("Successfully retrieved EULA blocked servers")
|
|
||||||
|
|
||||||
app.Use(recover.New())
|
app.Use(recover.New())
|
||||||
|
|
||||||
@@ -62,6 +54,8 @@ func init() {
|
|||||||
func main() {
|
func main() {
|
||||||
defer r.Close()
|
defer r.Close()
|
||||||
|
|
||||||
|
go StartBlockedServersGoroutine()
|
||||||
|
|
||||||
log.Printf("Listening on %s:%d\n", config.Host, config.Port)
|
log.Printf("Listening on %s:%d\n", config.Host, config.Port)
|
||||||
log.Fatal(app.Listen(fmt.Sprintf("%s:%d", config.Host, config.Port)))
|
log.Fatal(app.Listen(fmt.Sprintf("%s:%d", config.Host, config.Port)))
|
||||||
}
|
}
|
||||||
|
|||||||
16
src/redis.go
16
src/redis.go
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8"
|
"github.com/go-redis/redis/v8"
|
||||||
@@ -11,14 +12,13 @@ type Redis struct {
|
|||||||
Client *redis.Client
|
Client *redis.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Redis) Connect(uri string) error {
|
func (r *Redis) Connect() error {
|
||||||
opts, err := redis.ParseURL(uri)
|
r.Client = redis.NewClient(&redis.Options{
|
||||||
|
Addr: fmt.Sprintf("%s:%d", config.Redis.Host, config.Redis.Port),
|
||||||
if err != nil {
|
Username: config.Redis.Username,
|
||||||
return err
|
Password: config.Redis.Password,
|
||||||
}
|
DB: config.Redis.Database,
|
||||||
|
})
|
||||||
r.Client = redis.NewClient(opts)
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||||
|
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ func IconHandler(ctx *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func DefaultIconHandler(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 {
|
func NotFoundHandler(ctx *fiber.Ctx) error {
|
||||||
|
|||||||
@@ -114,7 +114,11 @@ func GetJavaStatus(host string, port uint16) (*JavaStatusResponse, *time.Duratio
|
|||||||
return &response, &ttl, err
|
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)
|
data, err := json.Marshal(response)
|
||||||
|
|
||||||
@@ -122,7 +126,7 @@ func GetJavaStatus(host string, port uint16) (*JavaStatusResponse, *time.Duratio
|
|||||||
return nil, nil, err
|
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
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,7 +160,11 @@ func GetBedrockStatus(host string, port uint16) (*BedrockStatusResponse, *time.D
|
|||||||
return &response, &ttl, err
|
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)
|
data, err := json.Marshal(response)
|
||||||
|
|
||||||
@@ -164,7 +172,7 @@ func GetBedrockStatus(host string, port uint16) (*BedrockStatusResponse, *time.D
|
|||||||
return nil, nil, err
|
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
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,7 +200,7 @@ func GetServerIcon(host string, port uint16) ([]byte, *time.Duration, error) {
|
|||||||
return data, &ttl, err
|
return data, &ttl, err
|
||||||
}
|
}
|
||||||
|
|
||||||
icon := defaultIconBytes
|
icon := defaultIcon
|
||||||
|
|
||||||
status, err := mcutil.Status(host, port)
|
status, err := mcutil.Status(host, port)
|
||||||
|
|
||||||
@@ -206,14 +214,20 @@ func GetServerIcon(host string, port uint16) ([]byte, *time.Duration, error) {
|
|||||||
icon = data
|
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 nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return icon, nil, nil
|
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)
|
status, err := mcutil.Status(host, port)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -225,11 +239,11 @@ func FetchJavaStatus(host string, port uint16, ttl time.Duration) *JavaStatusRes
|
|||||||
Online: false,
|
Online: false,
|
||||||
Host: host,
|
Host: host,
|
||||||
Port: port,
|
Port: port,
|
||||||
EULABlocked: IsBlockedAddress(host),
|
EULABlocked: isEULABlocked,
|
||||||
RetrievedAt: time.Now().UnixMilli(),
|
RetrievedAt: time.Now().UnixMilli(),
|
||||||
ExpiresAt: time.Now().Add(ttl).UnixMilli(),
|
ExpiresAt: time.Now().Add(config.Redis.JavaCacheDuration).UnixMilli(),
|
||||||
},
|
},
|
||||||
}
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
response := &JavaStatusResponse{
|
response := &JavaStatusResponse{
|
||||||
@@ -237,9 +251,9 @@ func FetchJavaStatus(host string, port uint16, ttl time.Duration) *JavaStatusRes
|
|||||||
Online: true,
|
Online: true,
|
||||||
Host: host,
|
Host: host,
|
||||||
Port: port,
|
Port: port,
|
||||||
EULABlocked: IsBlockedAddress(host),
|
EULABlocked: isEULABlocked,
|
||||||
RetrievedAt: time.Now().UnixMilli(),
|
RetrievedAt: time.Now().UnixMilli(),
|
||||||
ExpiresAt: time.Now().Add(ttl).UnixMilli(),
|
ExpiresAt: time.Now().Add(config.Redis.JavaCacheDuration).UnixMilli(),
|
||||||
},
|
},
|
||||||
JavaStatus: &JavaStatus{
|
JavaStatus: &JavaStatus{
|
||||||
Version: nil,
|
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)
|
playerList := make([]Player, 0)
|
||||||
@@ -299,9 +313,9 @@ func FetchJavaStatus(host string, port uint16, ttl time.Duration) *JavaStatusRes
|
|||||||
Online: true,
|
Online: true,
|
||||||
Host: host,
|
Host: host,
|
||||||
Port: port,
|
Port: port,
|
||||||
EULABlocked: IsBlockedAddress(host),
|
EULABlocked: isEULABlocked,
|
||||||
RetrievedAt: time.Now().UnixMilli(),
|
RetrievedAt: time.Now().UnixMilli(),
|
||||||
ExpiresAt: time.Now().Add(ttl).UnixMilli(),
|
ExpiresAt: time.Now().Add(config.Redis.JavaCacheDuration).UnixMilli(),
|
||||||
},
|
},
|
||||||
JavaStatus: &JavaStatus{
|
JavaStatus: &JavaStatus{
|
||||||
Version: &JavaVersion{
|
Version: &JavaVersion{
|
||||||
@@ -323,10 +337,16 @@ func FetchJavaStatus(host string, port uint16, ttl time.Duration) *JavaStatusRes
|
|||||||
Icon: status.Favicon,
|
Icon: status.Favicon,
|
||||||
Mods: modList,
|
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)
|
status, err := mcutil.StatusBedrock(host, port)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -335,11 +355,11 @@ func FetchBedrockStatus(host string, port uint16, ttl time.Duration) *BedrockSta
|
|||||||
Online: false,
|
Online: false,
|
||||||
Host: host,
|
Host: host,
|
||||||
Port: port,
|
Port: port,
|
||||||
EULABlocked: IsBlockedAddress(host),
|
EULABlocked: isEULABlocked,
|
||||||
RetrievedAt: time.Now().UnixMilli(),
|
RetrievedAt: time.Now().UnixMilli(),
|
||||||
ExpiresAt: time.Now().Add(ttl).UnixMilli(),
|
ExpiresAt: time.Now().Add(config.Redis.BedrockCacheDuration).UnixMilli(),
|
||||||
},
|
},
|
||||||
}
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
response := &BedrockStatusResponse{
|
response := &BedrockStatusResponse{
|
||||||
@@ -347,9 +367,9 @@ func FetchBedrockStatus(host string, port uint16, ttl time.Duration) *BedrockSta
|
|||||||
Online: true,
|
Online: true,
|
||||||
Host: host,
|
Host: host,
|
||||||
Port: port,
|
Port: port,
|
||||||
EULABlocked: IsBlockedAddress(host),
|
EULABlocked: isEULABlocked,
|
||||||
RetrievedAt: time.Now().UnixMilli(),
|
RetrievedAt: time.Now().UnixMilli(),
|
||||||
ExpiresAt: time.Now().Add(ttl).UnixMilli(),
|
ExpiresAt: time.Now().Add(config.Redis.BedrockCacheDuration).UnixMilli(),
|
||||||
},
|
},
|
||||||
BedrockStatus: &BedrockStatus{
|
BedrockStatus: &BedrockStatus{
|
||||||
Version: nil,
|
Version: nil,
|
||||||
@@ -413,5 +433,5 @@ func FetchBedrockStatus(host string, port uint16, ttl time.Duration) *BedrockSta
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return response
|
return response, nil
|
||||||
}
|
}
|
||||||
|
|||||||
67
src/util.go
67
src/util.go
@@ -5,68 +5,18 @@ import (
|
|||||||
_ "embed"
|
_ "embed"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
//go:embed icon.png
|
//go:embed icon.png
|
||||||
defaultIconBytes []byte
|
defaultIcon []byte
|
||||||
blockedServers *MutexArray[string] = nil
|
|
||||||
ipAddressRegExp *regexp.Regexp = regexp.MustCompile(`^\d{1,3}(\.\d{1,3}){3}$`)
|
ipAddressRegExp *regexp.Regexp = regexp.MustCompile(`^\d{1,3}(\.\d{1,3}){3}$`)
|
||||||
)
|
)
|
||||||
|
|
||||||
type MutexArray[K comparable] struct {
|
func IsBlockedAddress(address string) (bool, error) {
|
||||||
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 {
|
|
||||||
split := strings.Split(strings.ToLower(address), ".")
|
split := strings.Split(strings.ToLower(address), ".")
|
||||||
|
|
||||||
isIPAddress := ipAddressRegExp.MatchString(address)
|
isIPAddress := ipAddressRegExp.MatchString(address)
|
||||||
@@ -94,14 +44,19 @@ func IsBlockedAddress(address string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
newAddressBytes := sha1.Sum([]byte(newAddress))
|
newAddressBytes := sha1.Sum([]byte(newAddress))
|
||||||
newAddressHash := hex.EncodeToString(newAddressBytes[:])
|
|
||||||
|
|
||||||
if blockedServers.Has(newAddressHash) {
|
exists, err := r.Exists(fmt.Sprintf("blocked:%s", hex.EncodeToString(newAddressBytes[:])))
|
||||||
return true
|
|
||||||
|
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) {
|
func ParseAddress(address string, defaultPort uint16) (string, uint16, error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user