diff --git a/go.mod b/go.mod index 6627aaf..d0a19ee 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/bedrock-tool/bedrocktool go 1.20 //replace github.com/sandertv/gophertunnel => ./gophertunnel -replace github.com/sandertv/gophertunnel => github.com/olebeck/gophertunnel v1.28.1-1 +replace github.com/sandertv/gophertunnel => github.com/olebeck/gophertunnel v1.28.1-2 //replace github.com/df-mc/dragonfly => ./dragonfly replace github.com/df-mc/dragonfly => github.com/olebeck/dragonfly v0.9.4-9 @@ -51,6 +51,7 @@ require ( github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 // indirect github.com/df-mc/atomic v1.10.0 // indirect github.com/df-mc/worldupgrader v1.0.3 // indirect + github.com/dlclark/regexp2 v1.9.0 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-text/typesetting v0.0.0-20221214153724-0399769901d5 // indirect github.com/golang/protobuf v1.5.2 // indirect diff --git a/go.sum b/go.sum index d35b61b..bee11e2 100644 --- a/go.sum +++ b/go.sum @@ -37,6 +37,8 @@ github.com/df-mc/goleveldb v1.1.9 h1:ihdosZyy5jkQKrxucTQmN90jq/2lUwQnJZjIYIC/9YU github.com/df-mc/goleveldb v1.1.9/go.mod h1:+NHCup03Sci5q84APIA21z3iPZCuk6m6ABtg4nANCSk= github.com/df-mc/worldupgrader v1.0.3 h1:3nbthy6vfSNQZdqHBR+E5Fh3mCeWmCwLtqrYDiPUG5I= github.com/df-mc/worldupgrader v1.0.3/go.mod h1:6ybkJ/BV9b0XkcPzcLmvgT9Nv/xgBXdDQTmRhu7b8zQ= +github.com/dlclark/regexp2 v1.9.0 h1:pTK/l/3qYIKaRXuHnEnIf7Y5NxfRPfpb7dis6/gdlVI= +github.com/dlclark/regexp2 v1.9.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/flytam/filenamify v1.1.2 h1:dGlfWU4zrhDlsmvob4IFcfgjG5vIjfo4UwLyec6Wx94= @@ -100,6 +102,8 @@ github.com/olebeck/gio v0.0.0-20230321105529-d424f1a59af9 h1:TqDsMHwjW5ZYfh+RE8u github.com/olebeck/gio v0.0.0-20230321105529-d424f1a59af9/go.mod h1:+W1Kpf96YcfissZocFqIp6O42FDTuphkObbEybp+Ffc= github.com/olebeck/gophertunnel v1.28.1-1 h1:bw2jeMz94YHF5qQYhq1Yq/6fALkklGu7k26YbPI4DSs= github.com/olebeck/gophertunnel v1.28.1-1/go.mod h1:ekREo7U9TPHh86kbuPMaWA93NMyWsfVvP/iNT3XhAb8= +github.com/olebeck/gophertunnel v1.28.1-2 h1:EoGyQNzmsXtg+ObOshCKyNffTqAfk9yRFNdJcHGjxmY= +github.com/olebeck/gophertunnel v1.28.1-2/go.mod h1:HxQfl/8mZzvjzhekEH8RO6xLAgan9i/wIyrQzw0tIPY= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= diff --git a/handlers/second-user/second-user.go b/handlers/second-user/second-user.go index 03d1c1d..977ec16 100644 --- a/handlers/second-user/second-user.go +++ b/handlers/second-user/second-user.go @@ -66,7 +66,7 @@ func NewSecondUser() *utils.ProxyHandler { s.proxy = pc }, SecondaryClientCB: s.SecondaryClientCB, - OnClientConnect: func(conn *minecraft.Conn) { + OnClientConnect: func(conn minecraft.IConn) { id := conn.IdentityData() s.mainPlayer = player.New(id.DisplayName, skin.New(64, 64), mgl64.Vec3{0, 00}) s.server.World().AddEntity(s.mainPlayer) @@ -99,7 +99,7 @@ func NewSecondUser() *utils.ProxyHandler { } } -func (s *secondaryUser) SecondaryClientCB(conn *minecraft.Conn) { +func (s *secondaryUser) SecondaryClientCB(conn minecraft.IConn) { s.listener.Conn <- conn } diff --git a/handlers/worlds/chunk.go b/handlers/worlds/chunk.go index 6f6fa86..0fcf719 100644 --- a/handlers/worlds/chunk.go +++ b/handlers/worlds/chunk.go @@ -152,7 +152,7 @@ func (w *worldsHandler) ProcessChunkPackets(pk packet.Packet) packet.Packet { if ok { x, y, z := blockPosInChunk(pk.Position) c.SetBlock(x, y, z, uint8(pk.Layer), pk.NewBlockRuntimeID) - w.mapUI.SetChunk(cp, w.worldState.chunks[cp], true) + w.mapUI.SetChunk(cp, c, true) } } case *packet.UpdateSubChunkBlocks: @@ -168,7 +168,7 @@ func (w *worldsHandler) ProcessChunkPackets(pk packet.Packet) packet.Packet { c.SetBlock(x, y, z, 0, bce.BlockRuntimeID) } } - w.mapUI.SetChunk(cp, w.worldState.chunks[cp], true) + w.mapUI.SetChunk(cp, c, true) } } } diff --git a/handlers/worlds/entity.go b/handlers/worlds/entity.go index 0969c53..c81b036 100644 --- a/handlers/worlds/entity.go +++ b/handlers/worlds/entity.go @@ -62,7 +62,7 @@ func (e serverEntity) Type() world.EntityType { } func (w *worldsHandler) processAddActor(pk *packet.AddActor) { - e, ok := w.worldState.entities[pk.EntityRuntimeID] + e, ok := w.getEntity(pk.EntityRuntimeID) if !ok { e = &entityState{ RuntimeID: pk.EntityRuntimeID, @@ -216,6 +216,11 @@ func (s *entityState) ToServerEntity() serverEntity { return e } +func (w *worldsHandler) getEntity(id uint64) (*entityState, bool) { + e, ok := w.worldState.entities[id] + return e, ok +} + func (w *worldsHandler) ProcessEntityPackets(pk packet.Packet) packet.Packet { if !w.settings.SaveEntities { return pk @@ -226,7 +231,7 @@ func (w *worldsHandler) ProcessEntityPackets(pk packet.Packet) packet.Packet { w.processAddActor(pk) case *packet.RemoveActor: case *packet.SetActorData: - e, ok := w.worldState.entities[pk.EntityRuntimeID] + e, ok := w.getEntity(pk.EntityRuntimeID) if ok { e.Metadata = pk.EntityMetadata w.bp.AddEntity(behaviourpack.EntityIn{ @@ -236,12 +241,12 @@ func (w *worldsHandler) ProcessEntityPackets(pk packet.Packet) packet.Packet { }) } case *packet.SetActorMotion: - e, ok := w.worldState.entities[pk.EntityRuntimeID] + e, ok := w.getEntity(pk.EntityRuntimeID) if ok { e.Velocity = pk.Velocity } case *packet.MoveActorDelta: - e, ok := w.worldState.entities[pk.EntityRuntimeID] + e, ok := w.getEntity(pk.EntityRuntimeID) if ok { if pk.Flags&packet.MoveActorDeltaFlagHasX != 0 { e.Position[0] = pk.Position[0] @@ -263,14 +268,14 @@ func (w *worldsHandler) ProcessEntityPackets(pk packet.Packet) packet.Packet { //} } case *packet.MoveActorAbsolute: - e, ok := w.worldState.entities[pk.EntityRuntimeID] + e, ok := w.getEntity(pk.EntityRuntimeID) if ok { e.Position = pk.Position e.Pitch = pk.Rotation.X() e.Yaw = pk.Rotation.Y() } case *packet.MobEquipment: - e, ok := w.worldState.entities[pk.EntityRuntimeID] + e, ok := w.getEntity(pk.EntityRuntimeID) if ok { w, ok := e.Inventory[pk.WindowID] if !ok { @@ -280,7 +285,7 @@ func (w *worldsHandler) ProcessEntityPackets(pk packet.Packet) packet.Packet { w[pk.HotBarSlot] = pk.NewItem } case *packet.MobArmourEquipment: - e, ok := w.worldState.entities[pk.EntityRuntimeID] + e, ok := w.getEntity(pk.EntityRuntimeID) if ok { e.Helmet = &pk.Helmet e.Chestplate = &pk.Chestplate diff --git a/handlers/worlds/world.go b/handlers/worlds/world.go index 1d969bd..a0de390 100644 --- a/handlers/worlds/world.go +++ b/handlers/worlds/world.go @@ -139,7 +139,7 @@ func NewWorldsHandler(ctx context.Context, ui utils.UI, settings WorldSettings) w.serverState.Name = hostname return nil }, - OnClientConnect: func(conn *minecraft.Conn) { + OnClientConnect: func(conn minecraft.IConn) { w.gui.Message(messages.SetUIState(messages.UIStateConnecting)) }, GameDataModifier: func(gd *minecraft.GameData) { diff --git a/subcommands/capture.go b/subcommands/capture.go index f4f3b1b..d5e4d57 100644 --- a/subcommands/capture.go +++ b/subcommands/capture.go @@ -33,6 +33,7 @@ func (c *CaptureCMD) Execute(ctx context.Context, ui utils.UI) error { if err != nil { return err } + proxy.AlwaysGetPacks = true proxy.AddHandler(handlers.NewPacketCapturer()) return proxy.Run(ctx, address, hostname) } diff --git a/subcommands/skins/skins.go b/subcommands/skins/skins.go index adedde1..d0bc7b0 100644 --- a/subcommands/skins/skins.go +++ b/subcommands/skins/skins.go @@ -8,7 +8,6 @@ import ( "github.com/bedrock-tool/bedrocktool/locale" "github.com/bedrock-tool/bedrocktool/ui/messages" "github.com/bedrock-tool/bedrocktool/utils" - "github.com/sandertv/gophertunnel/minecraft" ) @@ -43,7 +42,7 @@ func (c *SkinCMD) Execute(ctx context.Context, ui utils.UI) error { })) proxy.AddHandler(&utils.ProxyHandler{ Name: "Skin CMD", - OnClientConnect: func(conn *minecraft.Conn) { + OnClientConnect: func(conn minecraft.IConn) { ui.Message(messages.SetUIState(messages.UIStateConnecting)) }, }) diff --git a/utils/proxy.go b/utils/proxy.go index ca4c18e..3a03fc3 100644 --- a/utils/proxy.go +++ b/utils/proxy.go @@ -67,8 +67,8 @@ type ProxyHandler struct { PacketCB func(pk packet.Packet, toServer bool, timeReceived time.Time) (packet.Packet, error) // called after client connected - OnClientConnect func(conn *minecraft.Conn) - SecondaryClientCB func(conn *minecraft.Conn) + OnClientConnect func(conn minecraft.IConn) + SecondaryClientCB func(conn minecraft.IConn) // called after game started ConnectCB func(err error) bool @@ -78,8 +78,8 @@ type ProxyHandler struct { } type ProxyContext struct { - Server *minecraft.Conn - Client *minecraft.Conn + Server minecraft.IConn + Client minecraft.IConn clientAddr net.Addr Listener *minecraft.Listener @@ -233,7 +233,7 @@ func (p *ProxyContext) CommandHandlerPacketCB(pk packet.Packet, toServer bool, _ } func (p *ProxyContext) proxyLoop(ctx context.Context, toServer bool) error { - var c1, c2 *minecraft.Conn + var c1, c2 minecraft.IConn if toServer { c1 = p.Client c2 = p.Server @@ -283,6 +283,56 @@ func (p *ProxyContext) IsClient(addr net.Addr) bool { var NewDebugLogger func(bool) *ProxyHandler +func (p *ProxyContext) connectClient(ctx context.Context, serverAddress string, cdpp **login.ClientData) (err error) { + GetTokenSource() // ask for login before listening + + var packs []*resource.Pack + if Options.Preload { + logrus.Info(locale.Loc("preloading_packs", nil)) + serverConn, err := connectServer(ctx, serverAddress, nil, true, func(header packet.Header, payload []byte, src, dst net.Addr) {}) + if err != nil { + return fmt.Errorf(locale.Loc("failed_to_connect", locale.Strmap{"Address": serverAddress, "Err": err})) + } + serverConn.Close() + packs = serverConn.ResourcePacks() + logrus.Infof(locale.Locm("pack_count_loaded", locale.Strmap{"Count": len(packs)}, len(packs))) + } + + status := minecraft.NewStatusProvider(fmt.Sprintf("%s Proxy", serverAddress)) + p.Listener, err = minecraft.ListenConfig{ + StatusProvider: status, + ResourcePacks: packs, + AcceptedProtocols: []minecraft.Protocol{ + //dummyProto{id: 567, ver: "1.19.60"}, + }, + }.Listen("raknet", ":19132") + if err != nil { + return err + } + + logrus.Infof(locale.Loc("listening_on", locale.Strmap{"Address": p.Listener.Addr()})) + logrus.Infof(locale.Loc("help_connect", nil)) + + go func() { + <-ctx.Done() + p.Listener.Close() + }() + + c, err := p.Listener.Accept() + if err != nil { + return err + } + p.Client = c.(*minecraft.Conn) + cd := p.Client.ClientData() + *cdpp = &cd + return nil +} + +func (p *ProxyContext) connectServer(ctx context.Context, serverAddress string, cdp *login.ClientData, packetFunc PacketFunc) (err error) { + p.Server, err = connectServer(ctx, serverAddress, cdp, p.AlwaysGetPacks, packetFunc) + return err +} + func (p *ProxyContext) Run(ctx context.Context, serverAddress, name string) (err error) { if Options.Debug || Options.ExtraDebug { p.AddHandler(NewDebugLogger(Options.ExtraDebug)) @@ -301,54 +351,38 @@ func (p *ProxyContext) Run(ctx context.Context, serverAddress, name string) (err } } + defer func() { + for _, handler := range p.handlers { + if handler.OnEnd != nil { + handler.OnEnd() + } + } + }() + + isReplay := false if strings.HasPrefix(serverAddress, "PCAP!") { - return createReplayConnection(ctx, serverAddress[5:], p) + isReplay = true } - GetTokenSource() // ask for login before listening - var cdp *login.ClientData = nil - if p.WithClient { - var packs []*resource.Pack - if Options.Preload { - logrus.Info(locale.Loc("preloading_packs", nil)) - serverConn, err := connectServer(ctx, serverAddress, nil, true, func(header packet.Header, payload []byte, src, dst net.Addr) {}) - if err != nil { - return fmt.Errorf(locale.Loc("failed_to_connect", locale.Strmap{"Address": serverAddress, "Err": err})) - } - serverConn.Close() - packs = serverConn.ResourcePacks() - logrus.Infof(locale.Locm("pack_count_loaded", locale.Strmap{"Count": len(packs)}, len(packs))) - } - - status := minecraft.NewStatusProvider(fmt.Sprintf("%s Proxy", name)) - p.Listener, err = minecraft.ListenConfig{ - StatusProvider: status, - ResourcePacks: packs, - AcceptedProtocols: []minecraft.Protocol{ - //dummyProto{id: 567, ver: "1.19.60"}, - }, - }.Listen("raknet", ":19132") + if p.WithClient && !isReplay { + err = p.connectClient(ctx, serverAddress, &cdp) if err != nil { return err } + defer func() { - if p.Client != nil { - p.Listener.Disconnect(p.Client, DisconnectReason) + if p.Listener != nil { + if p.Client != nil { + p.Listener.Disconnect(p.Client.(*minecraft.Conn), DisconnectReason) + } + p.Listener.Close() } - p.Listener.Close() }() + } - logrus.Infof(locale.Loc("listening_on", locale.Strmap{"Address": p.Listener.Addr()})) - logrus.Infof(locale.Loc("help_connect", nil)) - - c, err := p.Listener.Accept() - if err != nil { - return err - } - p.Client = c.(*minecraft.Conn) - cd := p.Client.ClientData() - cdp = &cd + if p.CustomClientData != nil { + cdp = p.CustomClientData } for _, handler := range p.handlers { @@ -358,11 +392,7 @@ func (p *ProxyContext) Run(ctx context.Context, serverAddress, name string) (err handler.OnClientConnect(p.Client) } - if p.CustomClientData != nil { - cdp = p.CustomClientData - } - - p.Server, err = connectServer(ctx, serverAddress, cdp, p.AlwaysGetPacks, func(header packet.Header, payload []byte, src, dst net.Addr) { + packetFunc := func(header packet.Header, payload []byte, src, dst net.Addr) { if header.PacketID == packet.IDRequestNetworkSettings { p.clientAddr = src } @@ -372,7 +402,16 @@ func (p *ProxyContext) Run(ctx context.Context, serverAddress, name string) (err } handler.PacketFunc(header, payload, src, dst) } - }) + } + + if isReplay { + p.Server, err = createReplayConnector(serverAddress[5:], packetFunc) + if err != nil { + return err + } + } else { + err = p.connectServer(ctx, serverAddress, cdp, packetFunc) + } if err != nil { for _, handler := range p.handlers { if handler.ConnectCB == nil { @@ -415,26 +454,22 @@ func (p *ProxyContext) Run(ctx context.Context, serverAddress, name string) (err } wg := sync.WaitGroup{} - // server to client - wg.Add(1) - go func() { + doProxy := func(client bool) { defer wg.Done() - if err := p.proxyLoop(ctx, false); err != nil { + if err := p.proxyLoop(ctx, client); err != nil { logrus.Error(err) return } - }() + } + + // server to client + wg.Add(1) + go doProxy(false) // client to server if p.Client != nil { wg.Add(1) - go func() { - defer wg.Done() - if err := p.proxyLoop(ctx, true); err != nil { - logrus.Error(err) - return - } - }() + go doProxy(true) } wantSecondary := fp.Filter(func(handler *ProxyHandler) bool { @@ -458,11 +493,5 @@ func (p *ProxyContext) Run(ctx context.Context, serverAddress, name string) (err } wg.Wait() - - for _, handler := range p.handlers { - if handler.OnEnd != nil { - handler.OnEnd() - } - } return err } diff --git a/utils/replay.go b/utils/replay.go index 2050d06..ae5cc41 100644 --- a/utils/replay.go +++ b/utils/replay.go @@ -4,15 +4,18 @@ import ( "bytes" "context" "encoding/binary" + "errors" "fmt" "io" "net" "os" + "sync" "time" "github.com/sandertv/gophertunnel/minecraft" - "github.com/sandertv/gophertunnel/minecraft/protocol" + "github.com/sandertv/gophertunnel/minecraft/protocol/login" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" + "github.com/sandertv/gophertunnel/minecraft/resource" "github.com/sirupsen/logrus" ) @@ -34,171 +37,430 @@ func WriteReplayHeader(f io.Writer) { binary.Write(f, binary.LittleEndian, &header) } -func createReplayConnection(ctx context.Context, filename string, proxy *ProxyContext) error { - logrus.Infof("Reading replay %s", filename) +type replayConnector struct { + f *os.File + totalSize int64 + ver int - f, err := os.Open(filename) - if err != nil { - return err - } - stat, err := f.Stat() - if err != nil { - return err - } - totalSize := stat.Size() + packets chan packet.Packet + spawn chan struct{} + close chan struct{} + once sync.Once - // default version is version 1, since that didnt have a header - ver := 1 + pool packet.Pool + proto minecraft.Protocol + clientData login.ClientData + + gameData minecraft.GameData + + packetFunc PacketFunc + + downloadingPacks map[string]*downloadingPack + resourcePacks []*resource.Pack +} + +// downloadingPack is a resource pack that is being downloaded by a client connection. +type downloadingPack struct { + buf *bytes.Buffer + chunkSize uint32 + size uint64 + expectedIndex uint32 + newFrag chan []byte + contentKey string +} + +func (r *replayConnector) readHeader() error { + r.ver = 1 magic := make([]byte, 4) - io.ReadAtLeast(f, magic, 4) + io.ReadAtLeast(r.f, magic, 4) if bytes.Equal(magic, replayMagic) { var header replayHeader - if err := binary.Read(f, binary.LittleEndian, &header); err != nil { + if err := binary.Read(r.f, binary.LittleEndian, &header); err != nil { return err } - ver = int(header.Version) + r.ver = int(header.Version) } else { logrus.Info("Version 1 capture assumed.") - f.Seek(-4, io.SeekCurrent) + r.f.Seek(-4, io.SeekCurrent) + } + return nil +} + +func (r *replayConnector) readPacket() (payload []byte, toServer bool, err error) { + var magic uint32 = 0 + var packetLength uint32 = 0 + timeReceived := time.Now() + + offset, _ := r.f.Seek(0, io.SeekCurrent) + if offset == r.totalSize { + logrus.Info("Reached End") + return nil, toServer, nil } - server, client := &net.UDPAddr{ - IP: net.IPv4(1, 1, 1, 1), - }, &net.UDPAddr{ - IP: net.IPv4(2, 2, 2, 2), + binary.Read(r.f, binary.LittleEndian, &magic) + if magic != 0xAAAAAAAA { + return nil, toServer, fmt.Errorf("wrong Magic") + } + binary.Read(r.f, binary.LittleEndian, &packetLength) + binary.Read(r.f, binary.LittleEndian, &toServer) + if r.ver >= 2 { + var timeMs int64 + binary.Read(r.f, binary.LittleEndian, &timeMs) + timeReceived = time.UnixMilli(timeMs) } - proxy.clientAddr = client - proxy.Server = minecraft.NewConn() + payload = make([]byte, packetLength) + n, err := r.f.Read(payload) + if err != nil { + return nil, toServer, err + } + if n != int(packetLength) { + return nil, toServer, fmt.Errorf("truncated") + } + var magic2 uint32 + binary.Read(r.f, binary.LittleEndian, &magic2) + if magic2 != 0xBBBBBBBB { + return nil, toServer, fmt.Errorf("wrong Magic2") + } + + _ = timeReceived + return payload, toServer, nil +} + +func (r *replayConnector) handleLoginSequence(pk packet.Packet) (bool, error) { + switch pk := pk.(type) { + case *packet.StartGame: + r.SetGameData(minecraft.GameData{ + WorldName: pk.WorldName, + WorldSeed: pk.WorldSeed, + Difficulty: pk.Difficulty, + EntityUniqueID: pk.EntityUniqueID, + EntityRuntimeID: pk.EntityRuntimeID, + PlayerGameMode: pk.PlayerGameMode, + PersonaDisabled: pk.PersonaDisabled, + CustomSkinsDisabled: pk.CustomSkinsDisabled, + BaseGameVersion: pk.BaseGameVersion, + PlayerPosition: pk.PlayerPosition, + Pitch: pk.Pitch, + Yaw: pk.Yaw, + Dimension: pk.Dimension, + WorldSpawn: pk.WorldSpawn, + EditorWorld: pk.EditorWorld, + WorldGameMode: pk.WorldGameMode, + GameRules: pk.GameRules, + Time: pk.Time, + ServerBlockStateChecksum: pk.ServerBlockStateChecksum, + CustomBlocks: pk.Blocks, + Items: pk.Items, + PlayerMovementSettings: pk.PlayerMovementSettings, + ServerAuthoritativeInventory: pk.ServerAuthoritativeInventory, + Experiments: pk.Experiments, + ClientSideGeneration: pk.ClientSideGeneration, + ChatRestrictionLevel: pk.ChatRestrictionLevel, + DisablePlayerInteractions: pk.DisablePlayerInteractions, + }) + + case *packet.ResourcePacksInfo: + for _, pack := range pk.TexturePacks { + r.downloadingPacks[pack.UUID] = &downloadingPack{ + size: pack.Size, + buf: bytes.NewBuffer(make([]byte, 0, pack.Size)), + newFrag: make(chan []byte), + contentKey: pack.ContentKey, + } + } + + case *packet.ResourcePackDataInfo: + pack, ok := r.downloadingPacks[pk.UUID] + if !ok { + // We either already downloaded the pack or we got sent an invalid UUID, that did not match any pack + // sent in the ResourcePacksInfo packet. + return false, fmt.Errorf("unknown pack to download with UUID %v", pk.UUID) + } + if pack.size != pk.Size { + // Size mismatch: The ResourcePacksInfo packet had a size for the pack that did not match with the + // size sent here. + logrus.Printf("pack %v had a different size in the ResourcePacksInfo packet than the ResourcePackDataInfo packet\n", pk.UUID) + pack.size = pk.Size + } + pack.chunkSize = pk.DataChunkSize + + chunkCount := uint32(pk.Size / uint64(pk.DataChunkSize)) + if pk.Size%uint64(pk.DataChunkSize) != 0 { + chunkCount++ + } + + go func() { + for i := uint32(0); i < chunkCount; i++ { + select { + case <-r.close: + return + case frag := <-pack.newFrag: + // Write the fragment to the full buffer of the downloading resource pack. + _, _ = pack.buf.Write(frag) + } + } + + if pack.buf.Len() != int(pack.size) { + logrus.Printf("incorrect resource pack size: expected %v, but got %v\n", pack.size, pack.buf.Len()) + return + } + + // First parse the resource pack from the total byte buffer we obtained. + newPack, err := resource.FromBytes(pack.buf.Bytes()) + if err != nil { + logrus.Printf("invalid full resource pack data for UUID %v: %v\n", pk.UUID, err) + return + } + + r.resourcePacks = append(r.resourcePacks, newPack.WithContentKey(pack.contentKey)) + }() + + case *packet.ResourcePackChunkData: + pack, ok := r.downloadingPacks[pk.UUID] + if !ok { + // We haven't received a ResourcePackDataInfo packet from the server, so we can't use this data to + // download a resource pack. + return false, fmt.Errorf("resource pack chunk data for resource pack that was not being downloaded") + } + lastData := pack.buf.Len()+int(pack.chunkSize) >= int(pack.size) + if !lastData && uint32(len(pk.Data)) != pack.chunkSize { + // The chunk data didn't have the full size and wasn't the last data to be sent for the resource pack, + // meaning we got too little data. + return false, fmt.Errorf("resource pack chunk data had a length of %v, but expected %v", len(pk.Data), pack.chunkSize) + } + if pk.ChunkIndex != pack.expectedIndex { + return false, fmt.Errorf("resource pack chunk data had chunk index %v, but expected %v", pk.ChunkIndex, pack.expectedIndex) + } + pack.expectedIndex++ + pack.newFrag <- pk.Data + + case *packet.SetLocalPlayerAsInitialised: + if pk.EntityRuntimeID != r.gameData.EntityRuntimeID { + return false, fmt.Errorf("entity runtime ID mismatch: entity runtime ID in StartGame and SetLocalPlayerAsInitialised packets should be equal") + } + close(r.spawn) + return true, nil + } + return false, nil +} + +func (r *replayConnector) loop() { gameStarted := false - i := 0 + defer r.Close() for { - i += 1 - var magic uint32 = 0 - var packetLength uint32 = 0 - var toServer bool = false - timeReceived := time.Now() - - offset, _ := f.Seek(0, io.SeekCurrent) - if offset == totalSize { - logrus.Info("Reached End") - break - } - - binary.Read(f, binary.LittleEndian, &magic) - if magic != 0xAAAAAAAA { - return fmt.Errorf("wrong Magic") - } - binary.Read(f, binary.LittleEndian, &packetLength) - binary.Read(f, binary.LittleEndian, &toServer) - if ver >= 2 { - var timeMs int64 - binary.Read(f, binary.LittleEndian, &timeMs) - timeReceived = time.UnixMilli(timeMs) - } - - payload := make([]byte, packetLength) - n, err := f.Read(payload) + payload, toServer, err := r.readPacket() if err != nil { logrus.Error(err) - break } - if n != int(packetLength) { - return fmt.Errorf("truncated %d", i) + if payload == nil { + return + } + var src, dst = r.RemoteAddr(), r.LocalAddr() + if toServer { + src, dst = r.LocalAddr(), r.RemoteAddr() } - var magic2 uint32 - binary.Read(f, binary.LittleEndian, &magic2) - if magic2 != 0xBBBBBBBB { - return fmt.Errorf("wrong Magic2") - } - - pkData, err := minecraft.ParseData(payload, proxy.Server) + pkData, err := minecraft.ParseData(payload, r, src, dst) if err != nil { - return err + logrus.Error(err) + return } - pks, err := pkData.Decode(proxy.Server) + pks, err := pkData.Decode(r) if err != nil { logrus.Error(err) continue } - for _, pk := range pks { - f := bytes.NewBuffer(nil) - b := protocol.NewWriter(f, 0) - pk.Marshal(b) - - hdr := packet.Header{PacketID: pk.ID()} - - var src, dst net.Addr - if toServer { - src = client - dst = server + if !gameStarted { + gameStarted, _ = r.handleLoginSequence(pk) } else { - src = server - dst = client - } - - for _, handler := range proxy.handlers { - if handler.PacketFunc != nil { - handler.PacketFunc(hdr, f.Bytes(), src, dst) - } - } - - if gameStarted { - for _, handler := range proxy.handlers { - if handler.PacketCB != nil { - handler.PacketCB(pk, toServer, timeReceived) - } - } - } else { - switch pk := pk.(type) { - case *packet.StartGame: - proxy.Server.SetGameData(minecraft.GameData{ - WorldName: pk.WorldName, - WorldSeed: pk.WorldSeed, - Difficulty: pk.Difficulty, - EntityUniqueID: pk.EntityUniqueID, - EntityRuntimeID: pk.EntityRuntimeID, - PlayerGameMode: pk.PlayerGameMode, - PersonaDisabled: pk.PersonaDisabled, - CustomSkinsDisabled: pk.CustomSkinsDisabled, - BaseGameVersion: pk.BaseGameVersion, - PlayerPosition: pk.PlayerPosition, - Pitch: pk.Pitch, - Yaw: pk.Yaw, - Dimension: pk.Dimension, - WorldSpawn: pk.WorldSpawn, - EditorWorld: pk.EditorWorld, - WorldGameMode: pk.WorldGameMode, - GameRules: pk.GameRules, - Time: pk.Time, - ServerBlockStateChecksum: pk.ServerBlockStateChecksum, - CustomBlocks: pk.Blocks, - Items: pk.Items, - PlayerMovementSettings: pk.PlayerMovementSettings, - ServerAuthoritativeInventory: pk.ServerAuthoritativeInventory, - Experiments: pk.Experiments, - ClientSideGeneration: pk.ClientSideGeneration, - ChatRestrictionLevel: pk.ChatRestrictionLevel, - DisablePlayerInteractions: pk.DisablePlayerInteractions, - }) - gameStarted = true - for _, handler := range proxy.handlers { - if handler.ConnectCB != nil { - handler.ConnectCB(nil) - } - } - } + r.packets <- pk } } } - for _, handler := range proxy.handlers { - if handler.OnEnd != nil { - handler.OnEnd() - } +} + +func createReplayConnector(filename string, packetFunc PacketFunc) (r *replayConnector, err error) { + r = &replayConnector{ + pool: packet.NewPool(), + proto: minecraft.DefaultProtocol, + packetFunc: packetFunc, + spawn: make(chan struct{}), + close: make(chan struct{}), + packets: make(chan packet.Packet), + downloadingPacks: make(map[string]*downloadingPack), } + + logrus.Infof("Reading replay %s", filename) + + r.f, err = os.Open(filename) + if err != nil { + return nil, err + } + stat, err := r.f.Stat() + if err != nil { + return nil, err + } + r.totalSize = stat.Size() + + err = r.readHeader() + if err != nil { + return nil, err + } + + go r.loop() + return r, nil +} + +func (r *replayConnector) Close() error { + r.once.Do(func() { + close(r.close) + close(r.packets) + }) return nil } + +func (r *replayConnector) Authenticated() bool { + return true +} + +func (r *replayConnector) ChunkRadius() int { + return 80 +} + +func (r *replayConnector) ClientCacheEnabled() bool { + return false +} + +func (r *replayConnector) ClientData() login.ClientData { + return r.clientData +} + +func (r *replayConnector) DoSpawn() error { + return r.DoSpawnContext(context.Background()) +} + +func (r *replayConnector) DoSpawnContext(ctx context.Context) error { + select { + case <-r.close: + return errors.New("do spawn") + case <-ctx.Done(): + return errors.New("do spawn") + case <-r.spawn: + // Conn was spawned successfully. + return nil + } +} + +func (r *replayConnector) DoSpawnTimeout(timeout time.Duration) error { + c, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + return r.DoSpawnContext(c) +} + +func (r *replayConnector) Flush() error { + return nil +} + +func (r *replayConnector) GameData() minecraft.GameData { + return r.gameData +} + +func (r *replayConnector) IdentityData() login.IdentityData { + return login.IdentityData{} +} + +func (r *replayConnector) Latency() time.Duration { + return 0 +} + +func (r *replayConnector) LocalAddr() net.Addr { + return &net.UDPAddr{ + IP: net.IPv4(1, 1, 1, 1), + } +} + +func (r *replayConnector) Read(b []byte) (n int, err error) { + return 0, errors.New("not Implemented") +} + +func (r *replayConnector) ReadPacket() (pk packet.Packet, err error) { + select { + case <-r.close: + return nil, net.ErrClosed + case p, ok := <-r.packets: + if !ok { + err = net.ErrClosed + } + return p, err + } +} + +func (r *replayConnector) Write(b []byte) (n int, err error) { + return 0, errors.New("not Implemented") +} + +func (r *replayConnector) WritePacket(pk packet.Packet) error { + return nil +} + +func (r *replayConnector) RemoteAddr() net.Addr { + return &net.UDPAddr{ + IP: net.IPv4(2, 2, 2, 2), + } +} + +func (r *replayConnector) ResourcePacks() []*resource.Pack { + return r.resourcePacks +} + +func (r *replayConnector) SetGameData(data minecraft.GameData) { + r.gameData = data +} + +func (r *replayConnector) StartGame(data minecraft.GameData) error { + return r.StartGameContext(context.Background(), data) +} + +func (r *replayConnector) StartGameContext(ctx context.Context, data minecraft.GameData) error { + return nil +} + +func (r *replayConnector) StartGameTimeout(data minecraft.GameData, timeout time.Duration) error { + c, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + return r.StartGameContext(c, data) +} + +func (r *replayConnector) SetDeadline(t time.Time) error { + return nil +} + +func (r *replayConnector) SetReadDeadline(t time.Time) error { + return nil +} + +func (r *replayConnector) SetWriteDeadline(time.Time) error { + return nil +} + +func (r *replayConnector) Pool() packet.Pool { + return r.pool +} + +func (r *replayConnector) ShieldID() int32 { + return 0 +} + +func (r *replayConnector) Proto() minecraft.Protocol { + return r.proto +} + +func (r *replayConnector) PacketFunc(header packet.Header, payload []byte, src, dst net.Addr) { + if r.packetFunc != nil { + r.packetFunc(header, payload, src, dst) + } +} diff --git a/utils/resourcepack.go b/utils/resourcepack.go index 5028b62..4db8def 100644 --- a/utils/resourcepack.go +++ b/utils/resourcepack.go @@ -60,7 +60,7 @@ var PackFromBase = func(pack *resource.Pack) Pack { return b } -func GetPacks(server *minecraft.Conn) (packs []Pack, err error) { +func GetPacks(server minecraft.IConn) (packs []Pack, err error) { for _, pack := range server.ResourcePacks() { pack := PackFromBase(pack) if pack.Encrypted() && pack.CanDecrypt() { diff --git a/utils/utils.go b/utils/utils.go index b1a27dc..c85a0d9 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -76,7 +76,7 @@ func connectServer(ctx context.Context, address string, ClientData *login.Client return serverConn, nil } -func spawnConn(ctx context.Context, clientConn *minecraft.Conn, serverConn *minecraft.Conn, gd minecraft.GameData) error { +func spawnConn(ctx context.Context, clientConn minecraft.IConn, serverConn minecraft.IConn, gd minecraft.GameData) error { wg := sync.WaitGroup{} errs := make(chan error, 2) if clientConn != nil {