From 62a98bf062d7460f90b78df44c85d72ea9027a2e Mon Sep 17 00:00:00 2001 From: haveachin Date: Fri, 1 Mar 2024 14:21:10 +0100 Subject: [PATCH] feat: add placeholder status --- pkg/infrared/conn.go | 1 - pkg/infrared/handshake_response.go | 204 ++++++++++++++++++ pkg/infrared/infrared.go | 11 +- .../protocol/status/clientbound_response.go | 10 +- pkg/infrared/server.go | 63 +++--- pkg/infrared/status_response.go | 137 ------------ 6 files changed, 250 insertions(+), 176 deletions(-) create mode 100644 pkg/infrared/handshake_response.go delete mode 100644 pkg/infrared/status_response.go diff --git a/pkg/infrared/conn.go b/pkg/infrared/conn.go index 49d01d6..5bc786c 100644 --- a/pkg/infrared/conn.go +++ b/pkg/infrared/conn.go @@ -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()) { diff --git a/pkg/infrared/handshake_response.go b/pkg/infrared/handshake_response.go new file mode 100644 index 0000000..48f42f1 --- /dev/null +++ b/pkg/infrared/handshake_response.go @@ -0,0 +1,204 @@ +package infrared + +import ( + "encoding/base64" + "encoding/json" + _ "image/png" + "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 { + 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 +} diff --git a/pkg/infrared/infrared.go b/pkg/infrared/infrared.go index 140c64e..305eca1 100644 --- a/pkg/infrared/infrared.go +++ b/pkg/infrared/infrared.go @@ -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() { @@ -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 { diff --git a/pkg/infrared/protocol/status/clientbound_response.go b/pkg/infrared/protocol/status/clientbound_response.go index a3c223c..874a73f 100644 --- a/pkg/infrared/protocol/status/clientbound_response.go +++ b/pkg/infrared/protocol/status/clientbound_response.go @@ -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 @@ -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 diff --git a/pkg/infrared/server.go b/pkg/infrared/server.go index 44a3a02..25edc53 100644 --- a/pkg/infrared/server.go +++ b/pkg/infrared/server.go @@ -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 ( @@ -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 { @@ -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) { @@ -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 @@ -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) @@ -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{ @@ -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 } } @@ -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 { diff --git a/pkg/infrared/status_response.go b/pkg/infrared/status_response.go deleted file mode 100644 index 3ac927b..0000000 --- a/pkg/infrared/status_response.go +++ /dev/null @@ -1,137 +0,0 @@ -package infrared - -import ( - "encoding/base64" - _ "image/png" - "os" - "strings" - "sync" - - "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 { - ss := make([]status.PlayerSampleJSON, len(ps)) - for i, s := range ps { - ss[i] = status.PlayerSampleJSON{ - Name: s.Name, - ID: s.UUID, - } - } - return ss -} - -type ServerStatusResponseConfig 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"` - - once sync.Once - respJSON status.ResponseJSON -} - -func (r ServerStatusResponseConfig) ResponseJSON() status.ResponseJSON { - r.once.Do(func() { - r.respJSON = status.ResponseJSON{ - Version: status.VersionJSON{ - Name: r.VersionName, - Protocol: r.ProtocolNumber, - }, - Players: status.PlayersJSON{ - Max: r.MaxPlayerCount, - Online: r.PlayerCount, - Sample: r.PlayerSamples.PlayerSampleJSON(), - }, - Favicon: parseServerIcon(r.Icon), - Description: status.DescriptionJSON{ - Text: r.MOTD, - }, - } - }) - - return r.respJSON -} - -type OverrideServerStatusResponseConfig 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"` - - iconOnce sync.Once -} - -func (r OverrideServerStatusResponseConfig) OverrideResponseJSON(resp status.ResponseJSON) status.ResponseJSON { - if r.Icon != nil { - r.iconOnce.Do(func() { - icon := parseServerIcon(*r.Icon) - r.Icon = &icon - }) - resp.Favicon = *r.Icon - } - - if r.VersionName != nil { - resp.Version.Name = *r.VersionName - } - - if r.ProtocolNumber != nil { - resp.Version.Protocol = *r.ProtocolNumber - } - - if r.MaxPlayerCount != nil { - resp.Players.Max = *r.MaxPlayerCount - } - - if r.PlayerCount != nil { - resp.Players.Online = *r.PlayerCount - } - - if len(r.PlayerSamples) != 0 { - resp.Players.Sample = r.PlayerSamples.PlayerSampleJSON() - } - - if r.MOTD != nil { - resp.Description = status.DescriptionJSON{ - Text: *r.MOTD, - } - } - - return 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 -}