diff --git a/go.mod b/go.mod index 4b6a9ef..a43365e 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,6 @@ require ( github.com/golang-jwt/jwt/v5 v5.2.0 github.com/google/uuid v1.5.0 github.com/lib/pq v1.10.9 - github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 github.com/r3labs/diff/v3 v3.0.1 gopkg.in/ini.v1 v1.67.0 gorm.io/driver/postgres v1.5.3 diff --git a/handlers/socket.go b/handlers/socket.go index be0f534..e16a750 100644 --- a/handlers/socket.go +++ b/handlers/socket.go @@ -5,7 +5,6 @@ import ( "github.com/ectrc/snow/socket" "github.com/gofiber/contrib/websocket" "github.com/gofiber/fiber/v2" - "github.com/google/uuid" ) func MiddlewareWebsocket(c *fiber.Ctx) error { @@ -22,7 +21,7 @@ func MiddlewareWebsocket(c *fiber.Ctx) error { protocol = "matchmaking" } - c.Locals("identifier", uuid.New().String()) + c.Locals("identifier", "ws-"+aid.RandomString(8)) c.Locals("protocol", protocol) return c.Next() diff --git a/handlers/tcp.go b/handlers/tcp.go new file mode 100644 index 0000000..03560f2 --- /dev/null +++ b/handlers/tcp.go @@ -0,0 +1,112 @@ +package handlers + +import ( + "fmt" + "net" + "strconv" + + "github.com/ectrc/snow/aid" + "github.com/ectrc/snow/socket" +) + +type tcpClient struct { + c *net.Conn + buffer []byte + jabber *socket.Socket[socket.JabberData] +} + +func (t *tcpClient) WriteMessage(messageType int, data []byte) error { + _, err := (*t.c).Write(data) + if err != nil { + return err + } + + return nil +} + +func (t *tcpClient) ReadMessage() (messageType int, p []byte, err error) { + n, err := (*t.c).Read(t.buffer) + if err != nil { + return 0, nil, err + } + + return 1, t.buffer[:n], nil +} + +func (t *tcpClient) loop() { + defer t.close() + + for { + _, p, err := t.ReadMessage() + if err != nil { + return + } + + aid.Print("(tcp) received: " + string(p)) + socket.JabberSocketOnMessage(t.jabber, p) + } +} + +func (t *tcpClient) close() error { + socket.JabberSockets.Delete(t.jabber.ID) + (*t.c).Close() + return nil +} + +type tcpServer struct { + ln net.Listener + port string + nope chan string +} + +func NewServer() (*tcpServer) { + portNumber, err := strconv.Atoi(aid.Config.API.Port[1:]) + if err != nil { + return nil + } + portNumber++ + + ln, err := net.Listen("tcp", fmt.Sprintf(":%d", portNumber)) + if err != nil { + return nil + } + + return &tcpServer{ + ln: ln, + port: fmt.Sprintf(":%d", portNumber), + } +} + +func (t *tcpServer) Listen() error { + defer t.ln.Close() + aid.Print("(tcp) listening on " + aid.Config.API.Host + t.port) + + go t.accept() + <-t.nope + + return nil +} + +func (t *tcpServer) accept() { + for { + conn, err := t.ln.Accept() + if err != nil { + return + } + + aid.Print("(tcp) new connection from " + conn.RemoteAddr().String()) + + go t.handle(conn) + } +} + +func (t *tcpServer) handle(conn net.Conn) { + tcpClient := &tcpClient{ + c: &conn, + buffer: make([]byte, 1024), + } + tcpClient.jabber = socket.NewJabberSocket(tcpClient, "tcp-"+aid.RandomString(8), socket.JabberData{}) + socket.JabberSockets.Set(tcpClient.jabber.ID, tcpClient.jabber) + + tcpClient.loop() +} \ No newline at end of file diff --git a/main.go b/main.go index ae5919b..2c5d9cd 100644 --- a/main.go +++ b/main.go @@ -192,11 +192,16 @@ func main() { aid.Print("(fiber) listening on " + aid.Config.API.Host + ":" + ld.Port) return nil }) - r.All("*", func(c *fiber.Ctx) error { return c.Status(fiber.StatusNotFound).JSON(aid.ErrorNotFound) }) + if aid.Config.Fortnite.Season <= 2 { + t := handlers.NewServer() + go t.Listen() + } + err := r.Listen("0.0.0.0" + aid.Config.API.Port) if err != nil { - panic(fmt.Sprintf("(fiber) ailed to listen: %v", err)) + panic(fmt.Sprintf("(fiber) failed to listen: %v", err)) } + } diff --git a/readme.md b/readme.md index 47f1e83..b843183 100644 --- a/readme.md +++ b/readme.md @@ -22,6 +22,19 @@ And once battle royale is completed ... - **Save The World** +## Feature List + +- **XMPP** From builds 3.6 onwards, season 1 and 2 are planned. +- **Friends** On every builds with the ability to add and remove friends. +- **Party System V2** This replaces the legacy xmpp driven party system. +- **Gifting** Of any item shop entry to any friend. +- **Locker Loadouts** On seasons 12 onwards, this allows for the saving and loading of multiple locker presets. +- **Item Refunding** Of previous shop purchases, will use a refund ticket if refunded in time. +- **Universal Item Shop** Works on all builds and will be updated every 24 hours. +- **Client Settings Storage** Uses amazon buckets to store client settings. +- **Support A Creator 5%** Use any display name and each purchase will give them 5% of the vbucks spent. +- **Discord Bot** Very useful to control players, their inventory and their settings + ## Supported MCP Actions `QueryProfile`, `ClientQuestLogin`, `MarkItemSeen`, `SetItemFavoriteStatusBatch`, `EquipBattleRoyaleCustomization`, `SetBattleRoyaleBanner`, `SetCosmeticLockerSlot`, `SetCosmeticLockerBanner`, `SetCosmeticLockerName`, `CopyCosmeticLoadout`, `DeleteCosmeticLoadout`, `PurchaseCatalogEntry`, `GiftCatalogEntry`, `RemoveGiftBox`, `RefundMtxPurchase`, `SetAffiliateName`, `SetReceiveGiftsEnabled` diff --git a/socket/jabber.go b/socket/jabber.go index 6ee0ba7..ce532cf 100644 --- a/socket/jabber.go +++ b/socket/jabber.go @@ -3,6 +3,7 @@ package socket import ( "fmt" "reflect" + "strings" "github.com/beevik/etree" "github.com/ectrc/snow/aid" @@ -17,6 +18,7 @@ type JabberData struct { } var jabberHandlers = map[string]func(*Socket[JabberData], *etree.Document) error { + "stream": jabberStreamHandler, "open": jabberOpenHandler, "iq": jabberIqRootHandler, "presence": jabberPresenceRootHandler, @@ -24,8 +26,11 @@ var jabberHandlers = map[string]func(*Socket[JabberData], *etree.Document) error } func HandleNewJabberSocket(identifier string) { + aid.Print("new jabber handle: " + identifier) + socket, ok := JabberSockets.Get(identifier) if !ok { + aid.Print("socket not found", identifier) return } defer JabberSockets.Delete(socket.ID) @@ -33,27 +38,47 @@ func HandleNewJabberSocket(identifier string) { for { _, message, failed := socket.Connection.ReadMessage() if failed != nil { + aid.Print("jabber message failed", failed) break } + + JabberSocketOnMessage(socket, message) + } +} - parsed := etree.NewDocument() - if err := parsed.ReadFromBytes(message); err != nil { +func JabberSocketOnMessage(socket *Socket[JabberData], message []byte) { + if strings.Contains(string(message), `">`) { + message = []byte(strings.ReplaceAll(string(message), `">`, `"/>`)) + } + + aid.Print("jabber message", string(message)) + parsed := etree.NewDocument() + if err := parsed.ReadFromString(string(message)); err != nil { + aid.Print("jabber message failed to parse", err) + return + } + + if handler, ok := jabberHandlers[parsed.Root().Tag]; ok { + if err := handler(socket, parsed); err != nil { + socket.Connection.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, err.Error())) return } - - if handler, ok := jabberHandlers[parsed.Root().Tag]; ok { - if err := handler(socket, parsed); err != nil { - socket.Connection.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, err.Error())) - return - } - } + return } + + aid.Print("jabber message not handled", parsed.Root().Tag) +} + +func jabberStreamHandler(socket *Socket[JabberData], parsed *etree.Document) error { + socket.Write([]byte(``)) + socket.Write([]byte(``)) + socket.Write([]byte(``)) + return nil } func jabberOpenHandler(socket *Socket[JabberData], parsed *etree.Document) error { socket.Write([]byte(``)) socket.Write([]byte(``)) - return nil } diff --git a/socket/socket.go b/socket/socket.go index a97b1ee..947ca2f 100644 --- a/socket/socket.go +++ b/socket/socket.go @@ -14,9 +14,14 @@ type MatchmakerData struct { Region string } +type WebSocket interface { + WriteMessage(messageType int, data []byte) error + ReadMessage() (messageType int, p []byte, err error) +} + type Socket[T JabberData | MatchmakerData] struct { ID string - Connection *websocket.Conn + Connection WebSocket Data *T Person *person.Person M sync.Mutex @@ -29,7 +34,7 @@ func (s *Socket[T]) Write(payload []byte) { s.Connection.WriteMessage(websocket.TextMessage, payload) } -func newSocket[T JabberData | MatchmakerData](conn *websocket.Conn, data ...T) *Socket[T] { +func newSocket[T JabberData | MatchmakerData](conn WebSocket, data ...T) *Socket[T] { additional := data[0] return &Socket[T]{ @@ -39,7 +44,7 @@ func newSocket[T JabberData | MatchmakerData](conn *websocket.Conn, data ...T) * } } -func NewJabberSocket(conn *websocket.Conn, id string, data JabberData) *Socket[JabberData] { +func NewJabberSocket(conn WebSocket, id string, data JabberData) *Socket[JabberData] { socket := newSocket[JabberData](conn, data) socket.ID = id return socket diff --git a/storage/hotfix.go b/storage/hotfix.go index 21dc4ba..3311526 100644 --- a/storage/hotfix.go +++ b/storage/hotfix.go @@ -1,29 +1,49 @@ package storage import ( + "fmt" + "strconv" + "github.com/ectrc/snow/aid" ) func GetDefaultEngine() []byte { - /*[OnlineSubsystemMcp] -bUsePartySystemV2=true - -[OnlineSubsystemMcp.OnlinePartySystemMcpAdapter] -bUsePartySystemV2=true*/ - return []byte(` -[OnlineSubsystemMcp.Xmpp] -bUseSSL=false -Protocol=ws -ServerAddr="ws://`+ aid.Config.API.Host + aid.Config.API.Port +`/?" - -[OnlineSubsystemMcp.Xmpp Prod] -bUseSSL=false -Protocol=ws -ServerAddr="ws://`+ aid.Config.API.Host + aid.Config.API.Port +`/?" + portNumber, err := strconv.Atoi(aid.Config.API.Port[1:]) + if err != nil { + return nil + } + portNumber++ + realPort := fmt.Sprintf("%d", portNumber) + str := ` [XMPP] bEnableWebsockets=true +[OnlineSubsystem] +bHasVoiceEnabled=true + +[Core.Log] +LogHttp=VeryVerbose +LogXmpp=VeryVerbose +LogBeacon=VeryVerbose +LogQos=VeryVerbose +LogOnline=VeryVerbose +LogOnlineGame=VeryVerbose +LogOnlineParty=VeryVerbose +LogParty=VeryVerbose +LogOnlineChat=VeryVerbose +LogGarbage=VeryVerbose +LogTemp=VeryVerbose +LogSourceControl=VeryVerbose +LogLootTables=VeryVerbose +LogMatchmakingServiceClient=VeryVerbose +LogMatchmakingServiceDedicatedServer=VeryVerbose +LogUAC=VeryVerbose +LogBattlEye=VeryVerbose +LogEasyAntiCheatServer=VeryVerbose +LogEasyAntiCheatClient=VeryVerbose +LogEasyAntiCheatNetComponent=VeryVerbose + [ConsoleVariables] n.VerifyPeer=0 FortMatchmakingV2.ContentBeaconFailureCancelsMatchmaking=0 @@ -32,7 +52,36 @@ FortMatchmakingV2.EnableContentBeacon=0 [/Script/Qos.QosRegionManager] NumTestsPerRegion=5 -PingTimeout=3.0`) +PingTimeout=3.0` + + if aid.Config.Fortnite.Season <= 2 { + str += ` + +[OnlineSubsystemMcp.Xmpp] +bUseSSL=false +Protocol=tcp +ServerAddr="`+ aid.Config.API.Host + `" +ServerPort=`+ realPort + ` + +[OnlineSubsystemMcp.Xmpp Prod] +bUseSSL=false +Protocol=tcp +ServerAddr="`+ aid.Config.API.Host + `" +ServerPort=`+ realPort + } else { + str += ` +[OnlineSubsystemMcp.Xmpp] +bUseSSL=false +Protocol=ws +ServerAddr="ws://`+ aid.Config.API.Host + aid.Config.API.Port +`/?" + +[OnlineSubsystemMcp.Xmpp Prod] +bUseSSL=false +Protocol=ws +ServerAddr="ws://`+ aid.Config.API.Host + aid.Config.API.Port +`/?"` + } + + return []byte(str) } func GetDefaultGame() []byte {