727 lines
16 KiB
Go
727 lines
16 KiB
Go
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
|
|
}
|
|
}
|
|
}
|