diff --git a/chat.go b/chat.go new file mode 100644 index 0000000..3bcd3b5 --- /dev/null +++ b/chat.go @@ -0,0 +1,255 @@ + +package liter + +import ( + "encoding/json" + "fmt" + "strings" +) + +type OptBool int +const ( + TRUE OptBool = 1 + FALSE OptBool = -1 +) + +var _ json.Marshaler = (OptBool)(0) +var _ json.Unmarshaler = (*OptBool)(nil) + +func (b OptBool)String()(string){ + switch b { + case TRUE: return "true" + case FALSE: return "false" + default: return "" + } +} + +func (b OptBool)IsDefined()(bool){ + switch b { + case TRUE, FALSE: return true + default: return false + } +} + +func (b OptBool)Bool()(bool){ + switch b { + case TRUE: return true + case FALSE: return false + default: panic("Cannot turn undefined optional bool to bool") + } +} + +func (b OptBool)ToBool(def bool)(bool){ + switch b { + case TRUE: return true + case FALSE: return false + default: return def + } +} + +func (b OptBool)MarshalJSON()(buf []byte, err error){ + return json.Marshal(b.ToBool(false)) +} + +func (b *OptBool)UnmarshalJSON(buf []byte)(err error){ + var v bool + if err = json.Unmarshal(buf, &v); err != nil { + return + } + if v { + *b = TRUE + }else{ + *b = FALSE + } + return +} + +type ClickAction string +const ( + OpenUrl ClickAction = "open_url" + RunCommand ClickAction = "run_command" + SuggestCommand ClickAction = "suggest_command" + ChangePage ClickAction = "change_page" + CopyToClipboard ClickAction = "copy_to_clipboard" +) + +type HoverAction string +const ( + ShowText HoverAction = "show_text" + ShowItem HoverAction = "show_item" + ShowEntity HoverAction = "show_entity" +) + +type ( + Chat struct { + Component + Extra []*Component `json:"extra,omitempty"` + } + Component struct { + // content: parse by order + Text string `json:"text,omitempty"` + Translate string `json:"translate,omitempty"` + With []*Component `json:"with,omitempty"` // the values for translate + Keybind string `json:"keybind,omitempty"` + Score *ScoreComponent `json:"score,omitempty"` + // properties + Bold OptBool `json:"bold,omitempty"` + Italic OptBool `json:"italic,omitempty"` + Underlined OptBool `json:"underlined,omitempty"` + Strikethrough OptBool `json:"strikethrough,omitempty"` + Obfuscated OptBool `json:"obfuscated,omitempty"` + Font string `json:"font,omitempty"` + Color string `json:"color,omitempty"` + Insertion string `json:"insertion,omitempty"` + ClickEvent *ClickEvent `json:"clickEvent,omitempty"` + HoverEvent *HoverEvent `json:"hoverEvent,omitempty"` + } + ClickEvent struct { + Action ClickAction `json:"action"` + Value any `json:"value"` + } + HoverEvent struct { + Action HoverAction `json:"action"` + Content any `json:"content"` + } + + ScoreComponent struct { + Name string `json:"name"` + Objective string `json:"objective"` + Value int `json:"value"` + } +) + +var _ json.Marshaler = (*Chat)(nil) +var _ json.Unmarshaler = (*Chat)(nil) +var _ json.Marshaler = (*Component)(nil) +var _ json.Unmarshaler = (*Component)(nil) + +func NewChatFromString(s string)(c *Chat){ + return &Chat{ + Component: Component{ + Text: s, + }, + } +} + + +func (c *Component)toMap()(data map[string]any){ + data = make(map[string]any, 12) + if c.Text != "" { + data["text"] = c.Text + }else if c.Translate != "" { + data["translate"] = c.Translate + if len(c.With) != 0 { + data["with"] = c.With + } + }else if c.Keybind != "" { + data["keybind"] = c.Keybind + }else if c.Score != nil { + data["score"] = c.Score + }else{ + data["text"] = "" + } + if c.Bold != 0 { + data["bold"] = c.Bold.Bool() + } + if c.Italic != 0 { + data["italic"] = c.Italic.Bool() + } + if c.Underlined != 0 { + data["underlined"] = c.Underlined.Bool() + } + if c.Strikethrough != 0 { + data["strikethrough"] = c.Strikethrough.Bool() + } + if c.Obfuscated != 0 { + data["obfuscated"] = c.Obfuscated.Bool() + } + if c.ClickEvent != nil { + data["clickEvent"] = c.ClickEvent + } + if c.HoverEvent != nil { + data["hoverEvent"] = c.HoverEvent + } + return +} + +func (c *Component)MarshalJSON()(buf []byte, err error){ + return json.Marshal(c.toMap()) +} + +func (c *Component)UnmarshalJSON(buf []byte)(err error){ + return json.Unmarshal(buf, c) +} + +// Plain() will return the plain text value for the component +func (c *Component)Plain()(string){ + if c.Text != "" { + return c.Text + } + if c.Translate != "" { + return c.Translate + } + if c.Keybind != "" { + return c.Keybind + } + if c.Score != nil { + return fmt.Sprintf("", c.Score.Objective, c.Score.Name) + } + return "" +} + +type ChatType int +const ( + _ ChatType = iota + TextChat + TranslateChat + KeybindChat + ScoreChat +) + +// Type will return the content's type of the chat component. +func (c *Component)Type()(ChatType){ + if c.Text != "" { + return TextChat + } + if c.Translate != "" { + return TranslateChat + } + if c.Keybind != "" { + return KeybindChat + } + if c.Score != nil { + return ScoreChat + } + return TextChat +} + + +func (c *Chat)MarshalJSON()(buf []byte, err error){ + data := c.toMap() + if len(c.Extra) != 0 { + extra := make([]map[string]any, len(c.Extra)) + for i, v := range c.Extra { + extra[i] = v.toMap() + } + data["extra"] = extra + } + return json.Marshal(data) +} + +func (c *Chat)UnmarshalJSON(buf []byte)(err error){ + return json.Unmarshal(buf, c) +} + +func (c *Chat)Plain()(string){ + if len(c.Extra) == 0 { + return c.Component.Plain() + } + var sb strings.Builder + sb.WriteString(c.Component.Plain()) + for _, v := range c.Extra { + sb.WriteString(v.Plain()) + } + return sb.String() +} diff --git a/cmds/liter-server/globals.go b/cmds/liter-server/globals.go index f35116f..0647437 100644 --- a/cmds/liter-server/globals.go +++ b/cmds/liter-server/globals.go @@ -3,11 +3,11 @@ package main import ( "encoding/json" + "fmt" "net" "os" "path/filepath" "runtime" - "sort" "sync" "gopkg.in/yaml.v3" @@ -122,9 +122,73 @@ func readConfig()(cfg Config){ return } +var AuthClient = liter.DefaultAuthClient + +type PlayerInfo struct { + liter.PlayerInfo + fixed bool `json:"-"` +} + +func (p *PlayerInfo)UnmarshalJSON(buf []byte)(err error){ + var v any + if err = json.Unmarshal(buf, &v); err != nil { + return + } + switch v := v.(type) { + case string: + p.fixed = true + var err error + if p.Id, err = uuid.Parse(v); err == nil { + profile, err := AuthClient.GetPlayerProfile(p.Id) + if err != nil { + return nil + } + p.Name = profile.Name + break + } + p.Name = v + info, err := AuthClient.GetPlayerInfo(p.Name) + if err != nil { + return nil + } + p.Id = info.Id + case map[string]any: + var ok, ok2 bool + var id string + p.Name, ok = v["name"].(string) + if id, ok2 = v["id"].(string); ok2 { + var e error + if p.Id, e = uuid.Parse(id); e != nil { + ok2 = false + } + } + if !ok2 { + if !ok { + return fmt.Errorf("Unknown player info") + } + info, err := AuthClient.GetPlayerInfo(p.Name) + if err != nil { + return nil + } + p.fixed = true + p.Id = info.Id + }else if !ok { + profile, err := AuthClient.GetPlayerProfile(p.Id) + if err != nil { + return nil + } + p.fixed = true + p.Name = profile.Name + } + default: + return fmt.Errorf("Unexpected player info (%T)%v", v, v) + } + return +} + type Whitelist struct { - UUIDWhitelist []uuid.UUID `json:"uuids"` - IPWhitelist []string `json:"ips"` + Players []PlayerInfo `json:"players"` + IPs []string `json:"ips"` } var whitelist = loadWhitelist() @@ -140,8 +204,8 @@ func loadWhitelist()(wl Whitelist){ data, err := os.ReadFile(path) if err != nil { if os.IsNotExist(err) { - wl.UUIDWhitelist = make([]uuid.UUID, 0) - wl.IPWhitelist = make([]string, 0) + wl.Players = make([]PlayerInfo, 0) + wl.IPs = make([]string, 0) if data, err = json.MarshalIndent(wl, "", " "); err != nil { loger.Fatalf("Cannot encode config data: %v", err) return @@ -159,23 +223,34 @@ func loadWhitelist()(wl Whitelist){ loger.Fatalf("Cannot parse whitelist file: %v", err) return } - sort.Slice(wl.UUIDWhitelist, func(i, j int)(bool){ - return uuidLess(wl.UUIDWhitelist[i], wl.UUIDWhitelist[j]) - }) + for _, v := range wl.Players { + if v.fixed { + if data, err = json.MarshalIndent(wl, "", " "); err != nil { + loger.Fatalf("Cannot encode config data: %v", err) + return + } + if err = os.WriteFile(path, data, 0644); err != nil { + loger.Fatalf("Cannot rewrite config file: %v", err) + return + } + break + } + } loger.Infof("Whitelist is loaded") return } -func (wl Whitelist)HasUUID(id uuid.UUID)(ok bool){ - l := len(wl.UUIDWhitelist) - i := sort.Search(l, func(i int)(bool){ - return !uuidLess(wl.UUIDWhitelist[i], id) - }) - return i < l && wl.UUIDWhitelist[i] == id +func (wl Whitelist)HasPlayer(player liter.PlayerInfo)(ok bool){ + for _, p := range wl.Players { + if p.Name == player.Name || p.Id == player.Id { + return true + } + } + return false } func (wl Whitelist)IncludeIP(ip net.IP)(ok bool){ - for _, o := range wl.IPWhitelist { + for _, o := range wl.IPs { if matchIP(o, ip) { return true } @@ -185,9 +260,8 @@ func (wl Whitelist)IncludeIP(ip net.IP)(ok bool){ type Blacklist struct { - NameBlacklist []string `json:"names"` - UUIDBlacklist []uuid.UUID `json:"uuids"` - IPBlacklist []string `json:"ips"` + Players []PlayerInfo `json:"players"` + IPs []string `json:"ips"` } var blacklist = loadBlacklist() @@ -203,9 +277,8 @@ func loadBlacklist()(bl Blacklist){ data, err := os.ReadFile(path) if err != nil { if os.IsNotExist(err) { - bl.NameBlacklist = make([]string, 0) - bl.UUIDBlacklist = make([]uuid.UUID, 0) - bl.IPBlacklist = make([]string, 0) + bl.Players = make([]PlayerInfo, 0) + bl.IPs = make([]string, 0) if data, err = json.MarshalIndent(bl, "", " "); err != nil { loger.Fatalf("Cannot encode config data: %v", err) return @@ -223,66 +296,40 @@ func loadBlacklist()(bl Blacklist){ loger.Fatalf("Cannot parse blacklist file: %v", err) return } - sort.Strings(bl.NameBlacklist) - sort.Slice(bl.UUIDBlacklist, func(i, j int)(bool){ - return uuidLess(bl.UUIDBlacklist[i], bl.UUIDBlacklist[j]) - }) + for _, v := range bl.Players { + if v.fixed { + if data, err = json.MarshalIndent(bl, "", " "); err != nil { + loger.Fatalf("Cannot encode config data: %v", err) + return + } + if err = os.WriteFile(path, data, 0644); err != nil { + loger.Fatalf("Cannot create config file: %v", err) + return + } + } + } loger.Infof("Blacklist is loaded") return } -func (bl Blacklist)HasName(name string)(ok bool){ - i := sort.SearchStrings(bl.NameBlacklist, name) - return i < len(bl.NameBlacklist) && bl.NameBlacklist[i] == name -} - -func (bl Blacklist)HasUUID(id uuid.UUID)(ok bool){ - l := len(bl.UUIDBlacklist) - i := sort.Search(l, func(i int)(bool){ - return !uuidLess(bl.UUIDBlacklist[i], id) - }) - return i < l && bl.UUIDBlacklist[i] == id -} - -func (bl Blacklist)IncludeIP(ip net.IP)(ok bool){ - for _, o := range bl.IPBlacklist { - if matchIP(o, ip) { +func (bl Blacklist)HasPlayer(player liter.PlayerInfo)(ok bool){ + for _, p := range bl.Players { + if p.Name == player.Name || p.Id == player.Id { return true } } return false } - -func (cfg Config)CheckConn(ip net.IP, username string)(ok bool){ - wl := getWhitelist() - bl := getBlacklist() - - if bl.HasName(username) { - return false - } - if bl.IncludeIP(ip) { - return false - } - if cfg.EnableIPWhitelist { - if !wl.IncludeIP(ip) { - return false - } - } - _, uid, err := liter.DefaultAuthClient.GetPlayerUUID(username) - if err != nil { - loger.Errorf("Cannot get uuid of player '%s': %v", username, err) - return false - } - if cfg.EnableWhitelist { - if !wl.HasUUID(uid) { - return false +func (bl Blacklist)IncludeIP(ip net.IP)(ok bool){ + for _, o := range bl.IPs { + if matchIP(o, ip) { + return true } } - return true + return false } - type ServerIns struct { Id string `json:"id" yaml:"id"` Target string `json:"target" yaml:"target"` diff --git a/cmds/liter-server/main.go b/cmds/liter-server/main.go index fc622dd..ec1560d 100644 --- a/cmds/liter-server/main.go +++ b/cmds/liter-server/main.go @@ -10,6 +10,7 @@ import ( "os/signal" "path/filepath" "strconv" + "strings" "sync" "syscall" "time" @@ -219,7 +220,7 @@ WAIT: type Server struct{ Listener net.Listener - conns []net.Conn + conns []*liter.Conn cond *sync.Cond } @@ -237,15 +238,39 @@ func (s *Server)Serve()(err error){ return } go func(c net.Conn){ + successed := false + defer func(){ + if !successed { + c.Close() + } + }() + cfg := getConfig() + if host, _, err := net.SplitHostPort(c.RemoteAddr().String()); err != nil { + return + }else{ + ip := net.ParseIP(host) + if ip == nil { + return + } + if blacklist.IncludeIP(ip) { + return + } + if cfg.EnableIPWhitelist { + if !whitelist.IncludeIP(ip) { + return + } + } + } + wc := liter.WrapConn(c) s.cond.L.Lock() - s.conns = append(s.conns, c) + s.conns = append(s.conns, wc) s.cond.L.Unlock() defer func(){ s.cond.L.Lock() defer s.cond.L.Unlock() e := len(s.conns) - 1 for i, d := range s.conns { - if d == c { + if d == wc { if i != e { s.conns[i], s.conns[e] = s.conns[e], s.conns[i] } @@ -257,7 +282,8 @@ func (s *Server)Serve()(err error){ } } }() - handler(liter.WrapConn(c)) + successed = true + handler(wc) }(c) } } @@ -266,6 +292,11 @@ func (s *Server)Shutdown(ctx context.Context)(err error){ if err = s.Listener.Close(); err != nil { return } + // s.cond.L.Lock() + // for _, c := range s.conns { + // c.SendPkt(0x00, ) + // } + // s.cond.L.Unlock() select { case <-waitUntilNot(s.cond, func()(bool){ return len(s.conns) > 0 @@ -287,12 +318,15 @@ func (s *Server)Shutdown(ctx context.Context)(err error){ func handler(c *liter.Conn){ defer c.Close() + + cfg := getConfig() + ploger := logger.NewPrefixLogger(loger, "client [%v]:", c.RemoteAddr()) ploger.Debugf("Connected!") var err error var hp *liter.HandshakePkt if hp, err = c.RecvHandshakePkt(); err != nil { - ploger.Errorf("read handshake packet error: %v", err) + ploger.Debugf("Read handshake packet error: %v", err) return } ploger.Tracef("Handshake packet: %v", hp) @@ -308,15 +342,57 @@ func handler(c *liter.Conn){ } } if svr == nil { - ploger.Infof("Trying connected unexcept address %q", hp.Addr) + ploger.Infof("Trying connected with unexpected address %q", hp.Addr) return } ploger.Infof("Connected with address [%s:%d], passing to server %q[%s]", hp.Addr, hp.Port, svr.Id, svr.Target) - if hp.NextState == liter.NextPingState && svr.HandlePing { - ploger.Debugf("Handle ping connection for server %q", svr.Id) - handleServerStatus(ploger, c, "Idle", svr.Motd) + var lp liter.LoginStartPacket + + if hp.NextState == liter.NextPingState { + if svr.HandlePing { + ploger.Debugf("Handle ping connection for server %q", svr.Id) + handleServerStatus(ploger, c, "Idle", svr.Motd) + return + } + }else if hp.NextState == liter.NextLoginState { + if err = c.RecvPkt(0x00, &lp); err != nil { + ploger.Debugf("Read login start packet error: %v", err) + return + } + + player, err := AuthClient.GetPlayerInfo(lp.Name) + if err != nil { + c.SendPkt(0x00, &liter.DisconnectPkt{ + Reason: liter.NewChatFromString("Your username is not exists or auth server error"), + }) + ploger.Debugf("Cannot get player info for %s: %v", lp.Name, err) + return + } + if lp.Id.Ok { + id := lp.Id.V + if id != player.Id { + c.SendPkt(0x00, &liter.DisconnectPkt{ + Reason: liter.NewChatFromString("Your user profile is not match"), + }) + return + } + } + if blacklist.HasPlayer(player) { + c.SendPkt(0x00, &liter.DisconnectPkt{ + Reason: liter.NewChatFromString("Your are in the blacklist"), + }) + return + } + if cfg.EnableWhitelist && !whitelist.HasPlayer(player) { + c.SendPkt(0x00, &liter.DisconnectPkt{ + Reason: liter.NewChatFromString("Your are not in the whitelist"), + }) + return + } + }else{ + // unknown type connection return } @@ -326,78 +402,89 @@ func handler(c *liter.Conn){ if hp.NextState == liter.NextPingState { ploger.Debugf("Handle ping connection for server %q", svr.Id) handleServerStatus(ploger, c, "Closed", svr.MotdFailed) + }else if hp.NextState == liter.NextLoginState { + c.SendPkt(0x00, &liter.DisconnectPkt{ + Reason: liter.NewChatFromString("Cannot connect to the subserver"), + }) } return } ploger.Debugf("Target %q connected", svr.Id) if err = conn.SendHandshakePkt(hp); err != nil { - ploger.Errorf("New connection handshake error: %v", err) + ploger.Errorf("Connection handshake error: %v", err) return } ploger.Tracef("Handshake sent successed") + if hp.NextState == liter.NextLoginState { + if err = conn.SendPkt(0x00, lp); err != nil { + ploger.Errorf("Connection login error: %v", err) + return + } + ploger.Tracef("Login start packet sent successed") + } + rc, cr := c.RawConn(), conn.RawConn() + buf := make([]byte, 32 * 1024) + cr.SetReadDeadline(time.Now().Add(time.Millisecond * 10)) // try read to ensure the connection is ok if n, err := cr.Read(buf); err != nil { - ploger.Errorf("First read failed for %q: %v", svr.Target, err) - if hp.NextState == liter.NextPingState { - ploger.Debugf("Handle ping connection for server %q", svr.Id) - handleServerStatus(ploger, c, "Closed", svr.MotdFailed) + if strings.Contains(err.Error(), "connection reset by peer") { + ploger.Errorf("Connection reset by peer") + if hp.NextState == liter.NextPingState { + ploger.Debugf("Handle ping connection for server %q", svr.Id) + handleServerStatus(ploger, c, "Closed", svr.MotdFailed) + } + return } - return - }else if _, err = rc.Write(buf[:n]); err != nil { - ploger.Errorf("First write failed: %v", err) - return + }else if n != 0 { + rc.Write(buf[:n]) } + cr.SetReadDeadline(time.Time{}) // clear read deadline + go func(){ defer c.Close() defer conn.Close() - io.Copy(rc, cr) + buf := make([]byte, 32 * 1024) + io.CopyBuffer(rc, cr, buf) }() io.CopyBuffer(cr, rc, buf) } -type playerInfo struct { - Name string `json:"name"` - Id liter.UUID `json:"id"` -} - func handleServerStatus(loger logger.Logger, c *liter.Conn, status string, motd string){ var srp liter.StatusRequestPkt var err error if err = c.RecvPkt(0x00, &srp); err != nil { - loger.Errorf("Read status request packet error: %v", err) + loger.Debugf("Read status request packet error: %v", err) return } if err = c.SendPkt(0x00, liter.StatusResponsePkt{ - Payload: liter.Object{ - "version": liter.Object{ - "name": status, - "protocol": c.Protocol(), + Payload: liter.StatusResponsePayload{ + Version: liter.ProtocolVersion{ + Name: status, + Protocol: c.Protocol(), }, - "players": liter.Object{ - "max": 2, - "online": 1, - "sample": []playerInfo{ + Players: liter.PlayerStatus{ + Max: 2, + Online: 1, + Sample: []liter.PlayerInfo{ { Name: status }, // to allow hover for the status }, }, - "description": liter.Object{ - "text": motd, - }, + Description: liter.NewChatFromString(motd), }, }); err != nil { - loger.Errorf("Send packet error: %v", err) + loger.Debugf("Send packet error: %v", err) return } var prp liter.PingRequestPkt if err = c.RecvPkt(0x01, &prp); err != nil { - loger.Errorf("Read ping request packet error: %v", err) + loger.Debugf("Read ping request packet error: %v", err) return } if err = c.SendPkt(0x01, (liter.PingResponsePkt)(prp)); err != nil { - loger.Errorf("Send ping response packet error: %v", err) + loger.Debugf("Send ping response packet error: %v", err) return } } diff --git a/conn.go b/conn.go index 89cf063..3a565c5 100644 --- a/conn.go +++ b/conn.go @@ -124,11 +124,9 @@ func (c *Conn)Send(p *PacketBuilder)(err error){ return } -func (c *Conn)SendPkt(id int32, values ...any)(err error){ +func (c *Conn)SendPkt(id int32, value Encodable)(err error){ p := NewPacket(c.protocol, (VarInt)(id)) - for _, v := range values { - p.Encode(v) - } + value.Encode(p) return c.Send(p) } diff --git a/packets.go b/packets.go index c557f77..0d9c7b9 100644 --- a/packets.go +++ b/packets.go @@ -301,6 +301,17 @@ func (p *PacketBuilder)JSON(v any)(*PacketBuilder){ return p } +func (p *PacketBuilder)Chat(v *Chat)(*PacketBuilder){ + buf, err := v.MarshalJSON() + if err != nil { + panic(err) + } + p. + VarInt((VarInt)(len(buf))). + ByteArray(buf) + return p +} + func (p *PacketBuilder)Encode(v any){ switch w := v.(type) { case Bool: @@ -333,6 +344,8 @@ func (p *PacketBuilder)Encode(v any){ p.JSON(w) case UUID: p.UUID(w) + case *Chat: + p.Chat(w) case Encodable: w.Encode(p) default: @@ -584,7 +597,26 @@ func (p *PacketReader)JSON(ptr any)(err error){ return io.EOF } if err = json.Unmarshal(buf, ptr); err != nil { - return err + return + } + return +} + +func (p *PacketReader)Chat()(c *Chat, err error){ + var ( + size VarInt + ok bool + ) + if size, ok = p.VarInt(); !ok { + return nil, io.EOF + } + buf := make([]byte, size) + if ok = p.ByteArray(buf); !ok { + return nil, io.EOF + } + c = new(Chat) + if err = c.UnmarshalJSON(buf); err != nil { + return nil, err } return } diff --git a/preset_packets.go b/preset_packets.go index 17cd29b..11ec01e 100644 --- a/preset_packets.go +++ b/preset_packets.go @@ -70,8 +70,30 @@ func (p StatusRequestPkt)String()(string){ return "" } func (p StatusRequestPkt)Encode(b *PacketBuilder){} func (p *StatusRequestPkt)DecodeFrom(r *PacketReader)(err error){ return } +type PlayerInfo struct { + Name string `json:"name"` + Id UUID `json:"id"` +} + +type ( + StatusResponsePayload struct { + Version ProtocolVersion `json:"version"` + Players PlayerStatus `json:"players"` + Description *Chat `json:"description"` + } + ProtocolVersion struct { + Name string `json:"name"` + Protocol int `json:"protocol"` + } + PlayerStatus struct { + Max int `json:"max"` + Online int `json:"online"` + Sample []PlayerInfo `json:"sample,omitempty"` + } +) + type StatusResponsePkt struct { - Payload Object + Payload StatusResponsePayload } var _ Packet = (*StatusResponsePkt)(nil) @@ -115,7 +137,9 @@ func (p *PingRequestPkt)DecodeFrom(r *PacketReader)(err error){ return } -type PingResponsePkt PingRequestPkt +type PingResponsePkt struct { + Payload int64 +} var _ Packet = (*PingResponsePkt)(nil) @@ -135,3 +159,113 @@ func (p *PingResponsePkt)DecodeFrom(r *PacketReader)(err error){ } return } + +type DisconnectPkt struct { + Reason *Chat +} + +var _ Packet = (*DisconnectPkt)(nil) + +func (p DisconnectPkt)Encode(b *PacketBuilder){ + b.Chat(p.Reason) +} + +func (p *DisconnectPkt)DecodeFrom(r *PacketReader)(err error){ + if err = r.JSON(&p.Reason); err != nil { + return + } + return +} + + +type LoginStartPacket struct { + Name string + + // if protocol >= 1.19 && protocol <= 1.19.2 + HasSign bool + Timestamp Long + PublicKey ByteArray + Sign ByteArray + + // if protocol >= 1.19.1 + Id Optional[UUID] +} + +var _ Packet = (*LoginStartPacket)(nil) + +func (p LoginStartPacket)String()(string){ + if p.Id.Ok { + return fmt.Sprintf("", p.Name, p.Id.V) + } + return fmt.Sprintf("", p.Name) +} + +func (p LoginStartPacket)Encode(b *PacketBuilder){ + protocol := b.Protocol() + b.String(p.Name) + if protocol >= V1_19 { + if protocol <= V1_19_2 { + b.Bool(p.HasSign) + if p.HasSign { + b. + Long(p.Timestamp). + VarInt((VarInt)(len(p.PublicKey))). + ByteArray(p.PublicKey). + VarInt((VarInt)(len(p.Sign))). + ByteArray(p.Sign) + } + } + if protocol >= V1_19_2 { + b.Bool(p.Id.Ok) + if p.Id.Ok { + b.UUID(p.Id.V) + } + } + } +} + +func (p *LoginStartPacket)DecodeFrom(r *PacketReader)(err error){ + protocol := r.Protocol() + var ok bool + if p.Name, ok = r.String(); !ok { + return io.EOF + } + if protocol >= V1_19 { + if protocol <= V1_19_2 { + if p.HasSign, ok = r.Bool(); !ok { + return io.EOF + } + if p.HasSign { + if p.Timestamp, ok = r.Long(); !ok { + return io.EOF + } + var n VarInt + if n, ok = r.VarInt(); !ok { + return io.EOF + } + p.PublicKey = make(ByteArray, n) + if ok = r.ByteArray(p.PublicKey); !ok { + return io.EOF + } + if n, ok = r.VarInt(); !ok { + return io.EOF + } + p.Sign = make(ByteArray, n) + if ok = r.ByteArray(p.Sign); !ok { + return io.EOF + } + } + } + if protocol >= V1_19_2 { + if p.Id.Ok, ok = r.Bool(); !ok { + return io.EOF + } + if p.Id.Ok { + if p.Id.V, ok = r.UUID(); !ok { + return io.EOF + } + } + } + } + return +} diff --git a/webapi.go b/webapi.go index 2af5911..60de998 100644 --- a/webapi.go +++ b/webapi.go @@ -10,8 +10,6 @@ import ( "net/http" "path" "strings" - - "github.com/google/uuid" ) const ( @@ -50,7 +48,7 @@ func (e *HttpStatusError)Error()(string){ return fmt.Sprintf("%d %s", e.Code, http.StatusText(e.Code)) } -func (cli *AuthClient)GetPlayerUUID(name string)(_ string, _ uuid.UUID, err error){ +func (cli *AuthClient)GetPlayerInfo(name string)(player PlayerInfo, err error){ const EndPoint = "/users/profiles/minecraft" url := joinUrl(cli.ApiServer, EndPoint, name) var res *http.Response @@ -69,14 +67,48 @@ func (cli *AuthClient)GetPlayerUUID(name string)(_ string, _ uuid.UUID, err erro if body, err = readBody(res); err != nil { return } - var resp struct{ - Name string `json:"name"` - Id uuid.UUID `json:"id"` + if err = json.Unmarshal(body, &player); err != nil{ + return + } + return +} + +type Property struct { + Name string `json:"name"` + Value any `json:"value"` +} + +type PlayerProfile struct { + Id UUID `json:"id"` + Name string `json:"name"` + Properties []Property `json:"properties` + ProfileActions []string `json:"profileActions"` +} + +func (cli *AuthClient)GetPlayerProfile(id UUID)(profile *PlayerProfile, err error){ + const EndPoint = "/profile" + url := joinUrl(cli.SessionServer, EndPoint, id.String()) + var res *http.Response + if res, err = cli.Client.Get(url); err != nil { + return + } + if res.StatusCode == http.StatusNoContent { + err = PlayerNotExistsErr + return + } + if res.StatusCode != http.StatusOK { + err = &HttpStatusError{res.StatusCode} + return } - if err = json.Unmarshal(body, &resp); err != nil{ + var body []byte + if body, err = readBody(res); err != nil { + return + } + profile = new(PlayerProfile) + if err = json.Unmarshal(body, profile); err != nil{ return } - return resp.Name, resp.Id, nil + return } func (cli *AuthClient)auth_request(point string, req any, res any)(err error){ @@ -153,11 +185,6 @@ type UserData struct{ Properties Props `json:"properties"` } -type Profile struct{ - Name string `json:"name"` - Id uuid.UUID `json:"id"` -} - type AuthData struct{ LoginData ClientToken string `json:"clientToken"` @@ -168,8 +195,8 @@ type AuthResponse struct{ ClientToken string `json:"clientToken"` AccessToken string `json:"accessToken"` - AvailableProfiles []Profile `json:"availableProfiles"` - SelectedProfile Profile `json:"selectedProfile"` + AvailableProfiles []PlayerInfo `json:"availableProfiles"` + SelectedProfile PlayerInfo `json:"selectedProfile"` } func (cli *AuthClient)Auth(data AuthData)(res *AuthResponse, err error){ @@ -202,7 +229,7 @@ type RefreshResponse struct{ ClientToken string `json:"clientToken"` AccessToken string `json:"accessToken"` - SelectedProfile Profile `json:"selectedProfile"` + SelectedProfile PlayerInfo `json:"selectedProfile"` } func (cli *AuthClient)Refresh(data AccessData)(res *RefreshResponse, err error){ diff --git a/webapi_test.go b/webapi_test.go index 684a125..994a056 100644 --- a/webapi_test.go +++ b/webapi_test.go @@ -4,17 +4,30 @@ package liter_test import ( "testing" + "github.com/google/uuid" . "github.com/kmcsr/go-liter" ) func TestAuthClient_GetPlayerUUID(t *testing.T){ names := []string{"north", "jeb_", "ckupen"} for _, n := range names { - realname, uid, err := DefaultAuthClient.GetPlayerUUID(n) + info, err := DefaultAuthClient.GetPlayerInfo(n) if err != nil { t.Logf("Cannot get uuid of player '%s': %v", n, err) continue } - t.Logf("Uuid of player %s is %s[%v]", n, realname, uid) + t.Logf("Uuid of player %s is %s[%v]", n, info.Name, info.Id) + } +} + +func TestAuthClient_GetPlayerProfile(t *testing.T){ + uuids := []string{"79c839d8-f837-40a6-a519-e46aa1b029ee", "853c80ef-3c37-49fd-aa49-938b674adae6", "7a0ba4fe-e6ec-4bfe-99fc-56bf677a15a5"} + for _, n := range uuids { + profile, err := DefaultAuthClient.GetPlayerProfile(uuid.MustParse(n)) + if err != nil { + t.Logf("Cannot get profile of player '%s': %v", n, err) + continue + } + t.Logf("Profile of player %s is %s[%v]", n, profile.Name, profile.Id) } }