diff --git a/cmd/bedrocktool/main.go b/cmd/bedrocktool/main.go index a87dba5..1a363bd 100644 --- a/cmd/bedrocktool/main.go +++ b/cmd/bedrocktool/main.go @@ -7,7 +7,7 @@ import ( "fmt" "os" "os/signal" - "regexp" + "runtime/debug" "syscall" "github.com/bedrock-tool/bedrocktool/utils" @@ -29,6 +29,7 @@ func main() { logrus.Infof("Version: %s", utils.Version) logrus.Infof("Cmdline: %s", os.Args) logrus.Errorf("Error: %s", err) + fmt.Println("stacktrace from panic: \n" + string(debug.Stack())) println("--END COPY HERE--") println("") println("if you want to report this error, please open an issue at") @@ -81,12 +82,11 @@ func main() { } fmt.Printf("Use '%s ' to run a command\n", os.Args[0]) - fmt.Printf("Input Command: ") - reader := bufio.NewReader(os.Stdin) - target, _ := reader.ReadString('\n') - r, _ := regexp.Compile(`[\n\r]`) - target = string(r.ReplaceAll([]byte(target), []byte(""))) - os.Args = append(os.Args, target) + cmd, cancelled := utils.User_input(ctx, "Input Command: ") + if cancelled { + return + } + os.Args = append(os.Args, cmd) utils.G_interactive = true } } @@ -103,6 +103,7 @@ func main() { signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) go func() { <-sigs + println("cancelling") cancel() }() diff --git a/subcommands/capture.go b/subcommands/capture.go index fa09836..8e97317 100644 --- a/subcommands/capture.go +++ b/subcommands/capture.go @@ -54,7 +54,7 @@ func (c *CaptureCMD) Usage() string { } func (c *CaptureCMD) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { - address, hostname, err := utils.ServerInput(c.server_address) + address, hostname, err := utils.ServerInput(ctx, c.server_address) if err != nil { logrus.Fatal(err) return 1 diff --git a/subcommands/chat_log.go b/subcommands/chat_log.go index 4358c78..ac3a9c7 100644 --- a/subcommands/chat_log.go +++ b/subcommands/chat_log.go @@ -32,7 +32,7 @@ func (c *ChatLogCMD) Usage() string { } func (c *ChatLogCMD) Execute(ctx context.Context, flags *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { - address, hostname, err := utils.ServerInput(c.Address) + address, hostname, err := utils.ServerInput(ctx, c.Address) if err != nil { logrus.Error(err) return 1 diff --git a/subcommands/debug.go b/subcommands/debug.go index 9fb4a17..781cdb1 100644 --- a/subcommands/debug.go +++ b/subcommands/debug.go @@ -29,7 +29,7 @@ func (c *DebugProxyCMD) Usage() string { } func (c *DebugProxyCMD) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { - address, _, err := utils.ServerInput(c.Address) + address, _, err := utils.ServerInput(ctx, c.Address) if err != nil { logrus.Error(err) return 1 diff --git a/subcommands/skins/skins-proxy.go b/subcommands/skins/skins-proxy.go index c5d0d3c..60dbe5d 100644 --- a/subcommands/skins/skins-proxy.go +++ b/subcommands/skins/skins-proxy.go @@ -14,8 +14,9 @@ import ( ) type SkinProxyCMD struct { - server_address string - filter string + server_address string + filter string + only_with_geometry bool } func (*SkinProxyCMD) Name() string { return "skins-proxy" } @@ -24,6 +25,7 @@ func (*SkinProxyCMD) Synopsis() string { return "download skins from players on func (c *SkinProxyCMD) SetFlags(f *flag.FlagSet) { f.StringVar(&c.server_address, "address", "", "remote server address") f.StringVar(&c.filter, "filter", "", "player name filter prefix") + f.BoolVar(&c.only_with_geometry, "only-geom", false, "only save skins with geometry") } func (c *SkinProxyCMD) Usage() string { @@ -31,7 +33,7 @@ func (c *SkinProxyCMD) Usage() string { } func (c *SkinProxyCMD) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { - address, hostname, err := utils.ServerInput(c.server_address) + address, hostname, err := utils.ServerInput(ctx, c.server_address) if err != nil { logrus.Error(err) return 1 @@ -42,7 +44,7 @@ func (c *SkinProxyCMD) Execute(ctx context.Context, f *flag.FlagSet, _ ...interf proxy := utils.NewProxy(logrus.StandardLogger()) proxy.PacketCB = func(pk packet.Packet, proxy *utils.ProxyContext, toServer bool) (packet.Packet, error) { if !toServer { - process_packet_skins(proxy.Client, out_path, pk, c.filter) + process_packet_skins(proxy.Client, out_path, pk, c.filter, c.only_with_geometry) } return pk, nil } diff --git a/subcommands/skins/skins.go b/subcommands/skins/skins.go index 119e446..68fc5f4 100644 --- a/subcommands/skins/skins.go +++ b/subcommands/skins/skins.go @@ -27,6 +27,18 @@ type Skin struct { protocol.Skin } +type SkinMeta struct { + SkinID string + PlayFabID string + PremiumSkin bool + PersonaSkin bool + CapeID string + SkinColour string + ArmSize string + Trusted bool + PersonaPieces []protocol.PersonaPiece +} + // WriteGeometry writes the geometry json for the skin to output_path func (skin *Skin) WriteGeometry(output_path string) error { f, err := os.Create(output_path) @@ -96,17 +108,7 @@ func (skin *Skin) WriteMeta(output_path string) error { return fmt.Errorf("failed to write Tint %s: %s", output_path, err) } defer f.Close() - d, err := json.MarshalIndent(struct { - SkinID string - PlayFabID string - PremiumSkin bool - PersonaSkin bool - CapeID string - SkinColour string - ArmSize string - Trusted bool - PersonaPieces []protocol.PersonaPiece - }{ + d, err := json.MarshalIndent(SkinMeta{ skin.SkinID, skin.PlayFabID, skin.PremiumSkin, @@ -124,13 +126,17 @@ func (skin *Skin) WriteMeta(output_path string) error { return nil } +func (skin *Skin) Complex() bool { + have_geometry, have_cape, have_animations, have_tint := len(skin.SkinGeometry) > 0, len(skin.CapeData) > 0, len(skin.Animations) > 0, len(skin.PieceTintColours) > 0 + return have_geometry || have_cape || have_animations || have_tint +} + // Write writes all data for this skin to a folder func (skin *Skin) Write(output_path, name string) error { name, _ = filenamify.FilenamifyV2(name) skin_dir := path.Join(output_path, name) have_geometry, have_cape, have_animations, have_tint := len(skin.SkinGeometry) > 0, len(skin.CapeData) > 0, len(skin.Animations) > 0, len(skin.PieceTintColours) > 0 - os.MkdirAll(skin_dir, 0o755) if have_geometry { if err := skin.WriteGeometry(path.Join(skin_dir, "geometry.json")); err != nil { @@ -157,17 +163,14 @@ func (skin *Skin) Write(output_path, name string) error { return err } - return skin.WriteTexture(path.Join(skin_dir, "skin.png")) + return skin.WriteTexture(skin_dir + "/skin.png") } // puts the skin at output_path if the filter matches it // internally converts the struct so it can use the extra methods -func write_skin(output_path, name string, skin protocol.Skin, filter string) { - if !strings.HasPrefix(name, filter) { - return - } +func write_skin(output_path, name string, skin *protocol.Skin) { logrus.Infof("Writing skin for %s\n", name) - _skin := &Skin{skin} + _skin := &Skin{*skin} if err := _skin.Write(output_path, name); err != nil { fmt.Fprintf(os.Stderr, "Error writing skin: %s\n", err) } @@ -176,37 +179,73 @@ func write_skin(output_path, name string, skin protocol.Skin, filter string) { var ( skin_players = make(map[string]string) skin_player_counts = make(map[string]int) - processed_skins = make(map[string]bool) ) -func process_packet_skins(conn *minecraft.Conn, out_path string, pk packet.Packet, filter string) { +func popup_skin_saved(conn *minecraft.Conn, name string) { + if conn != nil { + (&utils.ProxyContext{Client: conn}).SendPopup(fmt.Sprintf("%s Skin was Saved", name)) + } +} + +func skin_meta_get_skinid(path string) string { + cont, err := os.ReadFile(fmt.Sprintf("%s/metadata.json", path)) + if err != nil { + return "" + } + var meta SkinMeta + if err := json.Unmarshal(cont, &meta); err != nil { + return "" + } + return meta.SkinID +} + +func save_player_skin(conn *minecraft.Conn, out_path, player_name string, skin *protocol.Skin) { + count := skin_player_counts[player_name] + if count > 0 { + meta_id := skin_meta_get_skinid(fmt.Sprintf("%s/%s_%d", out_path, player_name, count-1)) + if meta_id == skin.SkinID { + return // skin same as before + } + } + + skin_player_counts[player_name]++ + count++ + write_skin(out_path, fmt.Sprintf("%s_%d", player_name, count), skin) + popup_skin_saved(conn, player_name) +} + +func process_packet_skins(conn *minecraft.Conn, out_path string, pk packet.Packet, filter string, only_if_geom bool) { switch _pk := pk.(type) { case *packet.PlayerSkin: - name := skin_players[_pk.UUID.String()] - if name == "" { - name = _pk.UUID.String() + player_name := skin_players[_pk.UUID.String()] + if player_name == "" { + player_name = _pk.UUID.String() } - skin_player_counts[name]++ - name = fmt.Sprintf("%s_%d", name, skin_player_counts[name]) - write_skin(out_path, name, _pk.Skin, filter) + if !strings.HasPrefix(player_name, filter) { + return + } + if only_if_geom && len(_pk.Skin.SkinGeometry) == 0 { + return + } + + save_player_skin(conn, out_path, player_name, &_pk.Skin) case *packet.PlayerList: if _pk.ActionType == 1 { // remove return } for _, player := range _pk.Entries { - name := utils.CleanupName(player.Username) - if name == "" { - name = player.UUID.String() + player_name := utils.CleanupName(player.Username) + if player_name == "" { + player_name = player.UUID.String() } - if _, ok := processed_skins[name]; ok { - continue + if !strings.HasPrefix(player_name, filter) { + return } - write_skin(out_path, name, player.Skin, filter) - skin_players[player.UUID.String()] = name - processed_skins[name] = true - if conn != nil { - (&utils.ProxyContext{Client: conn}).SendPopup(fmt.Sprintf("%s Skin was Saved", name)) + if only_if_geom && len(player.Skin.SkinGeometry) == 0 { + return } + skin_players[player.UUID.String()] = player_name + save_player_skin(conn, out_path, player_name, &player.Skin) } } } @@ -229,7 +268,7 @@ func (c *SkinCMD) Usage() string { } func (c *SkinCMD) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { - address, hostname, err := utils.ServerInput(c.server_address) + address, hostname, err := utils.ServerInput(ctx, c.server_address) if err != nil { fmt.Fprint(os.Stderr, err) return 1 @@ -259,7 +298,7 @@ func (c *SkinCMD) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{} if err != nil { return 1 } - process_packet_skins(nil, out_path, pk, c.filter) + process_packet_skins(nil, out_path, pk, c.filter, false) } } diff --git a/subcommands/world/world.go b/subcommands/world/world.go index 1274aca..dd7c557 100644 --- a/subcommands/world/world.go +++ b/subcommands/world/world.go @@ -142,7 +142,7 @@ func (c *WorldCMD) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{ }() */ - server_address, hostname, err := utils.ServerInput(c.Address) + server_address, hostname, err := utils.ServerInput(ctx, c.Address) if err != nil { fmt.Fprintln(os.Stderr, err) return 1 diff --git a/utils/input.go b/utils/input.go index 28902f8..1f77d59 100644 --- a/utils/input.go +++ b/utils/input.go @@ -12,6 +12,25 @@ import ( "github.com/sirupsen/logrus" ) +func User_input(ctx context.Context, q string) (string, bool) { + c := make(chan string) + go func() { + fmt.Print(q) + reader := bufio.NewReader(os.Stdin) + answer, _ := reader.ReadString('\n') + r, _ := regexp.Compile(`[\n\r]`) + answer = string(r.ReplaceAll([]byte(answer), []byte(""))) + c <- answer + }() + + select { + case <-ctx.Done(): + return "", true + case a := <-c: + return a, false + } +} + func server_url_to_name(server string) string { host, _, err := net.SplitHostPort(server) if err != nil { @@ -20,13 +39,13 @@ func server_url_to_name(server string) string { return host } -func ServerInput(server string) (address, name string, err error) { +func ServerInput(ctx context.Context, server string) (address, name string, err error) { if server == "" { // no arg provided, interactive input - fmt.Printf("Enter Server: ") - reader := bufio.NewReader(os.Stdin) - server, _ = reader.ReadString('\n') - r, _ := regexp.Compile(`[\n\r]`) - server = string(r.ReplaceAll([]byte(server), []byte(""))) + var cancelled bool + server, cancelled = User_input(ctx, "Enter Server: ") + if cancelled { + return "", "", context.Canceled + } } if strings.HasPrefix(server, "realm:") { // for realms use api to get ip address diff --git a/utils/proxy.go b/utils/proxy.go index 3a0358d..9496a66 100644 --- a/utils/proxy.go +++ b/utils/proxy.go @@ -156,94 +156,109 @@ func NewProxy(log *logrus.Logger) *ProxyContext { var Client_addr net.Addr func (p *ProxyContext) Run(ctx context.Context, server_address string) (err error) { + c := make(chan struct{}) + if strings.HasSuffix(server_address, ".pcap") { - return fmt.Errorf("not supported anymore!") + return fmt.Errorf("not supported anymore") } if strings.HasSuffix(server_address, ".pcap2") { return create_replay_connection(ctx, p.log, server_address, p.ConnectCB, p.PacketCB) } - GetTokenSource() // ask for login before listening - - var packs []*resource.Pack - if G_preload_packs { - p.log.Info("Preloading resourcepacks") - serverConn, err := ConnectServer(ctx, server_address, nil, true, nil) - if err != nil { - return fmt.Errorf("failed to connect to %s: %s", server_address, err) + go func() { + defer func() { c <- struct{}{} }() + GetTokenSource() // ask for login before listening + var packs []*resource.Pack + if G_preload_packs { + p.log.Info("Preloading resourcepacks") + var serverConn *minecraft.Conn + serverConn, err = ConnectServer(ctx, server_address, nil, true, nil) + if err != nil { + err = fmt.Errorf("failed to connect to %s: %s", server_address, err) + return + } + serverConn.Close() + packs = serverConn.ResourcePacks() + p.log.Infof("%d packs loaded", len(packs)) } - serverConn.Close() - packs = serverConn.ResourcePacks() - p.log.Infof("%d packs loaded", len(packs)) - } - _status := minecraft.NewStatusProvider("Server") - p.Listener, err = minecraft.ListenConfig{ - StatusProvider: _status, - ResourcePacks: packs, - AcceptedProtocols: []minecraft.Protocol{ - dummyProto{id: 544, ver: "1.19.20"}, - }, - }.Listen("raknet", ":19132") - if err != nil { + _status := minecraft.NewStatusProvider("Server") + p.Listener, err = minecraft.ListenConfig{ + StatusProvider: _status, + ResourcePacks: packs, + AcceptedProtocols: []minecraft.Protocol{ + dummyProto{id: 544, ver: "1.19.20"}, + }, + }.Listen("raknet", ":19132") + if err != nil { + return + } + defer p.Listener.Close() + + p.log.Infof("Listening on %s", p.Listener.Addr()) + + var c net.Conn + c, err = p.Listener.Accept() + if err != nil { + p.log.Fatal(err) + } + p.Client = c.(*minecraft.Conn) + + cd := p.Client.ClientData() + p.Server, err = ConnectServer(ctx, server_address, &cd, false, p.PacketFunc) + if err != nil { + err = fmt.Errorf("failed to connect to %s: %s", server_address, err) + return + } + // spawn and start the game + if err = spawn_conn(ctx, p.Client, p.Server); err != nil { + err = fmt.Errorf("failed to spawn: %s", err) + return + } + + defer p.Server.Close() + defer p.Listener.Disconnect(p.Client, G_disconnect_reason) + + if p.ConnectCB != nil { + p.ConnectCB(p) + } + + wg := sync.WaitGroup{} + cbs := []PacketCallback{ + p.CommandHandlerPacketCB, + } + if p.PacketCB != nil { + cbs = append(cbs, p.PacketCB) + } + + // server to client + wg.Add(1) + go func() { + defer wg.Done() + if err := proxyLoop(ctx, p, false, cbs); err != nil { + p.log.Error(err) + return + } + }() + + // client to server + wg.Add(1) + go func() { + defer wg.Done() + if err := proxyLoop(ctx, p, true, cbs); err != nil { + p.log.Error(err) + return + } + }() + + wg.Wait() + }() + + select { + case <-ctx.Done(): + println("Cancelled") + return context.Canceled + case <-c: return err } - defer p.Listener.Close() - - p.log.Infof("Listening on %s", p.Listener.Addr()) - - c, err := p.Listener.Accept() - if err != nil { - p.log.Fatal(err) - } - p.Client = c.(*minecraft.Conn) - - cd := p.Client.ClientData() - p.Server, err = ConnectServer(ctx, server_address, &cd, false, p.PacketFunc) - if err != nil { - return fmt.Errorf("failed to connect to %s: %s", server_address, err) - } - - // spawn and start the game - if err := spawn_conn(ctx, p.Client, p.Server); err != nil { - return fmt.Errorf("failed to spawn: %s", err) - } - - defer p.Server.Close() - defer p.Listener.Disconnect(p.Client, G_disconnect_reason) - - if p.ConnectCB != nil { - p.ConnectCB(p) - } - - wg := sync.WaitGroup{} - cbs := []PacketCallback{ - p.CommandHandlerPacketCB, - } - if p.PacketCB != nil { - cbs = append(cbs, p.PacketCB) - } - - // server to client - wg.Add(1) - go func() { - defer wg.Done() - if err := proxyLoop(ctx, p, false, cbs); err != nil { - p.log.Error(err) - return - } - }() - - // client to server - wg.Add(1) - go func() { - defer wg.Done() - if err := proxyLoop(ctx, p, true, cbs); err != nil { - p.log.Error(err) - return - } - }() - - wg.Wait() - return nil } diff --git a/utils/replay.go b/utils/replay.go index 39cf46d..fd33b07 100644 --- a/utils/replay.go +++ b/utils/replay.go @@ -5,6 +5,7 @@ import ( "encoding/binary" "io" "os" + "reflect" "github.com/sandertv/gophertunnel/minecraft" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" @@ -31,7 +32,9 @@ func create_replay_connection(ctx context.Context, log *logrus.Logger, filename proxy.Server = minecraft.NewConn() game_started := false + i := 0 for { + i += 1 var magic uint32 = 0 var packet_length uint32 = 0 var toServer bool = false @@ -55,7 +58,7 @@ func create_replay_connection(ctx context.Context, log *logrus.Logger, filename return nil } if n != int(packet_length) { - log.Errorf("Truncated") + log.Errorf("Truncated %d", i) return nil } @@ -76,6 +79,8 @@ func create_replay_connection(ctx context.Context, log *logrus.Logger, filename } for _, pk := range pks { + logrus.Printf("%s", reflect.TypeOf(pk).String()[1:]) + if game_started { if packetCB != nil { packetCB(pk, proxy, toServer)