Support query lookup for Java servers
This commit is contained in:
2
go.mod
2
go.mod
@@ -7,7 +7,7 @@ require (
|
|||||||
github.com/go-redis/redis/v8 v8.11.5
|
github.com/go-redis/redis/v8 v8.11.5
|
||||||
github.com/gofiber/fiber/v2 v2.43.0
|
github.com/gofiber/fiber/v2 v2.43.0
|
||||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
|
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
|
golang.org/x/image v0.6.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|||||||
6
go.sum
6
go.sum
@@ -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-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 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
||||||
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
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 v1.0.0 h1:QoULNHXbzYLC2PN1OG3rX4I8AXUivvTFjSpbf5aQqcQ=
|
||||||
github.com/mcstatus-io/mcutil v0.0.0-20230331012121-a2cb94e8ebfe/go.mod h1:VUB87/x9EYITmQVXZO4eS+egaZOdvxId4IdpU4L5LoA=
|
github.com/mcstatus-io/mcutil v1.0.0/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/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||||
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
|
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import (
|
|||||||
func init() {
|
func init() {
|
||||||
app.Get("/ping", PingHandler)
|
app.Get("/ping", PingHandler)
|
||||||
app.Get("/status/java/:address", JavaStatusHandler)
|
app.Get("/status/java/:address", JavaStatusHandler)
|
||||||
app.Get("/widget/java/:address", JavaWidgetHandler)
|
|
||||||
app.Get("/status/bedrock/:address", BedrockStatusHandler)
|
app.Get("/status/bedrock/:address", BedrockStatusHandler)
|
||||||
|
app.Get("/widget/java/:address", JavaWidgetHandler)
|
||||||
app.Get("/icon", DefaultIconHandler)
|
app.Get("/icon", DefaultIconHandler)
|
||||||
app.Get("/icon/:address", IconHandler)
|
app.Get("/icon/:address", IconHandler)
|
||||||
app.Use(NotFoundHandler)
|
app.Use(NotFoundHandler)
|
||||||
|
|||||||
318
src/status.go
318
src/status.go
@@ -4,10 +4,15 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mcstatus-io/mcutil"
|
"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.
|
// StatusResponse is the root response for returning any status response from the API.
|
||||||
@@ -33,6 +38,8 @@ type JavaStatus struct {
|
|||||||
MOTD MOTD `json:"motd"`
|
MOTD MOTD `json:"motd"`
|
||||||
Icon *string `json:"icon"`
|
Icon *string `json:"icon"`
|
||||||
Mods []Mod `json:"mods"`
|
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.
|
// 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"`
|
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.
|
// 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) {
|
func GetJavaStatus(host string, port uint16) (*JavaStatusResponse, time.Duration, error) {
|
||||||
cacheKey := fmt.Sprintf("java:%s-%d", host, port)
|
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
|
return &response, ttl, err
|
||||||
}
|
}
|
||||||
|
|
||||||
response, err := FetchJavaStatus(host, port)
|
response := FetchJavaStatus(host, port)
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := json.Marshal(response)
|
data, err := json.Marshal(response)
|
||||||
|
|
||||||
@@ -133,7 +142,7 @@ func GetJavaStatus(host string, port uint16) (*JavaStatusResponse, time.Duration
|
|||||||
return nil, 0, err
|
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.
|
// 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
|
return icon, 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// FetchJavaStatus fetches a fresh status of a Java Edition server.
|
// FetchJavaStatus fetches fresh information about a Java Edition Minecraft server.
|
||||||
func FetchJavaStatus(host string, port uint16) (*JavaStatusResponse, error) {
|
func FetchJavaStatus(host string, port uint16) JavaStatusResponse {
|
||||||
status, err := mcutil.Status(host, port)
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
if err != nil {
|
wg.Add(2)
|
||||||
statusLegacy, err := mcutil.StatusLegacy(host, port)
|
|
||||||
|
|
||||||
if err != nil {
|
var status interface{} = nil
|
||||||
return &JavaStatusResponse{
|
var query *response.FullQuery = nil
|
||||||
StatusResponse: StatusResponse{
|
|
||||||
Online: false,
|
go func() {
|
||||||
Host: host,
|
defer wg.Done()
|
||||||
Port: port,
|
|
||||||
EULABlocked: IsBlockedAddress(host),
|
if result, _ := mcutil.Status(host, port); result != nil {
|
||||||
RetrievedAt: time.Now().UnixMilli(),
|
status = result
|
||||||
ExpiresAt: time.Now().Add(conf.Cache.JavaStatusDuration).UnixMilli(),
|
} else if result, _ := mcutil.StatusLegacy(host, port); result != nil {
|
||||||
},
|
status = result
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
response := &JavaStatusResponse{
|
go func() {
|
||||||
StatusResponse: StatusResponse{
|
defer wg.Done()
|
||||||
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),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if statusLegacy.Version != nil {
|
query, _ = mcutil.FullQuery(host, port, options.Query{
|
||||||
response.Version = &JavaVersion{
|
Timeout: time.Second,
|
||||||
NameRaw: statusLegacy.Version.NameRaw,
|
|
||||||
NameClean: statusLegacy.Version.NameClean,
|
|
||||||
NameHTML: statusLegacy.Version.NameHTML,
|
|
||||||
Protocol: statusLegacy.Version.Protocol,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return response, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
wg.Wait()
|
||||||
|
|
||||||
if status.ModInfo != nil {
|
return BuildJavaResponse(host, port, status, query)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// FetchBedrockStatus fetches a fresh status of a Bedrock Edition server.
|
// 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
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -230,3 +230,8 @@ func ScaleImageNearestNeighbor(img image.Image, sx, sy int) image.Image {
|
|||||||
|
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PointerOf returns a pointer of the argument passed.
|
||||||
|
func PointerOf[T any](v T) *T {
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user