package main import ( "bufio" "crypto/rand" "crypto/rsa" "encoding/base64" "errors" "fmt" "github.com/google/uuid" "log" "net" "os" "reflect" "regexp" "strings" "time" ) const ( HOST = "127.0.0.2" PORT = "25565" TYPE = "tcp" ProtocolVersion = 767 VERSION = "1.21.1" CompressionThreshold = -1 ) var maxPlayers int32 var icon string var iconData []byte var serverID = make([]byte, 20) var serverPrivateKey *rsa.PrivateKey var serverPublicKey *rsa.PublicKey var usernameRegex = regexp.MustCompile("^[a-zA-Z0-9_]{3,16}$") var world World func readIcon(fileName string) (icon string) { var err error iconData, err = os.ReadFile(fileName) if err != nil { return } icon = "data:image/png;base64," + base64.StdEncoding.EncodeToString(iconData) return } func main() { maxPlayers = 200 icon = readIcon("icon.png") world = World{ Difficulty: 0, DifficultyLocked: false, Hardcore: false, Dimensions: []Dimension{ { ID: 0, Name: "minecraft:overworld", }, { ID: 1, Name: "minecraft:the_nether", }, { ID: 2, Name: "minecraft:the_end", }, }, ViewDistance: 4, SimulationDistance: 4, ReducedDebugInfo: false, EnableRespawnScreen: true, DoLimitedCrafting: false, Seed: uuid.New(), FeatureFlags: []string{"minecraft:vanilla", "minecraft:bundle", "minecraft:trade_rebalance"}, DataPacks: []DatapackInfo{{ Namespace: "minecraft", ID: "core", Version: "1.21", }}, ReportDetails: map[string]string{"I hate myself": "YES, REALLY"}, ServerLinks: []ServerLink{{ LabelString: "", LabelInt: 0, URL: "https://brn.systems", }}, TickRate: 20, } var err error //serverID, err = serverIDCreate() serverID = []byte("") if err != nil { log.Fatal(err) } serverPrivateKey, serverPublicKey, err = GenerateRSAKeyPair() if err != nil { return } listen, err := net.Listen(TYPE, HOST+":"+PORT) if err != nil { log.Fatal(err) } // close listener defer func(listen net.Listener) { err := listen.Close() if err != nil { log.Println(err) } }(listen) for { conn, err := listen.Accept() if err != nil { log.Fatal(err) } go handleRequest(conn) } } func handleRequest(conn net.Conn) { var errOut error var player = Player{ state: 0, conn: conn, version: 0, requestedAddress: "", requestedPort: 0, reader: bufio.NewReader(conn), compressionThreshold: -1, } for { packetID, packetData, err := player.readPacket() if err != nil { errOut = err break } switch player.state { case 0: // Handshake state switch packetID { case 0x00: version, address, port, nextState, err := getHandshakePacket(packetData) if err != nil { errOut = err break } player.version = version player.requestedAddress = address player.requestedPort = port player.state = nextState if player.state == 3 { player.state = 2 player.transferred = true } world.Players = append(world.Players, player) default: errOut = fmt.Errorf("unexpected packet %x in handshake state", packetID) log.Println(errOut) } case 1: // Status state switch packetID { case 0x00: err := player.sendStatusResponse("Hello there") if err != nil { errOut = err break } case 0x01: payload, err := getPingPacket(packetData) if err != nil { errOut = err break } err = player.sendPongPacket(payload) if err != nil { errOut = err break } default: errOut = fmt.Errorf("unexpected packet %x in status state", packetID) log.Println(errOut) } case 2: // Login state switch packetID { case 0x00: username, playerUUID, err := getLoginStart(packetData) if err != nil { errOut = err break } player.Entity.Name = username player.uuid = playerUUID if !usernameRegex.MatchString(player.Entity.Name) { errOut = errors.New("bad username") break } err = player.sendEncryptionRequest() if err != nil { errOut = err break } case 0x01: sharedSecretFromClient, verifyTokenFromClient, err := getEncryptionResponse(packetData) if err != nil { errOut = err break } decryptedVerifyTokenFromClient, err := rsa.DecryptPKCS1v15(rand.Reader, serverPrivateKey, verifyTokenFromClient) if err != nil { errOut = err break } if !reflect.DeepEqual(player.verifyToken, decryptedVerifyTokenFromClient) { errOut = errors.New("wrong session verify token") break } decryptedSecret, err := rsa.DecryptPKCS1v15(rand.Reader, serverPrivateKey, sharedSecretFromClient) if err != nil { errOut = err break } success, err := player.hasJoinedSession(decryptedSecret) if err != nil { errOut = err break } if !success { errOut = errors.New("joined session not found") break } if player.profile.ID != strings.Replace(player.uuid.String(), "-", "", -1) || player.profile.Name != player.Entity.Name { errOut = errors.New("wrong session name") break } player.encryptStream, player.decryptStream, err = NewCFB8Cipher(decryptedSecret) if err != nil { errOut = err break } err = player.sendCompression() if err != nil { errOut = err break } player.compressionThreshold = CompressionThreshold log.Println("sent compression") err = player.sendLoginSuccess() if err != nil { errOut = err break } log.Println("sent login success") err = player.sendDataPacks() if err != nil { errOut = err break } err = player.sendRegistryData() if err != nil { errOut = err break } err = player.sendFeatureFlags() if err != nil { errOut = err break } err = player.sendUpdateTags() if err != nil { errOut = err break } err = player.sendReportDetails() if err != nil { errOut = err break } err = player.sendServerLinks() if err != nil { errOut = err break } case 0x03: player.state = 3 default: errOut = fmt.Errorf("unexpected packet %x in login state", packetID) log.Println(errOut) } case 3: //configuration switch packetID { case 0x00: locale, viewDistance, chatMode, chatColors, skinParts, isRightHanded, textFiltering, serverListing, err := getClientInformation(packetData) if err != nil { errOut = err break } player.locale = locale player.viewDistance = viewDistance player.chatMode = chatMode player.chatColors = chatColors player.skinParts = skinParts player.isRightHanded = isRightHanded player.textFiltering = textFiltering player.serverListing = serverListing case 0x01: //TODO Implement cookie case 0x02: _, _, _ = getPluginPacket(packetData) case 0x03: player.state = 4 go player.sendKeepAlives() err := player.sendPlayStart() if err != nil { errOut = err break } player.sendDifficulty() //player.send case 0x04: player.keepAliveReceivedTimestamp = time.Now().Unix() case 0x05: err := player.getPongPacket(packetData) if err != nil { errOut = err break } case 0x06: err := player.getResourcePackPacket(packetData) if err != nil { errOut = err break } case 0x07: err := player.getDataPacksPacket(packetData) if err != nil { errOut = err break } default: errOut = fmt.Errorf("unexpected packet %x in configration state", packetID) log.Println(errOut) } case 4: switch packetID { case 0x00: // Confirm Teleportation teleportID, err := receiveVarint(packetData) if err != nil { errOut = err break } // Handle teleport confirmation logic here if player.teleportID == teleportID { player.waitingForTeleportConfirm = false } case 0x01: // Query Block Entity Tag transactionID, err := receiveVarint(packetData) if err != nil { errOut = err break } var position BlockPosition err = position.receivePosition(packetData) if err != nil { errOut = err break } // Handle block entity query logic here //player.queryBlockEntityTag(transactionID, position) case 0x02: // Change Difficulty difficulty, err := packetData.ReadByte() if err != nil { errOut = err break } // Change difficulty logic, ensure player is an operator if player.PermissionLevel >= 2 { world.Difficulty = difficulty for _, iterPlayer := range world.Players { iterPlayer.sendDifficulty() } } case 0x03: // Acknowledge Message messageCount, err := receiveVarint(packetData) if err != nil { errOut = err break } // Acknowledge message logic here messageCount = messageCount //player.acknowledgeMessage(messageCount) case 0x04, 0x05: // Chat Command & Signed Chat Command command, err := receiveString(packetData) if err != nil { errOut = err break } player.processChatCommand(command) case 0x06: // Chat Message message, err := receiveString(packetData) if err != nil { errOut = err break } _ = receiveInt64(packetData) _ = receiveInt64(packetData) // Handle the chat message here player.handleChatMessage(message) case 0x07: // Player Session //sessionId, err := receiveUUID(packetData) //if err != nil { // errOut = err // break //} //expiresAt := receiveInt64(packetData) //pubKeyLen, err := receiveVarint(packetData) // //maybe implement in the future case 0x08: // Chunk Batch Received _ = receiveFloat32(packetData) //chunks per tick case 0x09: // Client Status actionID, err := receiveVarint(packetData) if err != nil { errOut = err break } switch actionID { case 0: player.sendRespawn(true, true) case 1: player.sendAwardStatistics() } case 0x0A: // Client Information // Read Locale (16-character string) locale, err := receiveString(packetData) if err != nil { errOut = err break } player.locale = locale // Read View Distance (1 byte) viewDistance, err := packetData.ReadByte() if err != nil { errOut = err break } player.viewDistance = int8(viewDistance) // Read Chat Mode (VarInt Enum) chatMode, err := receiveVarint(packetData) if err != nil { errOut = err break } player.chatMode = chatMode // Read Chat Colors (Boolean) chatColors, err := packetData.ReadByte() if err != nil { errOut = err break } player.chatColors = chatColors != 0 // Read Displayed Skin Parts (Unsigned Byte) displayedSkinParts, err := packetData.ReadByte() if err != nil { errOut = err break } player.skinParts = displayedSkinParts // Read Main Hand (VarInt Enum) mainHand, err := receiveVarint(packetData) if err != nil { errOut = err break } player.isRightHanded = mainHand == 1 // Read Enable Text Filtering (Boolean) enableTextFiltering, err := receiveBool(packetData) if err != nil { errOut = err break } player.textFiltering = enableTextFiltering // Read Allow Server Listings (Boolean) allowServerListings, err := receiveBool(packetData) if err != nil { errOut = err break } player.serverListing = allowServerListings // Successfully parsed the packet errOut = nil case 0x0B: // Command Suggestions Request transactionId, err := receiveVarint(packetData) if err != nil { errOut = err break } text, err := receiveString(packetData) if err != nil { errOut = err break } player.requestCommandSuggestions(transactionId, text) case 0x0C: // Acknowledge Configuration player.state = 3 case 0x0D: // Click Container Button windowID, err := packetData.ReadByte() if err != nil { errOut = err break } if windowID != player.WindowID { break } buttonID, err := packetData.ReadByte() if err != nil { errOut = err break } _ = buttonID //TODO implement logic case 0x0E: // Click Container // Parse fields and handle click container logic here windowID, err := packetData.ReadByte() if err != nil { errOut = err break } if windowID != player.WindowID { break } // Read StateID (VarInt) stateID, err := receiveVarint(packetData) if err != nil { errOut = err break } // Read Slot (Short) var slot int16 slot = receiveInt16(packetData) if err != nil { errOut = err break } // Read Button (Byte) button, err := packetData.ReadByte() if err != nil { errOut = err break } // Read Mode (VarInt) mode, err := receiveVarint(packetData) if err != nil { errOut = err break } // Read number of ChangedSlots (VarInt) changedSlotsCount, err := receiveVarint(packetData) if err != nil { errOut = err break } var packet ClickContainerPacket // Initialize ChangedSlots slice packet.ChangedSlots = make([]Slot, changedSlotsCount) // Read each Slot in the ChangedSlots array for i := 0; i < int(changedSlotsCount); i++ { slotData, err := readSlot(data) if err != nil { errOut = err break } packet.ChangedSlots[i] = slotData } // Read CarriedItem (ItemStack) carriedItem, err := readSlot(data) if err != nil { errOut = err break } // Populate the packet struct packet.WindowID = windowID packet.StateID = stateID packet.Slot = slot packet.Button = int8(button) packet.Mode = mode packet.CarriedItem = carriedItem player.handleClickContainer(packet) case 0x0F: // Close Container windowID, err := packetData.ReadByte() if err != nil { errOut = err break } player.sendCloseContainer(windowID) case 0x10: // Change Container ItemStack State slotID, err := receiveVarint(packetData) if err != nil { errOut = err break } windowID, err := receiveVarint(packetData) if err != nil { errOut = err break } state, err := receiveBool(packetData) if err != nil { errOut = err break } player.changeContainerSlotState(slotID, windowID, state) case 0x11: // Cookie Response key, err := receiveString(packetData) if err != nil { errOut = err break } hasPayload, err := receiveBool(packetData) if err != nil { errOut = err break } // Handle cookie response logic here player.handleCookieResponse(key, hasPayload) case 0x12: // Serverbound Plugin Message channel, err := receiveString(packetData) if err != nil { errOut = err break } data := packetData // Rest is the byte array player.handlePluginMessage(channel, data) case 0x13: // Debug Sample Subscription sampleType, err := receiveVarint(packetData) if err != nil { errOut = err break } player.subscribeToDebugSample(sampleType) case 0x14: // Edit Book // Parse fields and handle book editing logic player.handleEditBook(packetData) default: errOut = fmt.Errorf("unexpected packet %x in play state", packetID) log.Println(errOut) } default: errOut = fmt.Errorf("unknown player state %d", player.state) break } if errOut != nil { break } } if //goland:noinspection GoDfaConstantCondition errOut != nil { log.Println(errOut.Error()) _ = player.sendDisconnect(fmt.Sprintf("Your server thread crashed: %s", errOut.Error())) } // Close connection err := conn.Close() if err != nil { log.Println(err) } for index, client := range world.Players { if player.conn == client.conn { world.Players = append(world.Players[:index], world.Players[index+1:]...) break } } }