Rewrite v2
This commit is contained in:
@@ -4,21 +4,23 @@ import (
|
||||
"io/ioutil"
|
||||
"time"
|
||||
|
||||
"github.com/go-yaml/yaml"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Configuration struct {
|
||||
Environment string `yaml:"environment"`
|
||||
Host string `yaml:"host"`
|
||||
Port uint16 `yaml:"port"`
|
||||
Redis string `yaml:"redis"`
|
||||
CacheEnable bool `yaml:"cache_enable"`
|
||||
StatusCacheTTL time.Duration `yaml:"status_cache_ttl"`
|
||||
FaviconCacheTTL time.Duration `yaml:"favicon_cache_ttl"`
|
||||
type Config struct {
|
||||
Host string `yaml:"host"`
|
||||
Port uint16 `yaml:"port"`
|
||||
Redis string `yaml:"redis"`
|
||||
Cache struct {
|
||||
Enable bool `yaml:"enable"`
|
||||
JavaCacheDuration time.Duration `yaml:"java_cache_duration"`
|
||||
BedrockCacheDuration time.Duration `yaml:"bedrock_cache_duration"`
|
||||
IconCacheDuration time.Duration `yaml:"icon_cache_duration"`
|
||||
} `yaml:"cache"`
|
||||
}
|
||||
|
||||
func (c *Configuration) ReadFile(path string) error {
|
||||
data, err := ioutil.ReadFile(path)
|
||||
func (c *Config) ReadFile(file string) error {
|
||||
data, err := ioutil.ReadFile(file)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
99
src/main.go
99
src/main.go
@@ -3,22 +3,20 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"github.com/buaazp/fasthttprouter"
|
||||
"github.com/valyala/fasthttp"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||
)
|
||||
|
||||
var (
|
||||
config *Configuration = &Configuration{}
|
||||
r *Redis = &Redis{}
|
||||
blockedServers []string = nil
|
||||
blockedServersMutex *sync.Mutex = &sync.Mutex{}
|
||||
app *fiber.App = fiber.New(fiber.Config{DisableStartupMessage: true})
|
||||
r *Redis = &Redis{}
|
||||
config *Config = &Config{}
|
||||
blockedServers []string = nil
|
||||
blockedServersMutex *sync.Mutex = &sync.Mutex{}
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -32,42 +30,12 @@ func init() {
|
||||
|
||||
log.Println("Successfully connected to Redis")
|
||||
|
||||
if Contains(os.Args, "--flush-cache") {
|
||||
keys, err := r.Keys("java:*")
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err = r.Delete(keys...); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
keys, err = r.Keys("bedrock:*")
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err = r.Delete(keys...); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
keys, err = r.Keys("favicon:*")
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err = r.Delete(keys...); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Println("Successfully flushed all cache keys")
|
||||
|
||||
os.Exit(0)
|
||||
if err := GetBlockedServerList(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Println("Successfully retrieved EULA blocked servers")
|
||||
|
||||
if instanceID := os.Getenv("INSTANCE_ID"); len(instanceID) > 0 {
|
||||
value, err := strconv.ParseUint(instanceID, 10, 16)
|
||||
|
||||
@@ -78,48 +46,15 @@ func init() {
|
||||
config.Port += uint16(value)
|
||||
}
|
||||
|
||||
if err := GetBlockedServerList(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func middleware(next fasthttp.RequestHandler) fasthttp.RequestHandler {
|
||||
return func(ctx *fasthttp.RequestCtx) {
|
||||
if config.Environment == "development" {
|
||||
log.Printf("GET %s - %s \"%s\"", ctx.Request.URI().Path(), ctx.RemoteAddr(), ctx.UserAgent())
|
||||
}
|
||||
|
||||
ctx.Response.Header.Set("Access-Control-Allow-Headers", "*")
|
||||
ctx.Response.Header.Set("Access-Control-Allow-Methods", "HEAD,GET,POST,OPTIONS")
|
||||
ctx.Response.Header.Set("Access-Control-Allow-Origin", "*")
|
||||
ctx.Response.Header.Set("Access-Control-Expose-Headers", "X-Cache-Hit,X-Cache-Time-Remaining,X-Server-Status,Content-Disposition")
|
||||
|
||||
next(ctx)
|
||||
}
|
||||
app.Use(cors.New(cors.Config{
|
||||
AllowOrigins: "*",
|
||||
AllowMethods: "HEAD,OPTIONS,GET",
|
||||
ExposeHeaders: "Content-Type",
|
||||
}))
|
||||
}
|
||||
|
||||
func main() {
|
||||
defer r.Close()
|
||||
|
||||
router := fasthttprouter.New()
|
||||
router.GET("/ping", PingHandler)
|
||||
router.GET("/status/java/:address", JavaStatusHandler)
|
||||
router.GET("/status/bedrock/:address", BedrockStatusHandler)
|
||||
router.GET("/favicon/:address", FaviconNoExtensionHandler)
|
||||
router.GET("/favicon/:address/*filename", FaviconHandler)
|
||||
|
||||
router.PanicHandler = func(rc *fasthttp.RequestCtx, i interface{}) {
|
||||
log.Println(i)
|
||||
}
|
||||
|
||||
router.NotFound = func(ctx *fasthttp.RequestCtx) {
|
||||
WriteError(ctx, nil, http.StatusNotFound)
|
||||
}
|
||||
|
||||
log.Printf("Listening on %s:%d\n", config.Host, config.Port)
|
||||
log.Fatal(fasthttp.ListenAndServe(fmt.Sprintf("%s:%d", config.Host, config.Port), middleware(router.Handler)))
|
||||
|
||||
s := make(chan os.Signal)
|
||||
signal.Notify(s, os.Interrupt, syscall.SIGTERM)
|
||||
<-s
|
||||
log.Fatal(app.Listen(fmt.Sprintf("%s:%d", config.Host, config.Port)))
|
||||
}
|
||||
|
||||
108
src/redis.go
108
src/redis.go
@@ -2,14 +2,14 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
)
|
||||
|
||||
type Redis struct {
|
||||
Conn *redis.Client
|
||||
Client *redis.Client
|
||||
}
|
||||
|
||||
func (r *Redis) Connect(uri string) error {
|
||||
@@ -19,37 +19,13 @@ func (r *Redis) Connect(uri string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
conn := redis.NewClient(opts)
|
||||
r.Client = redis.NewClient(opts)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
|
||||
defer cancel()
|
||||
|
||||
if err = conn.Ping(ctx).Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.Conn = conn
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Redis) TTL(key string) (time.Duration, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
|
||||
defer cancel()
|
||||
|
||||
val, err := r.Conn.TTL(ctx, key).Result()
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, redis.Nil) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return val, nil
|
||||
return r.Client.Ping(ctx).Err()
|
||||
}
|
||||
|
||||
func (r *Redis) Exists(key string) (bool, error) {
|
||||
@@ -57,27 +33,29 @@ func (r *Redis) Exists(key string) (bool, error) {
|
||||
|
||||
defer cancel()
|
||||
|
||||
val, err := r.Conn.Exists(ctx, key).Result()
|
||||
res := r.Client.Exists(ctx, key)
|
||||
|
||||
if err := res.Err(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
val, err := res.Result()
|
||||
|
||||
return val == 1, err
|
||||
}
|
||||
|
||||
func (r *Redis) Get(key string) (string, error) {
|
||||
func (r *Redis) GetString(key string) (string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
|
||||
defer cancel()
|
||||
|
||||
val, err := r.Conn.Get(ctx, key).Result()
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, redis.Nil) {
|
||||
return "", nil
|
||||
}
|
||||
res := r.Client.Get(ctx, key)
|
||||
|
||||
if err := res.Err(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return val, nil
|
||||
return res.Result()
|
||||
}
|
||||
|
||||
func (r *Redis) GetBytes(key string) ([]byte, error) {
|
||||
@@ -85,17 +63,13 @@ func (r *Redis) GetBytes(key string) ([]byte, error) {
|
||||
|
||||
defer cancel()
|
||||
|
||||
result := r.Conn.Get(ctx, key)
|
||||
|
||||
if err := result.Err(); err != nil {
|
||||
if errors.Is(err, redis.Nil) {
|
||||
return nil, nil
|
||||
}
|
||||
res := r.Client.Get(ctx, key)
|
||||
|
||||
if err := res.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result.Bytes()
|
||||
return res.Bytes()
|
||||
}
|
||||
|
||||
func (r *Redis) Set(key string, value interface{}, ttl time.Duration) error {
|
||||
@@ -103,53 +77,19 @@ func (r *Redis) Set(key string, value interface{}, ttl time.Duration) error {
|
||||
|
||||
defer cancel()
|
||||
|
||||
return r.Conn.Set(ctx, key, value, ttl).Err()
|
||||
return r.Client.Set(ctx, key, value, ttl).Err()
|
||||
}
|
||||
|
||||
func (r *Redis) GetValueAndTTL(key string) (bool, string, time.Duration, error) {
|
||||
exists, err := r.Exists(key)
|
||||
func (r *Redis) SetJSON(key string, value interface{}, ttl time.Duration) error {
|
||||
data, err := json.Marshal(value)
|
||||
|
||||
if err != nil {
|
||||
return false, "", 0, err
|
||||
return err
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return false, "", 0, nil
|
||||
}
|
||||
|
||||
value, err := r.Get(key)
|
||||
|
||||
if err != nil {
|
||||
return false, "", 0, err
|
||||
}
|
||||
|
||||
ttl, err := r.TTL(key)
|
||||
|
||||
return true, value, ttl, err
|
||||
}
|
||||
|
||||
func (r *Redis) Keys(pattern string) ([]string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
|
||||
defer cancel()
|
||||
|
||||
res := r.Conn.Keys(ctx, pattern)
|
||||
|
||||
if err := res.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res.Result()
|
||||
}
|
||||
|
||||
func (r *Redis) Delete(keys ...string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
|
||||
defer cancel()
|
||||
|
||||
return r.Conn.Del(ctx, keys...).Err()
|
||||
return r.Set(key, data, ttl)
|
||||
}
|
||||
|
||||
func (r *Redis) Close() error {
|
||||
return r.Conn.Close()
|
||||
return r.Client.Close()
|
||||
}
|
||||
|
||||
300
src/routes.go
300
src/routes.go
@@ -1,308 +1,74 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func PingHandler(ctx *fasthttp.RequestCtx) {
|
||||
ctx.SetBodyString(http.StatusText(http.StatusOK))
|
||||
func init() {
|
||||
app.Get("/status/java/:address", JavaStatusHandler)
|
||||
app.Get("/status/bedrock/:address", BedrockStatusHandler)
|
||||
app.Get("/icon/:address", IconHandler)
|
||||
}
|
||||
|
||||
func JavaStatusHandler(ctx *fasthttp.RequestCtx) {
|
||||
host, port, err := ParseAddress(ctx.UserValue("address").(string), 25565)
|
||||
func JavaStatusHandler(ctx *fiber.Ctx) error {
|
||||
host, port, err := ParseAddress(ctx.Params("address"), 25565)
|
||||
|
||||
if err != nil {
|
||||
WriteError(ctx, nil, http.StatusBadRequest, "Invalid/malformed address")
|
||||
log.Println(err)
|
||||
|
||||
return
|
||||
return ctx.Status(http.StatusBadRequest).SendString("Invalid address value")
|
||||
}
|
||||
|
||||
cacheEnabled, cacheKey, err := IsCacheEnabled(ctx, "java", host, port)
|
||||
response, err := GetJavaStatus(host, port)
|
||||
|
||||
if err != nil {
|
||||
WriteError(ctx, err, http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
if cacheEnabled {
|
||||
exists, cache, ttl, err := r.GetValueAndTTL(cacheKey)
|
||||
|
||||
if err != nil {
|
||||
WriteError(ctx, err, http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if exists {
|
||||
ctx.Response.Header.Set("X-Cache-Hit", "TRUE")
|
||||
ctx.Response.Header.Set("X-Cache-Time-Remaining", strconv.Itoa(int(ttl.Seconds())))
|
||||
ctx.SetContentType("application/json")
|
||||
ctx.SetBodyString(cache)
|
||||
|
||||
return
|
||||
}
|
||||
switch v := response.(type) {
|
||||
case string:
|
||||
return ctx.Type("json").SendString(v)
|
||||
default:
|
||||
return ctx.JSON(response)
|
||||
}
|
||||
|
||||
ctx.Response.Header.Set("X-Cache-Hit", "FALSE")
|
||||
|
||||
status := GetJavaStatus(host, port)
|
||||
|
||||
if status.Online && status.Response.Favicon != nil {
|
||||
data, err := base64.StdEncoding.DecodeString(strings.Replace(*status.Response.Favicon, "data:image/png;base64,", "", 1))
|
||||
|
||||
if err != nil {
|
||||
WriteError(ctx, err, http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err := r.Set(fmt.Sprintf("favicon:%s-%d", host, port), data, config.FaviconCacheTTL); err != nil {
|
||||
WriteError(ctx, err, http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
data, err := json.Marshal(status)
|
||||
|
||||
if err != nil {
|
||||
WriteError(ctx, err, http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err = r.Set(cacheKey, data, config.StatusCacheTTL); err != nil {
|
||||
WriteError(ctx, err, http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx.SetContentType("application/json")
|
||||
ctx.SetBody(data)
|
||||
}
|
||||
|
||||
func BedrockStatusHandler(ctx *fasthttp.RequestCtx) {
|
||||
host, port, err := ParseAddress(ctx.UserValue("address").(string), 19132)
|
||||
func BedrockStatusHandler(ctx *fiber.Ctx) error {
|
||||
host, port, err := ParseAddress(ctx.Params("address"), 19132)
|
||||
|
||||
if err != nil {
|
||||
WriteError(ctx, nil, http.StatusBadRequest, "Invalid/malformed address")
|
||||
|
||||
return
|
||||
return ctx.Status(http.StatusBadRequest).SendString("Invalid address value")
|
||||
}
|
||||
|
||||
cacheEnabled, cacheKey, err := IsCacheEnabled(ctx, "bedrock", host, port)
|
||||
response, err := GetBedrockStatus(host, port)
|
||||
|
||||
if err != nil {
|
||||
WriteError(ctx, err, http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
if cacheEnabled {
|
||||
exists, cache, ttl, err := r.GetValueAndTTL(cacheKey)
|
||||
|
||||
if err != nil {
|
||||
WriteError(ctx, err, http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if exists {
|
||||
ctx.Response.Header.Set("X-Cache-Hit", "TRUE")
|
||||
ctx.Response.Header.Set("X-Cache-Time-Remaining", strconv.Itoa(int(ttl.Seconds())))
|
||||
ctx.SetContentType("application/json")
|
||||
ctx.SetBodyString(cache)
|
||||
|
||||
return
|
||||
}
|
||||
switch v := response.(type) {
|
||||
case string:
|
||||
return ctx.Type("json").SendString(v)
|
||||
default:
|
||||
return ctx.JSON(response)
|
||||
}
|
||||
|
||||
ctx.Response.Header.Set("X-Cache-Hit", "FALSE")
|
||||
|
||||
data, err := json.Marshal(GetBedrockStatus(host, port))
|
||||
|
||||
if err != nil {
|
||||
WriteError(ctx, err, http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err = r.Set(cacheKey, data, config.StatusCacheTTL); err != nil {
|
||||
WriteError(ctx, err, http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx.SetContentType("application/json")
|
||||
ctx.SetBody(data)
|
||||
}
|
||||
|
||||
func FaviconNoExtensionHandler(ctx *fasthttp.RequestCtx) {
|
||||
host, port, err := ParseAddress(ctx.UserValue("address").(string), 25565)
|
||||
func IconHandler(ctx *fiber.Ctx) error {
|
||||
host, port, err := ParseAddress(ctx.Params("address"), 25565)
|
||||
|
||||
if err != nil {
|
||||
WriteError(ctx, nil, http.StatusBadRequest, "Invalid/malformed address")
|
||||
|
||||
return
|
||||
return ctx.Status(http.StatusBadRequest).SendString("Invalid address value")
|
||||
}
|
||||
|
||||
cacheKey := fmt.Sprintf("favicon:%s-%d", host, port)
|
||||
|
||||
exists, err := r.Exists(cacheKey)
|
||||
icon, err := GetServerIcon(host, port)
|
||||
|
||||
if err != nil {
|
||||
WriteError(ctx, err, http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
if exists {
|
||||
value, err := r.GetBytes(cacheKey)
|
||||
|
||||
if err != nil {
|
||||
WriteError(ctx, err, http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Response.Header.Set("X-Cache-Hit", "TRUE")
|
||||
ctx.Response.Header.Set("X-Server-Status", "online-cache")
|
||||
ctx.SetContentType("image/png")
|
||||
ctx.SetBody(value)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Response.Header.Set("X-Cache-Hit", "FALSE")
|
||||
|
||||
status := GetJavaStatus(host, port)
|
||||
|
||||
if !status.Online {
|
||||
ctx.Response.Header.Set("X-Server-Status", "offline")
|
||||
ctx.SetContentType("image/png")
|
||||
ctx.SetBody(defaultIconBytes)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if status.Response.Favicon == nil {
|
||||
ctx.Response.Header.Set("X-Server-Status", "online-no-icon")
|
||||
ctx.SetContentType("image/png")
|
||||
ctx.SetBody(defaultIconBytes)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
data, err := base64.StdEncoding.DecodeString(strings.Replace(*status.Response.Favicon, "data:image/png;base64,", "", 1))
|
||||
|
||||
if err != nil {
|
||||
WriteError(ctx, err, http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err := r.Set(cacheKey, data, config.FaviconCacheTTL); err != nil {
|
||||
WriteError(ctx, err, http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Response.Header.Set("X-Server-Status", "online")
|
||||
ctx.SetContentType("image/png")
|
||||
ctx.SetBody(data)
|
||||
}
|
||||
|
||||
func FaviconHandler(ctx *fasthttp.RequestCtx) {
|
||||
filename := ctx.UserValue("filename").(string)
|
||||
|
||||
if !strings.HasSuffix(filename, ".png") {
|
||||
WriteError(ctx, nil, http.StatusBadRequest, "Filename must end with .png")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
host, port, err := ParseAddress(ctx.UserValue("address").(string), 25565)
|
||||
|
||||
if err != nil {
|
||||
WriteError(ctx, nil, http.StatusBadRequest, "Invalid/malformed address")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
cacheKey := fmt.Sprintf("favicon:%s-%d", host, port)
|
||||
|
||||
exists, err := r.Exists(cacheKey)
|
||||
|
||||
if err != nil {
|
||||
WriteError(ctx, err, http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if exists {
|
||||
value, err := r.GetBytes(cacheKey)
|
||||
|
||||
if err != nil {
|
||||
WriteError(ctx, err, http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Response.Header.Set("X-Cache-Hit", "TRUE")
|
||||
ctx.Response.Header.Set("X-Server-Status", "online-cache")
|
||||
ctx.Response.Header.Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", filename))
|
||||
ctx.SetContentType("image/png")
|
||||
ctx.SetBody(value)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Response.Header.Set("X-Cache-Hit", "FALSE")
|
||||
|
||||
status := GetJavaStatus(host, port)
|
||||
|
||||
if !status.Online {
|
||||
ctx.Response.Header.Set("X-Server-Status", "offline")
|
||||
ctx.Response.Header.Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", filename))
|
||||
ctx.SetContentType("image/png")
|
||||
ctx.SetBody(defaultIconBytes)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if status.Response.Favicon == nil {
|
||||
ctx.Response.Header.Set("X-Server-Status", "online-no-icon")
|
||||
ctx.Response.Header.Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", filename))
|
||||
ctx.SetContentType("image/png")
|
||||
ctx.SetBody(defaultIconBytes)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
data, err := base64.StdEncoding.DecodeString(strings.Replace(*status.Response.Favicon, "data:image/png;base64,", "", 1))
|
||||
|
||||
if err != nil {
|
||||
WriteError(ctx, err, http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err := r.Set(cacheKey, data, config.FaviconCacheTTL); err != nil {
|
||||
WriteError(ctx, err, http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Response.Header.Set("X-Server-Status", "online")
|
||||
ctx.Response.Header.Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", filename))
|
||||
ctx.SetContentType("image/png")
|
||||
ctx.SetBody(data)
|
||||
return ctx.Type("png").Send(icon)
|
||||
}
|
||||
|
||||
414
src/status.go
414
src/status.go
@@ -1,61 +1,73 @@
|
||||
package main
|
||||
|
||||
import "github.com/PassTheMayo/mcstatus/v4"
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
type StatusResponse[T JavaStatus | BedrockStatus] struct {
|
||||
"github.com/PassTheMayo/mcstatus/v4"
|
||||
)
|
||||
|
||||
type StatusOffline struct {
|
||||
Online bool `json:"online"`
|
||||
Host string `json:"host"`
|
||||
Port uint16 `json:"port"`
|
||||
EULABlocked bool `json:"eula_blocked"`
|
||||
Response *T `json:"response"`
|
||||
}
|
||||
|
||||
type JavaStatus struct {
|
||||
Version *Version `json:"version"`
|
||||
Players Players `json:"players"`
|
||||
MOTD MOTD `json:"motd"`
|
||||
Favicon *string `json:"favicon"`
|
||||
ModInfo *ModInfo `json:"mod_info"`
|
||||
SRVRecord *SRVRecord `json:"srv_record"`
|
||||
type JavaStatusResponse struct {
|
||||
Online bool `json:"online"`
|
||||
Host string `json:"host"`
|
||||
Port uint16 `json:"port"`
|
||||
EULABlocked bool `json:"eula_blocked"`
|
||||
Version *JavaVersion `json:"version"`
|
||||
Players JavaPlayers `json:"players"`
|
||||
MOTD MOTD `json:"motd"`
|
||||
Icon *string `json:"icon"`
|
||||
Mods []Mod `json:"mods"`
|
||||
}
|
||||
|
||||
type Players struct {
|
||||
Online int `json:"online"`
|
||||
Max int `json:"max"`
|
||||
Sample []SamplePlayer `json:"sample"`
|
||||
type BedrockStatusResponse struct {
|
||||
Online bool `json:"online"`
|
||||
Host string `json:"host"`
|
||||
Port uint16 `json:"port"`
|
||||
EULABlocked bool `json:"eula_blocked"`
|
||||
Version *BedrockVersion `json:"version"`
|
||||
Players *BedrockPlayers `json:"players"`
|
||||
MOTD *MOTD `json:"motd"`
|
||||
Gamemode *string `json:"gamemode"`
|
||||
ServerID *string `json:"server_id"`
|
||||
Edition *string `json:"edition"`
|
||||
}
|
||||
|
||||
type SamplePlayer struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Clean string `json:"clean"`
|
||||
HTML string `json:"html"`
|
||||
type JavaVersion struct {
|
||||
NameRaw string `json:"name_raw"`
|
||||
NameClean string `json:"name_clean"`
|
||||
NameHTML string `json:"name_html"`
|
||||
Protocol int `json:"protocol"`
|
||||
}
|
||||
|
||||
type ModInfo struct {
|
||||
Type string `json:"type"`
|
||||
Mods []Mod `json:"mods"`
|
||||
type BedrockVersion struct {
|
||||
Name *string `json:"name"`
|
||||
Protocol *int64 `json:"protocol"`
|
||||
}
|
||||
|
||||
type Mod struct {
|
||||
ID string `json:"id"`
|
||||
Version string `json:"version"`
|
||||
type JavaPlayers struct {
|
||||
Online int `json:"online"`
|
||||
Max int `json:"max"`
|
||||
List []Player `json:"list"`
|
||||
}
|
||||
|
||||
type BedrockStatus struct {
|
||||
ServerGUID int64 `json:"server_guid"`
|
||||
Edition *string `json:"edition"`
|
||||
MOTD *MOTD `json:"motd"`
|
||||
ProtocolVersion *int64 `json:"protocol_version"`
|
||||
Version *string `json:"version"`
|
||||
OnlinePlayers *int64 `json:"online_players"`
|
||||
MaxPlayers *int64 `json:"max_players"`
|
||||
ServerID *string `json:"server_id"`
|
||||
Gamemode *string `json:"gamemode"`
|
||||
GamemodeID *int64 `json:"gamemode_id"`
|
||||
PortIPv4 *uint16 `json:"port_ipv4"`
|
||||
PortIPv6 *uint16 `json:"port_ipv6"`
|
||||
SRVRecord *SRVRecord `json:"srv_record"`
|
||||
type BedrockPlayers struct {
|
||||
Online *int64 `json:"online"`
|
||||
Max *int64 `json:"max"`
|
||||
}
|
||||
|
||||
type Player struct {
|
||||
UUID string `json:"uuid"`
|
||||
NameRaw string `json:"name_raw"`
|
||||
NameClean string `json:"name_clean"`
|
||||
NameHTML string `json:"name_html"`
|
||||
}
|
||||
|
||||
type MOTD struct {
|
||||
@@ -64,188 +76,274 @@ type MOTD struct {
|
||||
HTML string `json:"html"`
|
||||
}
|
||||
|
||||
type Version struct {
|
||||
Name string `json:"name"`
|
||||
Protocol int `json:"protocol"`
|
||||
type Mod struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
type SRVRecord struct {
|
||||
Host string `json:"host"`
|
||||
Port uint16 `json:"port"`
|
||||
func GetJavaStatus(host string, port uint16) (interface{}, error) {
|
||||
cacheKey := fmt.Sprintf("java:%s-%d", host, port)
|
||||
|
||||
if config.Cache.Enable {
|
||||
exists, err := r.Exists(cacheKey)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if exists {
|
||||
return r.GetString(cacheKey)
|
||||
}
|
||||
}
|
||||
|
||||
response := FetchJavaStatus(host, port)
|
||||
|
||||
if config.Cache.Enable {
|
||||
if err := r.SetJSON(cacheKey, response, config.Cache.JavaCacheDuration); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func GetJavaStatus(host string, port uint16) (resp StatusResponse[JavaStatus]) {
|
||||
func GetBedrockStatus(host string, port uint16) (interface{}, error) {
|
||||
cacheKey := fmt.Sprintf("bedrock:%s-%d", host, port)
|
||||
|
||||
if config.Cache.Enable {
|
||||
exists, err := r.Exists(cacheKey)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if exists {
|
||||
return r.GetString(cacheKey)
|
||||
}
|
||||
}
|
||||
|
||||
response := FetchBedrockStatus(host, port)
|
||||
|
||||
if config.Cache.Enable {
|
||||
if err := r.SetJSON(cacheKey, response, config.Cache.BedrockCacheDuration); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func GetServerIcon(host string, port uint16) ([]byte, error) {
|
||||
cacheKey := fmt.Sprintf("icon:%s-%d", host, port)
|
||||
|
||||
if config.Cache.Enable {
|
||||
exists, err := r.Exists(cacheKey)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if exists {
|
||||
return r.GetBytes(cacheKey)
|
||||
}
|
||||
}
|
||||
|
||||
icon := defaultIconBytes
|
||||
|
||||
status, err := mcstatus.Status(host, port)
|
||||
|
||||
if err == nil && status.Favicon != nil && strings.HasPrefix(*status.Favicon, "data:image/png;base64,") {
|
||||
data, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(*status.Favicon, "data:image/png;base64,"))
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
icon = data
|
||||
}
|
||||
|
||||
if config.Cache.Enable {
|
||||
if err := r.Set(cacheKey, icon, config.Cache.IconCacheDuration); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return icon, nil
|
||||
}
|
||||
|
||||
func FetchJavaStatus(host string, port uint16) interface{} {
|
||||
status, err := mcstatus.Status(host, port)
|
||||
|
||||
if err != nil {
|
||||
statusLegacy, err := mcstatus.StatusLegacy(host, port)
|
||||
|
||||
if err != nil {
|
||||
resp = StatusResponse[JavaStatus]{
|
||||
return StatusOffline{
|
||||
Online: false,
|
||||
Host: host,
|
||||
Port: port,
|
||||
EULABlocked: IsBlockedAddress(host),
|
||||
Response: nil,
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
resp = StatusResponse[JavaStatus]{
|
||||
response := JavaStatusResponse{
|
||||
Online: true,
|
||||
Host: host,
|
||||
Port: port,
|
||||
EULABlocked: IsBlockedAddress(host),
|
||||
Response: &JavaStatus{
|
||||
Version: nil,
|
||||
Players: Players{
|
||||
Online: statusLegacy.Players.Online,
|
||||
Max: statusLegacy.Players.Max,
|
||||
Sample: make([]SamplePlayer, 0),
|
||||
},
|
||||
MOTD: MOTD{
|
||||
Raw: statusLegacy.MOTD.Raw,
|
||||
Clean: statusLegacy.MOTD.Clean,
|
||||
HTML: statusLegacy.MOTD.HTML,
|
||||
},
|
||||
Favicon: nil,
|
||||
ModInfo: nil,
|
||||
SRVRecord: nil,
|
||||
Version: nil,
|
||||
Players: JavaPlayers{
|
||||
Online: statusLegacy.Players.Online,
|
||||
Max: statusLegacy.Players.Max,
|
||||
List: make([]Player, 0),
|
||||
},
|
||||
MOTD: MOTD{
|
||||
Raw: statusLegacy.MOTD.Raw,
|
||||
Clean: statusLegacy.MOTD.Clean,
|
||||
HTML: statusLegacy.MOTD.HTML,
|
||||
},
|
||||
Icon: nil,
|
||||
Mods: make([]Mod, 0),
|
||||
}
|
||||
|
||||
if statusLegacy.Version != nil {
|
||||
resp.Response.Version = &Version{
|
||||
Name: statusLegacy.Version.Name,
|
||||
Protocol: statusLegacy.Version.Protocol,
|
||||
response.Version = &JavaVersion{
|
||||
NameRaw: statusLegacy.Version.Name,
|
||||
NameClean: statusLegacy.Version.Clean,
|
||||
NameHTML: statusLegacy.Version.HTML,
|
||||
Protocol: statusLegacy.Version.Protocol,
|
||||
}
|
||||
}
|
||||
|
||||
if statusLegacy.SRVResult != nil {
|
||||
resp.Response.SRVRecord = &SRVRecord{
|
||||
Host: statusLegacy.SRVResult.Host,
|
||||
Port: statusLegacy.SRVResult.Port,
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
playerList := make([]Player, 0)
|
||||
|
||||
if status.Players.Sample != nil {
|
||||
for _, player := range status.Players.Sample {
|
||||
playerList = append(playerList, Player{
|
||||
UUID: player.ID,
|
||||
NameRaw: player.Name,
|
||||
NameClean: player.Clean,
|
||||
NameHTML: player.HTML,
|
||||
})
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
samplePlayers := make([]SamplePlayer, 0)
|
||||
modList := make([]Mod, 0)
|
||||
|
||||
for _, player := range status.Players.Sample {
|
||||
samplePlayers = append(samplePlayers, SamplePlayer{
|
||||
ID: player.ID,
|
||||
Name: player.Name,
|
||||
Clean: player.Clean,
|
||||
HTML: player.HTML,
|
||||
})
|
||||
if status.ModInfo != nil {
|
||||
for _, mod := range status.ModInfo.Mods {
|
||||
modList = append(modList, Mod{
|
||||
Name: mod.ID,
|
||||
Version: mod.Version,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
resp = StatusResponse[JavaStatus]{
|
||||
return JavaStatusResponse{
|
||||
Online: true,
|
||||
Host: host,
|
||||
Port: port,
|
||||
EULABlocked: IsBlockedAddress(host),
|
||||
Response: &JavaStatus{
|
||||
Version: &Version{
|
||||
Name: status.Version.Name,
|
||||
Protocol: status.Version.Protocol,
|
||||
},
|
||||
Players: Players{
|
||||
Online: status.Players.Online,
|
||||
Max: status.Players.Max,
|
||||
Sample: samplePlayers,
|
||||
},
|
||||
MOTD: MOTD{
|
||||
Raw: status.MOTD.Raw,
|
||||
Clean: status.MOTD.Clean,
|
||||
HTML: status.MOTD.HTML,
|
||||
},
|
||||
Favicon: status.Favicon,
|
||||
ModInfo: nil,
|
||||
SRVRecord: nil,
|
||||
Version: &JavaVersion{
|
||||
NameRaw: status.Version.Name,
|
||||
NameClean: status.Version.Clean,
|
||||
NameHTML: status.Version.HTML,
|
||||
Protocol: status.Version.Protocol,
|
||||
},
|
||||
Players: JavaPlayers{
|
||||
Online: status.Players.Online,
|
||||
Max: status.Players.Max,
|
||||
List: playerList,
|
||||
},
|
||||
MOTD: MOTD{
|
||||
Raw: status.MOTD.Raw,
|
||||
Clean: status.MOTD.Clean,
|
||||
HTML: status.MOTD.HTML,
|
||||
},
|
||||
Icon: status.Favicon,
|
||||
Mods: modList,
|
||||
}
|
||||
|
||||
if status.ModInfo != nil {
|
||||
mods := make([]Mod, 0)
|
||||
|
||||
for _, mod := range status.ModInfo.Mods {
|
||||
mods = append(mods, Mod{
|
||||
ID: mod.ID,
|
||||
Version: mod.Version,
|
||||
})
|
||||
}
|
||||
|
||||
resp.Response.ModInfo = &ModInfo{
|
||||
Type: status.ModInfo.Type,
|
||||
Mods: mods,
|
||||
}
|
||||
}
|
||||
|
||||
if status.SRVResult != nil {
|
||||
resp.Response.SRVRecord = &SRVRecord{
|
||||
Host: status.SRVResult.Host,
|
||||
Port: status.SRVResult.Port,
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func GetBedrockStatus(host string, port uint16) (resp StatusResponse[BedrockStatus]) {
|
||||
func FetchBedrockStatus(host string, port uint16) interface{} {
|
||||
status, err := mcstatus.StatusBedrock(host, port)
|
||||
|
||||
if err != nil {
|
||||
resp = StatusResponse[BedrockStatus]{
|
||||
return StatusOffline{
|
||||
Online: false,
|
||||
Host: host,
|
||||
Port: port,
|
||||
EULABlocked: IsBlockedAddress(host),
|
||||
Response: nil,
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
resp = StatusResponse[BedrockStatus]{
|
||||
response := BedrockStatusResponse{
|
||||
Online: true,
|
||||
Host: host,
|
||||
Port: port,
|
||||
EULABlocked: IsBlockedAddress(host),
|
||||
Response: &BedrockStatus{
|
||||
ServerGUID: status.ServerGUID,
|
||||
Edition: status.Edition,
|
||||
MOTD: nil,
|
||||
ProtocolVersion: status.ProtocolVersion,
|
||||
Version: status.Version,
|
||||
OnlinePlayers: status.OnlinePlayers,
|
||||
MaxPlayers: status.MaxPlayers,
|
||||
ServerID: status.ServerID,
|
||||
Gamemode: status.Gamemode,
|
||||
GamemodeID: status.GamemodeID,
|
||||
PortIPv4: status.PortIPv4,
|
||||
PortIPv6: status.PortIPv6,
|
||||
SRVRecord: nil,
|
||||
},
|
||||
Version: nil,
|
||||
Players: nil,
|
||||
MOTD: nil,
|
||||
Gamemode: status.Gamemode,
|
||||
ServerID: status.ServerID,
|
||||
Edition: status.Edition,
|
||||
}
|
||||
|
||||
if status.Version != nil {
|
||||
if response.Version == nil {
|
||||
response.Version = &BedrockVersion{
|
||||
Name: nil,
|
||||
Protocol: nil,
|
||||
}
|
||||
}
|
||||
|
||||
response.Version.Name = status.Version
|
||||
}
|
||||
|
||||
if status.ProtocolVersion != nil {
|
||||
if response.Version == nil {
|
||||
response.Version = &BedrockVersion{
|
||||
Name: nil,
|
||||
Protocol: nil,
|
||||
}
|
||||
}
|
||||
|
||||
response.Version.Protocol = status.ProtocolVersion
|
||||
}
|
||||
|
||||
if status.OnlinePlayers != nil {
|
||||
if response.Players == nil {
|
||||
response.Players = &BedrockPlayers{
|
||||
Online: nil,
|
||||
Max: nil,
|
||||
}
|
||||
}
|
||||
|
||||
response.Players.Online = status.OnlinePlayers
|
||||
}
|
||||
|
||||
if status.MaxPlayers != nil {
|
||||
if response.Players == nil {
|
||||
response.Players = &BedrockPlayers{
|
||||
Online: nil,
|
||||
Max: nil,
|
||||
}
|
||||
}
|
||||
|
||||
response.Players.Max = status.MaxPlayers
|
||||
}
|
||||
|
||||
if status.MOTD != nil {
|
||||
resp.Response.MOTD = &MOTD{
|
||||
response.MOTD = &MOTD{
|
||||
Raw: status.MOTD.Raw,
|
||||
Clean: status.MOTD.Clean,
|
||||
HTML: status.MOTD.HTML,
|
||||
}
|
||||
}
|
||||
|
||||
if status.SRVResult != nil {
|
||||
resp.Response.SRVRecord = &SRVRecord{
|
||||
Host: status.SRVResult.Host,
|
||||
Port: status.SRVResult.Port,
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
return response
|
||||
}
|
||||
|
||||
64
src/util.go
64
src/util.go
@@ -4,27 +4,29 @@ import (
|
||||
"crypto/sha1"
|
||||
_ "embed"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed default-icon.png
|
||||
//go:embed icon.png
|
||||
defaultIconBytes []byte
|
||||
ipAddressRegExp = regexp.MustCompile("^\\d{1,3}(\\.\\d{1,3}){3}$")
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoAddressMatch = errors.New("address does not match any known format")
|
||||
)
|
||||
func Contains[T comparable](arr []T, v T) bool {
|
||||
for _, value := range arr {
|
||||
if v == value {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func GetBlockedServerList() error {
|
||||
resp, err := http.Get("https://sessionserver.mojang.com/blockedservers")
|
||||
@@ -100,7 +102,7 @@ func ParseAddress(address string, defaultPort uint16) (string, uint16, error) {
|
||||
result := strings.SplitN(address, ":", 2)
|
||||
|
||||
if len(result) < 1 {
|
||||
return "", 0, ErrNoAddressMatch
|
||||
return "", 0, fmt.Errorf("'%s' does not match any known address", address)
|
||||
}
|
||||
|
||||
if len(result) < 2 {
|
||||
@@ -115,47 +117,3 @@ func ParseAddress(address string, defaultPort uint16) (string, uint16, error) {
|
||||
|
||||
return result[0], uint16(port), nil
|
||||
}
|
||||
|
||||
func WriteError(ctx *fasthttp.RequestCtx, err error, statusCode int, body ...string) {
|
||||
ctx.SetStatusCode(statusCode)
|
||||
|
||||
if len(body) > 0 {
|
||||
ctx.SetBodyString(body[0])
|
||||
} else {
|
||||
ctx.SetBodyString(http.StatusText(statusCode))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func IsCacheEnabled(ctx *fasthttp.RequestCtx, cacheType, host string, port uint16) (bool, string, error) {
|
||||
key := fmt.Sprintf("%s:%s-%d", cacheType, host, port)
|
||||
|
||||
if authKey := ctx.Request.Header.Peek("Authorization"); len(authKey) > 0 {
|
||||
exists, err := r.Exists(fmt.Sprintf("auth_key:%s", authKey))
|
||||
|
||||
if err != nil {
|
||||
WriteError(ctx, err, http.StatusInternalServerError)
|
||||
|
||||
return config.CacheEnable, key, err
|
||||
}
|
||||
|
||||
if exists {
|
||||
return false, key, nil
|
||||
}
|
||||
}
|
||||
|
||||
return config.CacheEnable, key, nil
|
||||
}
|
||||
|
||||
func Contains[T comparable](arr []T, x T) bool {
|
||||
for _, v := range arr {
|
||||
if v == x {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user