Support query lookup for Java servers

This commit is contained in:
Jacob Gunther 2023-04-18 00:38:55 -05:00
parent 2ea126daed
commit 30311f200a
No known key found for this signature in database
GPG Key ID: 3F72AFAF0F74E5FD
5 changed files with 223 additions and 122 deletions

2
go.mod
View File

@ -7,7 +7,7 @@ require (
github.com/go-redis/redis/v8 v8.11.5
github.com/gofiber/fiber/v2 v2.43.0
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
github.com/mcstatus-io/mcutil v0.0.0-20230416213822-e8ba7a2ea0dd
github.com/mcstatus-io/mcutil v1.0.0
golang.org/x/image v0.6.0
gopkg.in/yaml.v3 v3.0.1
)

6
go.sum
View File

@ -24,10 +24,8 @@ github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp9
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mcstatus-io/mcutil v0.0.0-20230331012121-a2cb94e8ebfe h1:hbmOZmXWQgx+R7RTL4V0ORTEn4GNp2/+cKcqyz4k9yU=
github.com/mcstatus-io/mcutil v0.0.0-20230331012121-a2cb94e8ebfe/go.mod h1:VUB87/x9EYITmQVXZO4eS+egaZOdvxId4IdpU4L5LoA=
github.com/mcstatus-io/mcutil v0.0.0-20230416213822-e8ba7a2ea0dd h1:RL36cvcSw53zICdg0CtPD72RuUUH/MgwcEaLz6pE84s=
github.com/mcstatus-io/mcutil v0.0.0-20230416213822-e8ba7a2ea0dd/go.mod h1:VUB87/x9EYITmQVXZO4eS+egaZOdvxId4IdpU4L5LoA=
github.com/mcstatus-io/mcutil v1.0.0 h1:QoULNHXbzYLC2PN1OG3rX4I8AXUivvTFjSpbf5aQqcQ=
github.com/mcstatus-io/mcutil v1.0.0/go.mod h1:VUB87/x9EYITmQVXZO4eS+egaZOdvxId4IdpU4L5LoA=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=

View File

@ -11,8 +11,8 @@ import (
func init() {
app.Get("/ping", PingHandler)
app.Get("/status/java/:address", JavaStatusHandler)
app.Get("/widget/java/:address", JavaWidgetHandler)
app.Get("/status/bedrock/:address", BedrockStatusHandler)
app.Get("/widget/java/:address", JavaWidgetHandler)
app.Get("/icon", DefaultIconHandler)
app.Get("/icon/:address", IconHandler)
app.Use(NotFoundHandler)

View File

@ -4,10 +4,15 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
"strconv"
"strings"
"sync"
"time"
"github.com/mcstatus-io/mcutil"
"github.com/mcstatus-io/mcutil/description"
"github.com/mcstatus-io/mcutil/options"
"github.com/mcstatus-io/mcutil/response"
)
// StatusResponse is the root response for returning any status response from the API.
@ -28,11 +33,13 @@ type JavaStatusResponse struct {
// JavaStatus is the status response properties for Java Edition.
type JavaStatus struct {
Version *JavaVersion `json:"version"`
Players JavaPlayers `json:"players"`
MOTD MOTD `json:"motd"`
Icon *string `json:"icon"`
Mods []Mod `json:"mods"`
Version *JavaVersion `json:"version"`
Players JavaPlayers `json:"players"`
MOTD MOTD `json:"motd"`
Icon *string `json:"icon"`
Mods []Mod `json:"mods"`
Software *string `json:"software"`
Plugins []Plugin `json:"plugins"`
}
// BedrockStatusResponse is the combined response of the root response and the Bedrock Edition status response.
@ -99,6 +106,12 @@ type Mod struct {
Version string `json:"version"`
}
// Plugin is a plugin that is enabled on a Java Edition server.
type Plugin struct {
Name string `json:"name"`
Version *string `json:"version"`
}
// GetJavaStatus returns the status response of a Java Edition server, either using cache or fetching a fresh status.
func GetJavaStatus(host string, port uint16) (*JavaStatusResponse, time.Duration, error) {
cacheKey := fmt.Sprintf("java:%s-%d", host, port)
@ -117,11 +130,7 @@ func GetJavaStatus(host string, port uint16) (*JavaStatusResponse, time.Duration
return &response, ttl, err
}
response, err := FetchJavaStatus(host, port)
if err != nil {
return nil, 0, err
}
response := FetchJavaStatus(host, port)
data, err := json.Marshal(response)
@ -133,7 +142,7 @@ func GetJavaStatus(host string, port uint16) (*JavaStatusResponse, time.Duration
return nil, 0, err
}
return response, 0, nil
return &response, 0, nil
}
// GetBedrockStatus returns the status response of a Bedrock Edition server, either using cache or fetching a fresh status.
@ -208,118 +217,36 @@ func GetServerIcon(host string, port uint16) ([]byte, time.Duration, error) {
return icon, 0, nil
}
// FetchJavaStatus fetches a fresh status of a Java Edition server.
func FetchJavaStatus(host string, port uint16) (*JavaStatusResponse, error) {
status, err := mcutil.Status(host, port)
// FetchJavaStatus fetches fresh information about a Java Edition Minecraft server.
func FetchJavaStatus(host string, port uint16) JavaStatusResponse {
var wg sync.WaitGroup
if err != nil {
statusLegacy, err := mcutil.StatusLegacy(host, port)
wg.Add(2)
if err != nil {
return &JavaStatusResponse{
StatusResponse: StatusResponse{
Online: false,
Host: host,
Port: port,
EULABlocked: IsBlockedAddress(host),
RetrievedAt: time.Now().UnixMilli(),
ExpiresAt: time.Now().Add(conf.Cache.JavaStatusDuration).UnixMilli(),
},
}, nil
var status interface{} = nil
var query *response.FullQuery = nil
go func() {
defer wg.Done()
if result, _ := mcutil.Status(host, port); result != nil {
status = result
} else if result, _ := mcutil.StatusLegacy(host, port); result != nil {
status = result
}
}()
response := &JavaStatusResponse{
StatusResponse: StatusResponse{
Online: true,
Host: host,
Port: port,
EULABlocked: IsBlockedAddress(host),
RetrievedAt: time.Now().UnixMilli(),
ExpiresAt: time.Now().Add(conf.Cache.JavaStatusDuration).UnixMilli(),
},
JavaStatus: &JavaStatus{
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),
},
}
go func() {
defer wg.Done()
if statusLegacy.Version != nil {
response.Version = &JavaVersion{
NameRaw: statusLegacy.Version.NameRaw,
NameClean: statusLegacy.Version.NameClean,
NameHTML: statusLegacy.Version.NameHTML,
Protocol: statusLegacy.Version.Protocol,
}
}
query, _ = mcutil.FullQuery(host, port, options.Query{
Timeout: time.Second,
})
}()
return response, nil
}
wg.Wait()
playerList := make([]Player, 0)
if status.Players.Sample != nil {
for _, player := range status.Players.Sample {
playerList = append(playerList, Player{
UUID: player.ID,
NameRaw: player.NameRaw,
NameClean: player.NameClean,
NameHTML: player.NameHTML,
})
}
}
modList := make([]Mod, 0)
if status.ModInfo != nil {
for _, mod := range status.ModInfo.Mods {
modList = append(modList, Mod{
Name: mod.ID,
Version: mod.Version,
})
}
}
return &JavaStatusResponse{
StatusResponse: StatusResponse{
Online: true,
Host: host,
Port: port,
EULABlocked: IsBlockedAddress(host),
RetrievedAt: time.Now().UnixMilli(),
ExpiresAt: time.Now().Add(conf.Cache.JavaStatusDuration).UnixMilli(),
},
JavaStatus: &JavaStatus{
Version: &JavaVersion{
NameRaw: status.Version.NameRaw,
NameClean: status.Version.NameClean,
NameHTML: status.Version.NameHTML,
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,
},
}, nil
return BuildJavaResponse(host, port, status, query)
}
// FetchBedrockStatus fetches a fresh status of a Bedrock Edition server.
@ -412,3 +339,174 @@ func FetchBedrockStatus(host string, port uint16) (*BedrockStatusResponse, error
return response, nil
}
// BuildJavaResponse builds the response data from the status and query information.
func BuildJavaResponse(host string, port uint16, status interface{}, query *response.FullQuery) (result JavaStatusResponse) {
result = JavaStatusResponse{
StatusResponse: StatusResponse{
Online: status != nil || query != nil,
Host: host,
Port: port,
EULABlocked: IsBlockedAddress(host),
RetrievedAt: time.Now().UnixMilli(),
ExpiresAt: time.Now().Add(conf.Cache.JavaStatusDuration).UnixMilli(),
},
JavaStatus: nil,
}
if status == nil && query == nil {
return
}
result.JavaStatus = &JavaStatus{
Players: JavaPlayers{
List: make([]Player, 0),
},
Mods: make([]Mod, 0),
Plugins: make([]Plugin, 0),
}
if status != nil {
switch s := status.(type) {
case *response.JavaStatus:
{
result.Version = &JavaVersion{
NameRaw: s.Version.NameRaw,
NameClean: s.Version.NameClean,
NameHTML: s.Version.NameHTML,
Protocol: s.Version.Protocol,
}
result.Players = JavaPlayers{
Online: s.Players.Online,
Max: s.Players.Max,
List: make([]Player, 0),
}
result.MOTD = MOTD{
Raw: s.MOTD.Raw,
Clean: s.MOTD.Clean,
HTML: s.MOTD.HTML,
}
result.Icon = s.Favicon
if s.Players.Sample != nil {
for _, player := range s.Players.Sample {
result.Players.List = append(result.Players.List, Player{
UUID: player.ID,
NameRaw: player.NameRaw,
NameClean: player.NameClean,
NameHTML: player.NameHTML,
})
}
}
if s.ModInfo != nil {
for _, mod := range s.ModInfo.Mods {
result.Mods = append(result.Mods, Mod{
Name: mod.ID,
Version: mod.Version,
})
}
}
break
}
case *response.JavaStatusLegacy:
{
if s.Version != nil {
result.Version = &JavaVersion{
NameRaw: s.Version.NameRaw,
NameClean: s.Version.NameClean,
NameHTML: s.Version.NameHTML,
Protocol: s.Version.Protocol,
}
}
result.Players = JavaPlayers{
Online: &s.Players.Online,
Max: &s.Players.Max,
List: make([]Player, 0),
}
result.MOTD = MOTD{
Raw: s.MOTD.Raw,
Clean: s.MOTD.Clean,
HTML: s.MOTD.HTML,
}
break
}
default:
panic(fmt.Errorf("unknown status type: %T", status))
}
}
if query != nil {
if status == nil {
if motd, ok := query.Data["hostname"]; ok {
if parsedMOTD, err := description.ParseFormatting(motd); err == nil {
result.MOTD = MOTD{
Raw: parsedMOTD.Raw,
Clean: parsedMOTD.Clean,
HTML: parsedMOTD.HTML,
}
}
}
if onlinePlayers, ok := query.Data["numplayers"]; ok {
value, err := strconv.ParseInt(onlinePlayers, 10, 64)
if err == nil {
result.Players.Online = &value
}
}
if maxPlayers, ok := query.Data["maxplayers"]; ok {
value, err := strconv.ParseInt(maxPlayers, 10, 64)
if err == nil {
result.Players.Max = &value
}
}
for _, playerName := range query.Players {
parsedName, err := description.ParseFormatting(playerName)
if err == nil {
result.Players.List = append(result.Players.List, Player{
UUID: "",
NameRaw: parsedName.Raw,
NameClean: parsedName.Clean,
NameHTML: parsedName.HTML,
})
}
}
}
if plugins, ok := query.Data["plugins"]; ok {
if softwareSplit := strings.Split(strings.Trim(plugins, " "), ":"); len(softwareSplit) > 1 {
result.Software = PointerOf(strings.Trim(softwareSplit[0], " "))
for _, plugin := range strings.Split(softwareSplit[1], ";") {
pluginSplit := strings.Split(strings.Trim(plugin, " "), " ")
if len(pluginSplit) > 1 {
result.Plugins = append(result.Plugins, Plugin{
Name: pluginSplit[0],
Version: PointerOf(pluginSplit[1]),
})
} else {
result.Plugins = append(result.Plugins, Plugin{
Name: pluginSplit[0],
Version: nil,
})
}
}
}
}
}
return
}

View File

@ -230,3 +230,8 @@ func ScaleImageNearestNeighbor(img image.Image, sx, sy int) image.Image {
return out
}
// PointerOf returns a pointer of the argument passed.
func PointerOf[T any](v T) *T {
return &v
}