Skip to content

Commit

Permalink
feat: add placeholder status
Browse files Browse the repository at this point in the history
  • Loading branch information
haveachin committed Mar 1, 2024
1 parent 12d0e7b commit 62a98bf
Show file tree
Hide file tree
Showing 6 changed files with 250 additions and 176 deletions.
1 change: 0 additions & 1 deletion pkg/infrared/conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,6 @@ type clientConn struct {
handshake handshaking.ServerBoundHandshake
loginStart login.ServerBoundLoginStart
reqDomain ServerDomain
protoVer protocol.Version
}

func newClientConn(c net.Conn) (*clientConn, func()) {
Expand Down
204 changes: 204 additions & 0 deletions pkg/infrared/handshake_response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
package infrared

import (
"encoding/base64"
"encoding/json"
_ "image/png"

Check warning on line 6 in pkg/infrared/handshake_response.go

View workflow job for this annotation

GitHub Actions / Lint

blank-imports: a blank import should be only in a main or test package, or have a comment justifying it (revive)
"os"
"strings"
"sync"

"github.com/haveachin/infrared/pkg/infrared/protocol"
"github.com/haveachin/infrared/pkg/infrared/protocol/login"
"github.com/haveachin/infrared/pkg/infrared/protocol/status"
)

type PlayerSampleConfig struct {
Name string `yaml:"name"`
UUID string `yaml:"uuid"`
}

type PlayerSamples []PlayerSampleConfig

func (ps PlayerSamples) PlayerSampleJSON() []status.PlayerSampleJSON {
psJSON := make([]status.PlayerSampleJSON, len(ps))
for i, s := range ps {
psJSON[i] = status.PlayerSampleJSON{
Name: s.Name,
ID: s.UUID,
}
}
return psJSON
}

type HandshakeStatusResponseConfig struct {
VersionName string `yaml:"versionName"`
ProtocolNumber int `yaml:"protocolNumber"`
MaxPlayerCount int `yaml:"maxPlayerCount"`
PlayerCount int `yaml:"playerCount"`
PlayerSamples PlayerSamples `yaml:"playerSamples"`
Icon string `yaml:"icon"`
MOTD string `yaml:"motd"`
}

type HandshakeResponseConfig struct {
StatusConfig HandshakeStatusResponseConfig `yaml:"status"`
Message string `yaml:"message"`
}

type HandshakeResponse struct {
Config HandshakeResponseConfig

statusOnce sync.Once
statusRespJSON status.ResponseJSON
statusRespPk protocol.Packet

loginOnce sync.Once
loginRespPk protocol.Packet
}

func (r *HandshakeResponse) StatusResponse(protVer protocol.Version) (status.ResponseJSON, protocol.Packet) {
r.statusOnce.Do(func() {
cfg := r.Config.StatusConfig

protNum := cfg.ProtocolNumber
if protNum < 0 {
protNum = int(protVer)
}

r.statusRespJSON = status.ResponseJSON{
Version: status.VersionJSON{
Name: cfg.VersionName,
Protocol: protNum,
},
Players: status.PlayersJSON{
Max: cfg.MaxPlayerCount,
Online: cfg.PlayerCount,
Sample: cfg.PlayerSamples.PlayerSampleJSON(),
},
Favicon: parseServerIcon(cfg.Icon),
Description: parseJSONTextComponent(cfg.MOTD),
}

respBytes, err := json.Marshal(r.statusRespJSON)
if err != nil {
panic(err)
}

statusPk := status.ClientBoundResponse{
JSONResponse: protocol.String(string(respBytes)),
}

if err := statusPk.Marshal(&r.statusRespPk); err != nil {
panic(err)
}
})

return r.statusRespJSON, r.statusRespPk
}

func (r *HandshakeResponse) LoginReponse() protocol.Packet {
r.loginOnce.Do(func() {
msg := parseJSONTextComponent(r.Config.Message)
disconnectPk := login.ClientBoundDisconnect{
Reason: protocol.String(msg),
}

if err := disconnectPk.Marshal(&r.loginRespPk); err != nil {
panic(err)
}
})

return r.loginRespPk
}

type OverrideStatusResponseConfig struct {
VersionName *string `yaml:"versionName"`
ProtocolNumber *int `yaml:"protocolNumber"`
MaxPlayerCount *int `yaml:"maxPlayerCount"`
PlayerCount *int `yaml:"playerCount"`
PlayerSamples PlayerSamples `yaml:"playerSamples"`
Icon *string `yaml:"icon"`
MOTD *string `yaml:"motd"`
}

type OverrideStatusResponse struct {
Config OverrideStatusResponseConfig

once sync.Once
overrideFn func(resp status.ResponseJSON) status.ResponseJSON
}

func (r *OverrideStatusResponse) OverrideStatusResponseJSON(resp status.ResponseJSON) status.ResponseJSON {

Check failure on line 132 in pkg/infrared/handshake_response.go

View workflow job for this annotation

GitHub Actions / Lint

cognitive complexity 21 of func `(*OverrideStatusResponse).OverrideStatusResponseJSON` is high (> 20) (gocognit)
r.once.Do(func() {
cfg := r.Config
icon := parseServerIcon(*cfg.Icon)
playerSamples := cfg.PlayerSamples.PlayerSampleJSON()
motd := parseJSONTextComponent(*cfg.MOTD)

r.overrideFn = func(resp status.ResponseJSON) status.ResponseJSON {
if cfg.Icon != nil {
resp.Favicon = icon
}

if cfg.VersionName != nil {
resp.Version.Name = *cfg.VersionName
}

if cfg.ProtocolNumber != nil {
resp.Version.Protocol = *cfg.ProtocolNumber
}

if cfg.MaxPlayerCount != nil {
resp.Players.Max = *cfg.MaxPlayerCount
}

if cfg.PlayerCount != nil {
resp.Players.Online = *cfg.PlayerCount
}

if len(cfg.PlayerSamples) != 0 {
resp.Players.Sample = playerSamples
}

if cfg.MOTD != nil {
resp.Description = motd
}

return resp
}
})

return r.overrideFn(resp)
}

func parseServerIcon(iconStr string) string {
if iconStr == "" {
return ""
}

const base64PNGHeader = "data:image/png;base64,"
if strings.HasPrefix(iconStr, base64PNGHeader) {
return iconStr
}

iconBytes, err := os.ReadFile(iconStr)
if err != nil {
Log.Error().
Err(err).
Str("iconPath", iconStr).
Msg("Failed to open icon file")
return ""
}

iconBase64 := base64.StdEncoding.EncodeToString(iconBytes)
return base64PNGHeader + iconBase64
}

func parseJSONTextComponent(s string) json.RawMessage {
var motdJSON json.RawMessage
if err := json.Unmarshal([]byte(s), &motdJSON); err != nil {
motdJSON = []byte(`{"text":"` + s + `"}`)
}
return motdJSON
}
11 changes: 10 additions & 1 deletion pkg/infrared/infrared.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,12 @@ func (ir *Infrared) handleConn(c *clientConn) error {
ReadPackets: c.readPks,
})
if err != nil {
return err
switch {
case errors.Is(err, ErrServerNotReachable) && c.handshake.IsLoginRequest():
return ir.handleLoginDisconnect(c, resp)
default:
return err
}
}

if c.handshake.IsStatusRequest() {
Expand All @@ -273,6 +278,10 @@ func handleStatus(c *clientConn, resp ServerResponse) error {
return nil
}

func (ir *Infrared) handleLoginDisconnect(c *clientConn, resp ServerResponse) error {
return c.WritePacket(resp.StatusResponse)
}

func (ir *Infrared) handleLogin(c *clientConn, resp ServerResponse) error {
hsVersion := protocol.Version(c.handshake.ProtocolVersion)
if err := c.loginStart.Unmarshal(c.readPks[1], hsVersion); err != nil {
Expand Down
10 changes: 7 additions & 3 deletions pkg/infrared/protocol/status/clientbound_response.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package status

import "github.com/haveachin/infrared/pkg/infrared/protocol"
import (
"encoding/json"

"github.com/haveachin/infrared/pkg/infrared/protocol"
)

const (
ClientBoundResponseID int32 = 0x00
Expand Down Expand Up @@ -31,8 +35,8 @@ type ResponseJSON struct {
Version VersionJSON `json:"version"`
Players PlayersJSON `json:"players"`
// This has to be any to support the new chat style system
Description any `json:"description"`
Favicon string `json:"favicon,omitempty"`
Description json.RawMessage `json:"description"`
Favicon string `json:"favicon,omitempty"`
// Added since 1.19
PreviewsChat bool `json:"previewsChat"`
// Added since 1.19.1
Expand Down
63 changes: 29 additions & 34 deletions pkg/infrared/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ import (
)

var (
ErrNoServers = errors.New("no servers to route to")

errServerNotReachable = errors.New("server not reachable")
ErrNoServers = errors.New("no servers to route to")
ErrServerNotFound = errors.New("server not found")
ErrServerNotReachable = errors.New("server not reachable")
)

type (
Expand All @@ -29,11 +29,11 @@ type (
)

type ServerConfig struct {
Domains []ServerDomain `yaml:"domains"`
Addresses []ServerAddress `yaml:"addresses"`
SendProxyProtocol bool `yaml:"sendProxyProtocol"`
ServerStatusResponse ServerStatusResponseConfig `yaml:"statusResponse"`
OverrideServerStatusResponse OverrideServerStatusResponseConfig `yaml:"overrideStatusResponse"`
Domains []ServerDomain `yaml:"domains"`
Addresses []ServerAddress `yaml:"addresses"`
SendProxyProtocol bool `yaml:"sendProxyProtocol"`
DialTimeoutResponse HandshakeResponseConfig `yaml:"dialTimeoutResponse"`
OverrideStatusResponse OverrideStatusResponseConfig `yaml:"overrideStatus"`
}

func NewServerConfig() ServerConfig {
Expand All @@ -52,6 +52,9 @@ func (cfg ServerConfig) WithAddresses(addr ...ServerAddress) ServerConfig {

type Server struct {
cfg ServerConfig

dialTimeoutResp HandshakeResponse
overrideStatusResp OverrideStatusResponse
}

func NewServer(cfg ServerConfig) (*Server, error) {
Expand All @@ -61,10 +64,16 @@ func NewServer(cfg ServerConfig) (*Server, error) {

return &Server{
cfg: cfg,
dialTimeoutResp: HandshakeResponse{
Config: cfg.DialTimeoutResponse,
},
overrideStatusResp: OverrideStatusResponse{
Config: cfg.OverrideStatusResponse,
},
}, nil
}

func (s Server) Dial() (*ServerConn, error) {
func (s *Server) Dial() (*ServerConn, error) {
c, err := net.Dial("tcp", string(s.cfg.Addresses[0]))
if err != nil {
return nil, err
Expand Down Expand Up @@ -143,7 +152,7 @@ func (sg *ServerGateway) findServer(domain ServerDomain) *Server {
func (sg *ServerGateway) RequestServer(req ServerRequest) (ServerResponse, error) {
srv := sg.findServer(req.Domain)
if srv == nil {
return ServerResponse{}, errors.New("server not found")
return ServerResponse{}, ErrServerNotFound
}

return sg.responder.RespondeToServerRequest(req, srv)
Expand All @@ -168,7 +177,9 @@ func (r DialServerResponder) RespondeToServerRequest(req ServerRequest, srv *Ser
func (r DialServerResponder) respondeToLoginRequest(_ ServerRequest, srv *Server) (ServerResponse, error) {
rc, err := srv.Dial()
if err != nil {
return ServerResponse{}, err
return ServerResponse{
StatusResponse: srv.dialTimeoutResp.LoginReponse(),
}, ErrServerNotReachable
}

return ServerResponse{
Expand Down Expand Up @@ -235,28 +246,12 @@ func (s *statusResponseProvider) StatusResponse(
}

statusResp, statusPk, err := s.requestNewStatusResponseJSON(cliAddr, readPks)
switch {
case errors.Is(err, errServerNotReachable):
respJSON := status.ResponseJSON{
Version: status.VersionJSON{
Name: "Infrared",
Protocol: int(protocol.Version1_20_2),
},
Description: status.DescriptionJSON{
Text: "Hello there!",
},
}
bb, err := json.Marshal(respJSON)
if err != nil {
return status.ResponseJSON{}, protocol.Packet{}, err
}
pk := protocol.Packet{}
status.ClientBoundResponse{
JSONResponse: protocol.String(string(bb)),
}.Marshal(&pk)
return respJSON, pk, nil
default:
if err != nil {
if err != nil {
switch {
case errors.Is(err, ErrServerNotReachable):
respJSON, respPk := s.server.dialTimeoutResp.StatusResponse(protVer)
return respJSON, respPk, nil
default:
return status.ResponseJSON{}, protocol.Packet{}, err
}
}
Expand All @@ -276,7 +271,7 @@ func (s *statusResponseProvider) requestNewStatusResponseJSON(
) (status.ResponseJSON, protocol.Packet, error) {
rc, err := s.server.Dial()
if err != nil {
return status.ResponseJSON{}, protocol.Packet{}, errServerNotReachable
return status.ResponseJSON{}, protocol.Packet{}, ErrServerNotReachable
}

if s.server.cfg.SendProxyProtocol {
Expand Down
Loading

0 comments on commit 62a98bf

Please sign in to comment.