This commit is contained in:
Bruno Rybársky 2024-08-22 19:33:17 +02:00
commit 8726a03e66
13 changed files with 1573 additions and 0 deletions

3
.gitignore vendored Normal file

@ -0,0 +1,3 @@
out
out/
out/*

8
.idea/.gitignore vendored Normal file

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

9
.idea/mcpingquick.iml Normal file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml Normal file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/mcpingquick.iml" filepath="$PROJECT_DIR$/.idea/mcpingquick.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml Normal file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

BIN
favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

8
go.mod Normal file

@ -0,0 +1,8 @@
module mcpingquick
go 1.23.0
require (
github.com/Tnze/go-mc v1.20.2
github.com/google/uuid v1.6.0
)

4
go.sum Normal file

@ -0,0 +1,4 @@
github.com/Tnze/go-mc v1.20.2 h1:arHCE/WxLCxY73C/4ZNLdOymRYtdwoXE05ohB7HVN6Q=
github.com/Tnze/go-mc v1.20.2/go.mod h1:geoRj2HsXSkB3FJBuhr7wCzXegRlzWsVXd7h7jiJ6aQ=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=

27
main.go Normal file

@ -0,0 +1,27 @@
package main
import (
"encoding/json"
"fmt"
"os"
)
func main() {
//resp, err := PingHostname("play.survival-games.cz", 25565)
resp, err := PingHostname("127.0.0.2", 25565)
//resp, err := PingHostname("vps.brn.systems", 25965)
if err != nil {
fmt.Println("Ty debil")
fmt.Println(err)
}
// Pretty print the response
respJson, err := json.MarshalIndent(resp, "", " ")
if err != nil {
fmt.Println("Error marshalling response to JSON:", err)
return
}
err = os.WriteFile("out/server.json", respJson, 0644)
if err != nil {
fmt.Println("Error creating server.json:", err)
}
}

459
packetcreator.go Normal file

@ -0,0 +1,459 @@
package main
import (
"bufio"
"bytes"
"compress/zlib"
"crypto/md5"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"io"
"math"
"strings"
"time"
)
// Decompress function using zlib
func decompress(data []byte) ([]byte, error) {
reader, err := zlib.NewReader(bytes.NewReader(data))
if err != nil {
return nil, err
}
defer func(reader io.ReadCloser) {
_ = reader.Close()
}(reader)
var decompressedData bytes.Buffer
_, err = io.Copy(&decompressedData, reader)
if err != nil {
return nil, err
}
return decompressedData.Bytes(), nil
}
// Compress function using zlib
func compress(data []byte) ([]byte, error) {
var compressedData bytes.Buffer
writer := zlib.NewWriter(&compressedData)
_, err := writer.Write(data)
if err != nil {
return nil, err
}
err = writer.Close()
if err != nil {
return nil, err
}
return compressedData.Bytes(), nil
}
func readPacket(reader *bufio.Reader, threshold int32) (packetID int32, packetData []byte, packetLength int, err error) {
packetLengthTemp, _, err := readVarint(reader)
if err != nil {
return
}
packetLength = packetLengthTemp
packetData = make([]byte, packetLength)
n, err := io.ReadFull(reader, packetData)
if err != nil {
return
}
if n != packetLength {
err = errors.New("packet read length mismatch")
return
}
var dataLength int
if threshold > 0 {
n, dataLengthTemp := receiveVarint(packetData)
packetData = packetData[n:]
dataLength = int(dataLengthTemp)
}
// If dataLength > 0, it means the packet is compressed
if dataLength > 0 {
packetData, err = decompress(packetData)
if err != nil {
return
}
}
n, packetID = receiveVarint(packetData)
packetLength -= n
packetData = packetData[n:]
return
}
func createPacket(packetID int32, packetData []byte, threshold int32, startedCompression bool) ([]byte, error) {
var dataBuffer []byte
addVarint(&dataBuffer, packetID)
dataBuffer = append(dataBuffer, packetData...)
var outBuffer []byte
length := int32(len(dataBuffer))
if startedCompression {
if threshold > 0 && length >= threshold {
// Compress the packet
compressedData, err := compress(dataBuffer)
if err != nil {
return nil, err
}
dataBuffer = compressedData
// Add the uncompressed length
addVarint(&outBuffer, int32(len(packetData)))
} else {
// Set Data Length to 0 if not compressed
addVarint(&outBuffer, 0)
}
}
addVarint(&outBuffer, int32(len(dataBuffer)))
// Append compressed or uncompressed data
outBuffer = append(outBuffer, dataBuffer...)
// Add the Packet Length
return outBuffer, nil
}
func addString(buffer *[]byte, str string) {
length := len(str)
addVarint(buffer, int32(length))
*buffer = append(*buffer, str...)
}
// Reads a VarInt from a bufio.Reader and returns the decoded value, the length read, and an error if any.
// Only negative values are transformed similar to Java's implementation.
func readVarint(r *bufio.Reader) (value int, length int, err error) {
var position uint
value = 0
for {
currentByte, err := r.ReadByte()
if err != nil {
return 0, length, err
}
value |= int(currentByte&0x7F) << position
length++
if (currentByte & 0x80) == 0 {
break
}
position += 7
if position >= 32 {
return 0, length, fmt.Errorf("VarInt is too big")
}
}
return value, length, nil
}
// Decodes a VarInt from the byte slice.
// This function handles the decoding in a manner similar to Java's VarInt,
// correctly managing positive and negative values.
func receiveVarint(b []byte) (currentOffset int, value int32) {
var shift uint
for {
byteValue := b[currentOffset]
currentOffset++
value |= int32(byteValue&0x7F) << shift
if (byteValue & 0x80) == 0 {
// Apply sign extension for negative values
break
}
shift += 7
if shift >= 32 {
panic("VarInt is too big")
}
}
return
}
func addVarint(buffer *[]byte, value int32) {
for {
// Extract the lower 7 bits of the value
temp := byte(value & 0x7F)
value >>= 7
// If the value is not zero or if there are more bytes to encode
if value != 0 && value != -1 {
temp |= 0x80 // Set the continuation bit
} else {
temp &= 0x7F // Clear the continuation bit if we're done
}
// Append the byte to the buffer
addByte(buffer, temp)
// Break the loop if no more continuation bit is set
if (temp & 0x80) == 0 {
break
}
}
}
func addVarlong(buffer *[]byte, value int64) {
for {
temp := byte(value & 0x7F) // Get the last 7 bits
value >>= 7
if value != 0 && value != -1 {
temp |= 0x80 // Set the continuation bit
}
addByte(buffer, temp)
if temp&0x80 == 0 {
break
}
}
}
// Decodes a VarLong from the byte slice.
func receiveVarlong(b []byte) (currentOffset int, value int64) {
var shift uint
for {
byteValue := b[currentOffset]
currentOffset++
value |= int64(byteValue&0x7F) << shift
if (byteValue & 0x80) == 0 {
// Sign extension for negative values
if shift < 32 {
value |= -1 << shift
}
break
}
shift += 7
if shift >= 32 {
panic("VarLong is too big")
}
}
return
}
func addByte(buffer *[]byte, byte byte) {
*buffer = append(*buffer, byte)
}
func receiveBool(buffer []byte) (currentOffset int, boolean bool) {
boolean = buffer[0] == 1
currentOffset = 1
return
}
func addBool(buffer *[]byte, bool bool) {
var boolByte byte
boolByte = 0x00
if bool {
boolByte = 0x01
}
*buffer = append(*buffer, boolByte)
}
func constructOfflinePlayerUUID(username string) (dataArray []byte) {
data := md5.Sum([]byte("OfflinePlayer:" + username))
dataArray = data[:]
// Set the version to 3 -> Name based md5 hash
dataArray[6] = (dataArray[6] & 0x0f) | 0x30
// IETF variant
dataArray[8] = (dataArray[8] & 0x3f) | 0x80
return dataArray
}
func createOfflineLoginPacket(username string, threshold int32) (outBuffer []byte, err error) {
addString(&outBuffer, username)
outBuffer = append(outBuffer, constructOfflinePlayerUUID(username)...)
outBuffer, err = createPacket(0, outBuffer, threshold, false)
return
}
func createClientBrandPacket(brand string, threshold int32, startedCompression bool) (outBuffer []byte, err error) {
addString(&outBuffer, "minecraft:brand")
addString(&outBuffer, brand)
outBuffer, err = createPacket(2, outBuffer, threshold, startedCompression)
return
}
func createConfirmLoginPacket(threshold int32, startedCompression bool) (packet []byte, err error) {
packet, err = createPacket(3, []byte{}, threshold, startedCompression)
return
}
func createStatusRequestPacket(threshold int32) (packet []byte, err error) {
packet, err = createPacket(0, []byte{}, threshold, false)
return
}
func addUint16(buffer *[]byte, value uint16) {
*buffer = binary.BigEndian.AppendUint16(*buffer, value)
}
func addInt64(buffer *[]byte, value int64) {
*buffer = binary.BigEndian.AppendUint64(*buffer, uint64(value))
}
func receiveFloat32(b []byte) (currentOffset int, value float32) {
value = math.Float32frombits(binary.BigEndian.Uint32(b))
currentOffset = binary.Size(value)
return
}
func receiveFloat64(b []byte) (currentOffset int, value float64) {
value = math.Float64frombits(binary.BigEndian.Uint64(b))
currentOffset = binary.Size(value)
return
}
func receiveInt32(b []byte) (currentOffset int, value int32) {
value = int32(binary.BigEndian.Uint32(b))
currentOffset = binary.Size(value)
return
}
func receiveInt64(b []byte) (currentOffset int, value int64) {
value = int64(binary.BigEndian.Uint64(b))
currentOffset = binary.Size(value)
return
}
func receiveByte(b []byte) (currentOffset int, value byte) {
value = b[0]
currentOffset = binary.Size(value)
return
}
func receivePosition(b []byte) (currentOffset int, position Position) {
encoded := binary.BigEndian.Uint64(b)
x, y, z := DecodePosition(encoded)
position = Position{X: x, Y: y, Z: z}
currentOffset = binary.Size(encoded)
return
}
func DecodePosition(encoded uint64) (x int32, y int16, z int32) {
// Extract x, y, z from encoded value
x = int32((encoded >> 38) & 0x3FFFFFF)
y = int16(encoded & 0xFFF)
z = int32((encoded >> 12) & 0x3FFFFFF)
// Adjust for sign
if x >= 1<<25 {
x -= 1 << 26
}
if y >= 1<<11 {
y -= 1 << 12
}
if z >= 1<<25 {
z -= 1 << 26
}
return
}
func createHandshakePacket(version int32, address string, port uint16, nextState int32, threshold int32) (outBuffer []byte, err error) {
addVarint(&outBuffer, version)
addString(&outBuffer, address)
addUint16(&outBuffer, port)
addVarint(&outBuffer, nextState)
outBuffer, err = createPacket(0, outBuffer, threshold, false)
return
}
func createTeleportConfirmPacket(teleportID int32, threshold int32, startedCompression bool) (outBuffer []byte, err error) {
addVarint(&outBuffer, teleportID)
outBuffer, err = createPacket(0, outBuffer, threshold, startedCompression)
return
}
func receiveString(buf []byte) (currentOffset int, strOut string) {
n, stringLen := receiveVarint(buf)
currentOffset = n
strOut = string(buf[currentOffset : currentOffset+int(stringLen)])
currentOffset += int(stringLen)
return
}
func receiveTextComponent(buf []byte) (currentOffset int, component TextComponent, err error) {
currentOffset, jsonString := receiveString(buf)
if strings.ContainsAny(jsonString, "") {
}
jsonDecoderText := json.NewDecoder(strings.NewReader(jsonString))
err = jsonDecoderText.Decode(&component)
if err != nil {
err = nil
component.Text = jsonString
component.CleanText = cleanMinecraftFormatting(jsonString)
}
return
}
func createEnabledDatapacksPacket(datapacks []DatapackInfo, threshold int32, startedCompression bool) (outBuffer []byte, err error) {
addVarint(&outBuffer, int32(len(datapacks)))
for _, datapack := range datapacks {
addString(&outBuffer, datapack.Namespace)
addString(&outBuffer, datapack.ID)
addString(&outBuffer, datapack.Version)
}
outBuffer, err = createPacket(0x07, outBuffer, threshold, startedCompression)
return
}
func createChatMessagePacket(message string, threshold int32, startedCompression bool) (outBuffer []byte, err error) {
addString(&outBuffer, message)
addInt64(&outBuffer, time.Now().Unix())
addInt64(&outBuffer, time.Now().Unix())
addBool(&outBuffer, false)
addVarint(&outBuffer, 0)
addByte(&outBuffer, 0)
addByte(&outBuffer, 0)
addByte(&outBuffer, 0)
outBuffer, err = createPacket(0x06, outBuffer, threshold, startedCompression)
return
}
func decodeEnabledDatapacks(buf []byte) (currentOffset int, outPacks []DatapackInfo) {
currentOffset, datapackCount := receiveVarint(buf)
for i := 0; i < int(datapackCount); i++ {
nextOffset, namespace := receiveString(buf[currentOffset:])
currentOffset += nextOffset
nextOffset, ID := receiveString(buf[currentOffset:])
currentOffset += nextOffset
nextOffset, version := receiveString(buf[currentOffset:])
currentOffset += nextOffset
enabledDatapack := DatapackInfo{
Namespace: namespace,
ID: ID,
Version: version,
}
outPacks = append(outPacks, enabledDatapack)
}
return
}
func createClientInformationPacket(locale string, renderDistance byte, chatMode int32, colors bool, skinParts byte, mainLeftHand bool, textFiltering bool, serverListing bool, threshold int32, startedCompression bool) (outBuffer []byte, err error) {
var mainHand int32
if mainLeftHand {
mainHand = 0
} else {
mainHand = 1
}
addString(&outBuffer, locale)
addByte(&outBuffer, renderDistance)
addVarint(&outBuffer, chatMode)
addBool(&outBuffer, colors)
addByte(&outBuffer, skinParts)
addVarint(&outBuffer, mainHand)
addBool(&outBuffer, textFiltering)
addBool(&outBuffer, serverListing)
outBuffer, err = createPacket(0, outBuffer, threshold, startedCompression)
return
}

782
packetsender.go Normal file

@ -0,0 +1,782 @@
package main
import (
"bufio"
"encoding/binary"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"net"
"reflect"
"strconv"
"strings"
)
func PingIP(ip net.IP, port uint16, host string) (response Response, errOut error) {
if host == "" {
host = ip.String()
}
addr := &net.TCPAddr{IP: ip, Port: int(port)}
conn, err := net.DialTCP("tcp", nil, addr)
if err != nil {
errOut = err
return
}
didRestartConnection := false
defer func(conn *net.TCPConn) {
_ = conn.Close()
}(conn)
var state byte
state = 0
// 0 - handshaking
// 1 - status
// 2 - login
// 3 - configuration
// 4 - play(since this is a scanner, we don't need to actually join the game)
response.Encryption = false
response.CompressionThreshold = -2
response.PluginDataSent = map[string]string{}
handshakePacketPing, err := createHandshakePacket(-1, host, port, 1, response.CompressionThreshold)
if err != nil {
errOut = err
return
}
_, err = conn.Write(handshakePacketPing)
if err != nil {
errOut = err
return
}
statusRequestPacket, err := createStatusRequestPacket(response.CompressionThreshold)
if err != nil {
errOut = err
return
}
_, err = conn.Write(statusRequestPacket)
if err != nil {
errOut = err
return
}
var username string
var PlayerUUID []byte
compressionStarted := false
reader := bufio.NewReader(conn)
for {
packetID, packetData, packetLength, err := readPacket(reader, response.CompressionThreshold)
if err != nil {
errOut = err
return
}
switch packetID {
case 0x00:
if state == 0 {
_, stringJson := receiveString(packetData)
jsonDecoder := json.NewDecoder(strings.NewReader(stringJson))
err = jsonDecoder.Decode(&response)
if !didRestartConnection {
handshakePacketJoin, err := createHandshakePacket(response.Version.Protocol, host, port, 2, response.CompressionThreshold)
if err != nil {
errOut = err
return
}
err = conn.Close()
if err != nil {
errOut = err
return
}
conn, err = net.DialTCP("tcp", nil, addr)
if err != nil {
errOut = err
return
}
reader = bufio.NewReader(conn)
_, err = conn.Write(handshakePacketJoin)
if err != nil {
errOut = err
return
}
didRestartConnection = true
}
if len(response.Players.Sample) > 0 {
username = response.Players.Sample[0].Name
} else {
username = "YeahAkis_"
}
response.Username = username
PlayerUUID = constructOfflinePlayerUUID(username)
response.RawMessage = stringJson
if err != nil {
errOut = err
println(stringJson)
return
}
state = 1
loginPacket, err := createOfflineLoginPacket(username, response.CompressionThreshold)
if err != nil {
errOut = err
return
}
_, err = conn.Write(loginPacket)
if err != nil {
errOut = err
return
}
state = 2
} else if state == 2 {
currentOffset, component, err := receiveTextComponent(packetData)
if err != nil {
errOut = err
return
}
if currentOffset != packetLength {
errOut = errors.New("packet length mismatch")
return
}
response.Message = component
return
}
break
case 0x01:
if state == 3 {
currentOffset, channelName := receiveString(packetData)
nextOffset, channelValue := receiveString(packetData[currentOffset:])
currentOffset += nextOffset
if currentOffset != packetLength {
errOut = errors.New("packet length mismatch")
return
}
response.PluginDataSent[channelName] = channelValue
} else if state == 2 {
response.Encryption = true
return
} else if state == 0 {
//ping response
}
break
case 0x02:
if state == 2 {
playerRecvUUID := packetData[:16]
currentOffset := 16
if !reflect.DeepEqual(PlayerUUID, playerRecvUUID) {
errOut = errors.New("player UUID mismatch")
return
}
addToOffset, receivedUsername := receiveString(packetData[16:])
currentOffset += addToOffset
if receivedUsername != username {
errOut = errors.New("username mismatch")
return
}
//TODO property array handling
confirmLoginPacket, err := createConfirmLoginPacket(response.CompressionThreshold, compressionStarted)
if err != nil {
errOut = err
return
}
_, err = conn.Write(confirmLoginPacket)
if err != nil {
errOut = err
return
}
clientBrandPacket, err := createClientBrandPacket("vanilla", response.CompressionThreshold, compressionStarted)
if err != nil {
errOut = err
return
}
_, err = conn.Write(clientBrandPacket)
if err != nil {
errOut = err
return
}
state = 3
} else if state == 3 {
currentOffset, component, err := receiveTextComponent(packetData)
if err != nil {
errOut = err
return
}
if currentOffset != packetLength {
errOut = errors.New("packet length mismatch")
return
}
response.Message = component
return
}
break
case 0x03:
if state == 2 {
currentOffset, compression := receiveVarint(packetData)
response.CompressionThreshold = compression
compressionStarted = true
if currentOffset != packetLength {
errOut = errors.New("packet length mismatch")
return
}
} else if state == 3 {
confirmPlaySwitchPacket, err := createConfirmLoginPacket(response.CompressionThreshold, compressionStarted)
if err != nil {
errOut = err
return
}
_, err = conn.Write(confirmPlaySwitchPacket)
state = 4
}
break
case 0x07:
if state == 3 {
currentOffset, registryID := receiveString(packetData)
newOffset, entryCount := receiveVarint(packetData[currentOffset:])
currentOffset += newOffset
var entries []RegistryEntry
for i := 0; i < int(entryCount); i++ {
newOffset, entryID := receiveString(packetData[currentOffset:])
currentOffset += newOffset
newOffset, hasNBT := receiveBool(packetData[currentOffset:])
currentOffset += newOffset
var nbtData []byte
if hasNBT {
nbtData = packetData[currentOffset:]
}
entries = append(entries, RegistryEntry{EntryID: entryID, HasNBT: hasNBT, NBTData: nbtData})
}
registryData := RegistryData{RegistryID: registryID, Entries: entries}
response.RegistryDatas = append(response.RegistryDatas, registryData)
}
break
case 0x0b:
if state == 4 {
currentOffset, difficulty := receiveByte(packetData)
newOffset, locked := receiveBool(packetData[currentOffset:])
currentOffset += newOffset
if currentOffset != packetLength {
errOut = errors.New("packet length mismatch")
return
}
response.ServerDifficulty = DifficultyObject{Difficulty: difficulty, Locked: locked}
}
case 0x0c:
if state == 3 {
currentOffset, flagCount := receiveVarint(packetData)
for i := 0; i < int(flagCount); i++ {
nextOffset, feature := receiveString(packetData[currentOffset:])
response.FeatureFlags = append(response.FeatureFlags, feature)
currentOffset += nextOffset
}
if currentOffset != packetLength {
errOut = errors.New("packet length mismatch")
}
clientInformationPacket, err := createClientInformationPacket("en_us", 32, 0, true, 0x7f, false, false, false, response.CompressionThreshold, compressionStarted)
if err != nil {
errOut = err
return
}
_, err = conn.Write(clientInformationPacket)
if err != nil {
errOut = err
return
}
}
break
case 0x0d:
if state == 3 {
currentOffset, entryCount := receiveVarint(packetData)
for i := 0; i < int(entryCount); i++ {
nextOffset, registryIdentifier := receiveString(packetData[currentOffset:])
currentOffset += nextOffset
nextOffset, lengthSubarray := receiveVarint(packetData[currentOffset:])
currentOffset += nextOffset
var tagArray []TagArray
for j := 0; j < int(lengthSubarray); j++ {
nextOffset, tagIdentifier := receiveString(packetData[currentOffset:])
currentOffset += nextOffset
nextOffset, subSubArrayLength := receiveVarint(packetData[currentOffset:])
currentOffset += nextOffset
var varInts []int32
for e := 0; e < int(subSubArrayLength); e++ {
nextOffset, varIntInArray := receiveVarint(packetData[currentOffset:])
currentOffset += nextOffset
varInts = append(varInts, varIntInArray)
}
tagPiece := TagArray{TagName: tagIdentifier, Entries: varInts}
tagArray = append(tagArray, tagPiece)
}
updateTag := UpdateTag{TagRegistryIdentifier: registryIdentifier, Tags: tagArray}
response.Tags = append(response.Tags, updateTag)
}
}
break
case 0x0e:
if state == 3 {
currentOffset, enabledDatapacks := decodeEnabledDatapacks(packetData)
if currentOffset != packetLength {
errOut = errors.New("packet length mismatch")
}
response.EnabledDatapacks = enabledDatapacks
datapackPacket, err := createEnabledDatapacksPacket(enabledDatapacks, response.CompressionThreshold, compressionStarted)
if err != nil {
errOut = err
return
}
_, err = conn.Write(datapackPacket)
if err != nil {
errOut = err
return
}
}
break
case 0x1D:
if state == 4 {
currentOffset, component, err := receiveTextComponent(packetData)
if err != nil {
errOut = err
return
}
if currentOffset != packetLength {
errOut = errors.New("packet length mismatch")
return
}
response.Message = component
return
}
break
case 0x2b:
if state == 4 {
currentOffset, entityID := receiveInt32(packetData)
newOffset, isHardCore := receiveBool(packetData[currentOffset:])
currentOffset += newOffset
newOffset, dimensionCount := receiveVarint(packetData[currentOffset:])
currentOffset += newOffset
var dimensions []string
for i := 0; i < int(dimensionCount); i++ {
newOffset, dimension := receiveString(packetData[currentOffset:])
currentOffset += newOffset
dimensions = append(dimensions, dimension)
}
newOffset, maxPlayers := receiveVarint(packetData[currentOffset:])
currentOffset += newOffset
newOffset, viewDistance := receiveVarint(packetData[currentOffset:])
currentOffset += newOffset
newOffset, simulationDistance := receiveVarint(packetData[currentOffset:])
currentOffset += newOffset
newOffset, reducedDebugInfo := receiveBool(packetData[currentOffset:])
currentOffset += newOffset
newOffset, enableRespawnScreen := receiveBool(packetData[currentOffset:])
currentOffset += newOffset
newOffset, doLimitedCrafting := receiveBool(packetData[currentOffset:])
currentOffset += newOffset
newOffset, dimensionType := receiveVarint(packetData[currentOffset:])
currentOffset += newOffset
newOffset, dimensionName := receiveString(packetData[currentOffset:])
currentOffset += newOffset
newOffset, hashedSeed := receiveInt64(packetData[currentOffset:])
currentOffset += newOffset
newOffset, gameMode := receiveByte(packetData[currentOffset:])
currentOffset += newOffset
newOffset, previousGameMode := receiveByte(packetData[currentOffset:])
currentOffset += newOffset
newOffset, isDebug := receiveBool(packetData[currentOffset:])
currentOffset += newOffset
newOffset, isFlat := receiveBool(packetData[currentOffset:])
currentOffset += newOffset
newOffset, hasDeathLocation := receiveBool(packetData[currentOffset:])
currentOffset += newOffset
var deathDimensionName string
var deathLocation Position
if hasDeathLocation {
newOffset, deathDimensionName = receiveString(packetData[currentOffset:])
currentOffset += newOffset
newOffset, deathLocation = receivePosition(packetData[currentOffset:])
currentOffset += newOffset
} else {
deathDimensionName = ""
deathLocation = Position{}
}
newOffset, portalCooldown := receiveVarint(packetData[currentOffset:])
currentOffset += newOffset
newOffset, enforcesSecureChat := receiveBool(packetData[currentOffset:])
currentOffset += newOffset
if currentOffset != packetLength {
errOut = errors.New("packet length mismatch")
return
}
response.PlayerLoginInfo = LoginInfo{
EntityID: entityID,
Hardcore: isHardCore,
Dimensions: dimensions,
MaxPlayers: maxPlayers,
ViewDistance: viewDistance,
SimulationDistance: simulationDistance,
ReducedDebugInfo: reducedDebugInfo,
EnableRespawnScreen: enableRespawnScreen,
DoLimitedCrafting: doLimitedCrafting,
DimensionType: dimensionType,
DimensionName: dimensionName,
HashedSeed: hashedSeed,
GameMode: gameMode,
PreviousGameMode: previousGameMode,
IsDebug: isDebug,
IsFlat: isFlat,
HasDeathLocation: hasDeathLocation,
DeathDimensionName: deathDimensionName,
DeathLocation: deathLocation,
PortalCooldown: portalCooldown,
EnforcesSecureChat: enforcesSecureChat,
}
}
break
case 0x38:
if state == 4 {
currentOffset, flags := receiveByte(packetData)
newOffset, flyingSpeed := receiveFloat32(packetData[currentOffset:])
currentOffset += newOffset
newOffset, fieldOfViewModifer := receiveFloat32(packetData[currentOffset:])
currentOffset += newOffset
if currentOffset != packetLength {
errOut = errors.New("packet length mismatch")
}
response.PlayerAbilities = PlayerAbilitiesObject{
Invulnerable: flags&0x01 != 0x00,
Flying: flags&0x02 != 0x00,
AllowFlying: flags&0x04 != 0x00,
CreativeMode: flags&0x08 != 0x00,
FlyingSpeed: flyingSpeed,
FieldOfViewModifier: fieldOfViewModifer,
}
}
break
case 0x53:
if state == 4 {
currentOffset, heldSlot := receiveByte(packetData)
if currentOffset != packetLength {
errOut = errors.New("packet length mismatch")
return
}
response.PlayerSlot = heldSlot
}
break
case 0x77:
if state == 4 {
//todo RECIPES
}
break
case 0x11:
//commands
if state == 4 {
}
break
case 0x41:
//recipe book
if state == 4 {
}
break
case 0x40:
//position update
if state == 4 {
currentOffset, x := receiveFloat64(packetData)
newOffset, y := receiveFloat64(packetData[currentOffset:])
currentOffset += newOffset
newOffset, z := receiveFloat64(packetData[currentOffset:])
currentOffset += newOffset
newOffset, yaw := receiveFloat32(packetData[currentOffset:])
currentOffset += newOffset
newOffset, pitch := receiveFloat32(packetData[currentOffset:])
currentOffset += newOffset
newOffset, flags := receiveByte(packetData[currentOffset:])
currentOffset += newOffset
newOffset, teleportId := receiveVarint(packetData[currentOffset:])
currentOffset += newOffset
if currentOffset != packetLength {
errOut = errors.New("packet length mismatch")
return
}
teleportConfirmPacket, err := createTeleportConfirmPacket(teleportId, response.CompressionThreshold, compressionStarted)
if err != nil {
errOut = err
return
}
_, err = conn.Write(teleportConfirmPacket)
if err != nil {
errOut = err
return
}
response.PlayerLocation = PlayerPosition{
X: x,
Y: y,
Z: z,
Yaw: yaw,
Pitch: pitch,
IsXRelative: flags&0x01 != 0x00,
IsYRelative: flags&0x02 != 0x00,
IsZRelative: flags&0x04 != 0x00,
IsYawRelative: flags&0x08 != 0x00,
IsPitchRelative: flags&0x10 != 0x00,
}
}
break
case 0x4b:
if state == 4 {
currentOffset, motd, errx := receiveTextComponent(packetData[2:])
currentOffset += 2
err = errx
if err != nil {
errOut = err
return
}
response.Description = motd
newOffset, hasIcon := receiveBool(packetData[currentOffset:])
currentOffset += newOffset
var iconSize int32
var iconData []byte
if hasIcon {
newOffset, iconSize = receiveVarint(packetData[currentOffset:])
currentOffset += newOffset
iconData = packetData[currentOffset : currentOffset+int(iconSize)]
response.Favicon.PngData = iconData
} else {
iconSize = 0
iconData = []byte{}
}
}
break
case 0x3e:
if state == 4 {
currentOffset, actions := receiveByte(packetData)
newOffset, numberOfPlayers := receiveVarint(packetData[currentOffset:])
currentOffset += newOffset
for i := 0; i < int(numberOfPlayers); i++ {
player := PlayerUpdate{}
player.UUID = packetData[currentOffset : currentOffset+16]
currentOffset += 16
if actions&0x01 != 0x00 {
newOffset, player.Name = receiveString(packetData[currentOffset:])
currentOffset += newOffset
newOffset, numberOfProperties := receiveVarint(packetData[currentOffset:])
currentOffset += newOffset
for x := 0; x < int(numberOfProperties); x++ {
property := PlayerProperty{}
newOffset, property.Name = receiveString(packetData[currentOffset:])
currentOffset += newOffset
newOffset, property.Value = receiveString(packetData[currentOffset:])
currentOffset += newOffset
newOffset, propertySigned := receiveByte(packetData[currentOffset:])
currentOffset += newOffset
if propertySigned != 0 {
newOffset, property.Signature = receiveString(packetData[currentOffset:])
currentOffset += newOffset
}
player.Properties = append(player.Properties, property)
}
}
if actions&0x02 != 0x00 {
newOffset, hasSignatureData := receiveBool(packetData[currentOffset:])
currentOffset += newOffset
if hasSignatureData {
player.SignatureData = &PlayerSignatureData{}
player.SignatureData.ChatSessionID = packetData[currentOffset : currentOffset+16]
currentOffset += 16
newOffset, player.SignatureData.PublicKeyExpiryTime = receiveInt64(packetData[currentOffset:])
currentOffset += newOffset
newOffset, encodedPublicKeySize := receiveVarint(packetData[currentOffset:])
currentOffset += newOffset
player.SignatureData.EncodedPublicKey = packetData[currentOffset : currentOffset+int(encodedPublicKeySize)]
currentOffset += int(encodedPublicKeySize)
newOffset, publicKeySignatureSize := receiveVarint(packetData[currentOffset:])
currentOffset += newOffset
player.SignatureData.PublicKeySignature = packetData[currentOffset : currentOffset+int(publicKeySignatureSize)]
currentOffset += int(publicKeySignatureSize)
}
}
if actions&0x04 != 0x00 {
newOffset, player.GameMode = receiveVarint(packetData[currentOffset:])
currentOffset += newOffset
}
if actions&0x08 != 0x00 {
newOffset, player.Listed = receiveBool(packetData[currentOffset:])
currentOffset += newOffset
}
if actions&0x10 != 0x00 {
newOffset, player.Ping = receiveVarint(packetData[currentOffset:])
currentOffset += newOffset
}
if actions&0x20 != 0x00 {
newOffset, hasDisplayName := receiveBool(packetData[currentOffset:])
currentOffset += newOffset
if hasDisplayName {
newOffset, displayName, err := receiveTextComponent(packetData[currentOffset:])
if err != nil {
errOut = err
return
}
currentOffset += newOffset
player.DisplayName = &displayName
}
}
response.PlayersInfo = append(response.PlayersInfo, player)
}
}
break
case 0x25:
if state == 4 {
currentOffset, x := receiveFloat64(packetData)
newOffset, z := receiveFloat64(packetData[currentOffset:])
currentOffset += newOffset
newOffset, oldDiameter := receiveFloat64(packetData[currentOffset:])
currentOffset += newOffset
newOffset, newDiameter := receiveFloat64(packetData[currentOffset:])
currentOffset += newOffset
newOffset, speed := receiveVarlong(packetData[currentOffset:])
currentOffset += newOffset
newOffset, portalTeleportBoundary := receiveVarint(packetData[currentOffset:])
currentOffset += newOffset
newOffset, warningBlocks := receiveVarint(packetData[currentOffset:])
currentOffset += newOffset
newOffset, warningTime := receiveVarint(packetData[currentOffset:])
currentOffset += newOffset
if currentOffset != packetLength {
errOut = errors.New("packet length mismatch")
return
}
response.WorldBorder = WorldBorderInfo{
X: x,
Z: z,
OldDiameter: oldDiameter,
NewDiameter: newDiameter,
Speed: speed,
PortalTeleportBoundry: portalTeleportBoundary,
WarningBlocks: warningBlocks,
WarningTime: warningTime,
}
}
break
case 0x64:
if state == 4 {
currentOffset, worldAge := receiveInt64(packetData)
newOffset, timeOfDay := receiveInt64(packetData[currentOffset:])
currentOffset += newOffset
if currentOffset != packetLength {
errOut = errors.New("packet length mismatch")
return
}
response.WorldAge = worldAge
response.TimeOfDay = timeOfDay
}
break
case 0x56:
if state == 4 {
currentOffset, location := receivePosition(packetData)
newOffset, angle := receiveFloat32(packetData[currentOffset:])
currentOffset += newOffset
if currentOffset != packetLength {
errOut = errors.New("packet length mismatch")
return
}
response.DefaultPositionSpawn = DefaultSpawnPosition{
Location: location,
Angle: angle,
}
}
break
case 0x22:
if state == 4 {
currentOffset, event := receiveByte(packetData)
nextOffset, value := receiveFloat32(packetData[currentOffset:])
currentOffset += nextOffset
if currentOffset != packetLength {
errOut = errors.New("packet length mismatch")
return
}
if event == 13 && value == 0 {
chatMessagePacket, err := createChatMessagePacket("Thanks for letting me scan this server", response.CompressionThreshold, compressionStarted)
if err != nil {
errOut = err
return
}
_, err = conn.Write(chatMessagePacket)
if err != nil {
errOut = err
return
}
return //we dont want chunks
}
}
case 0x1f:
if state == 4 {
//todo entityEvent
}
break
default:
packetIDBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(packetIDBytes, uint64(packetID))
fmt.Printf("Unknown packet type %d (%s)\n", packetID, hex.EncodeToString(packetIDBytes))
}
}
}
func PingHostname(host string, port uint16) (Response, error) {
tcpServer, err := net.ResolveTCPAddr("tcp", host+":"+strconv.Itoa(int(port)))
if err != nil {
return Response{}, err
}
return PingIP(tcpServer.IP, port, host)
}

8
player.go Normal file

@ -0,0 +1,8 @@
package main
import "github.com/google/uuid"
type Player struct {
Name string `json:"name"`
ID uuid.UUID `json:"id"`
}

251
response.go Normal file

@ -0,0 +1,251 @@
package main
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"regexp"
"strings"
)
type PlayerPosition struct {
X float64 `json:"x,omitempty"`
Y float64 `json:"y,omitempty"`
Z float64 `json:"z,omitempty"`
Yaw float32 `json:"yaw,omitempty"`
Pitch float32 `json:"pitch,omitempty"`
IsXRelative bool `json:"isXRelative,omitempty"`
IsYRelative bool `json:"isYRelative,omitempty"`
IsZRelative bool `json:"isZRelative,omitempty"`
IsYawRelative bool `json:"isYawRelative,omitempty"`
IsPitchRelative bool `json:"isPitchRelative,omitempty"`
}
type Position struct {
X int32 `json:"x,omitempty"`
Y int16 `json:"y,omitempty"`
Z int32 `json:"z,omitempty"`
}
type DefaultSpawnPosition struct {
Location Position `json:"location,omitempty"`
Angle float32 `json:"angle,omitempty"`
}
type PlayerAbilitiesObject struct {
Invulnerable bool `json:"invulnerable,omitempty"`
Flying bool `json:"flying,omitempty"`
AllowFlying bool `json:"allowFlying,omitempty"`
CreativeMode bool `json:"creativeMode,omitempty"`
FlyingSpeed float32 `json:"flyingSpeed,omitempty"`
FieldOfViewModifier float32 `json:"fieldOfViewModifier,omitempty"`
}
type PlayerProperty struct {
Name string `json:"name,omitempty"`
Value string `json:"value,omitempty"`
Signature string `json:"signature,omitempty"`
}
type PlayerSignatureData struct {
ChatSessionID []byte `json:"chatSessionID,omitempty"`
PublicKeyExpiryTime int64 `json:"publicKeyExpiryTime,omitempty"`
EncodedPublicKey []byte `json:"encodedPublicKey,omitempty"`
PublicKeySignature []byte `json:"publicKeySignature,omitempty"`
}
type PlayerUpdate struct {
UUID []byte `json:"uuid,omitempty"`
Name string `json:"name,omitempty"`
Properties []PlayerProperty `json:"properties,omitempty"`
SignatureData *PlayerSignatureData `json:"signatureData,omitempty"`
GameMode int32 `json:"gameMode,omitempty"`
Listed bool `json:"listed,omitempty"`
Ping int32 `json:"ping,omitempty"`
DisplayName *TextComponent `json:"displayName,omitempty"`
}
type LoginInfo struct {
EntityID int32 `json:"entityID,omitempty"`
Hardcore bool `json:"hardcore,omitempty"`
Dimensions []string `json:"dimensions,omitempty"`
MaxPlayers int32 `json:"maxPlayers,omitempty"`
ViewDistance int32 `json:"viewDistance,omitempty"`
SimulationDistance int32 `json:"simulationDistance,omitempty"`
ReducedDebugInfo bool `json:"reducedDebugInfo,omitempty"`
EnableRespawnScreen bool `json:"enableRespawnScreen,omitempty"`
DoLimitedCrafting bool `json:"doLimitedCrafting,omitempty"`
DimensionType int32 `json:"dimensionType,omitempty"`
DimensionName string `json:"dimensionName,omitempty"`
HashedSeed int64 `json:"hashedSeed,omitempty"`
GameMode byte `json:"gameMode,omitempty"`
PreviousGameMode byte `json:"previousGameMode,omitempty"`
IsDebug bool `json:"isDebug,omitempty"`
IsFlat bool `json:"isFlat,omitempty"`
HasDeathLocation bool `json:"hasDeathLocation,omitempty"`
DeathDimensionName string `json:"deathDimensionName,omitempty"`
DeathLocation Position `json:"deathLocation,omitempty"`
PortalCooldown int32 `json:"portalCooldown,omitempty"`
EnforcesSecureChat bool `json:"enforcesSecureChat,omitempty"`
}
type DifficultyObject struct {
Difficulty byte `json:"difficulty,omitempty"`
Locked bool `json:"locked,omitempty"`
}
type TagArray struct {
TagName string `json:"tagName,omitempty"`
Entries []int32 `json:"entries,omitempty"`
}
type UpdateTag struct {
TagRegistryIdentifier string `json:"tagRegistryIdentifier,omitempty"`
Tags []TagArray `json:"tags,omitempty"`
}
type RegistryEntry struct {
EntryID string `json:"entryID,omitempty"`
HasNBT bool `json:"hasNBT,omitempty"`
//TODO implement the nbt data
NBTData []byte `json:"nbtData,omitempty"`
}
type RegistryData struct {
RegistryID string `json:"registryID,omitempty"`
Entries []RegistryEntry `json:"entries,omitempty"`
}
type Favicon struct {
PngData []byte `json:"pngData,omitempty"`
}
type DatapackInfo struct {
Namespace string `json:"namespace,omitempty"`
ID string `json:"id,omitempty"`
Version string `json:"version,omitempty"`
}
type TextPiece struct {
Text string `json:"text,omitempty"`
Color string `json:"color,omitempty"`
CleanText string `json:"cleantext,omitempty"`
}
type TextComponent struct {
Text string `json:"text,omitempty"`
Extra []TextPiece `json:"extra,omitempty"`
CleanText string `json:"cleantext,omitempty"`
}
type WorldBorderInfo struct {
X float64 `json:"x,omitempty"`
Z float64 `json:"z,omitempty"`
OldDiameter float64 `json:"oldDiameter,omitempty"`
NewDiameter float64 `json:"newDiameter,omitempty"`
Speed int64 `json:"speed,omitempty"`
PortalTeleportBoundry int32 `json:"portalTeleportBoundry,omitempty"`
WarningBlocks int32 `json:"warningBlocks,omitempty"`
WarningTime int32 `json:"warningTime,omitempty"`
}
// Custom unmarshaler for the TextComponent type
func (d *TextComponent) UnmarshalJSON(data []byte) error {
// Try to unmarshal as a simple string
var text string
if err := json.Unmarshal(data, &text); err == nil {
d.Text = text
d.CleanText = cleanMinecraftFormatting(text)
return nil
}
// If that fails, try to unmarshal as an object with a "text" field and optional "extra" field
var temp struct {
Text string `json:"text,omitempty"`
Extra []TextPiece `json:"extra,omitempty"`
}
if err := json.Unmarshal(data, &temp); err == nil {
d.Text = temp.Text
d.Extra = temp.Extra
// Clean the text and combine it with cleaned children texts
var sb strings.Builder
sb.Write([]byte(temp.Text))
for _, extra := range temp.Extra {
sb.Write([]byte(extra.Text))
}
d.CleanText = cleanMinecraftFormatting(sb.String())
return nil
}
// Return an error if neither unmarshaling succeeds
return fmt.Errorf("could not unmarshal TextComponent: data is neither a string nor an object with 'text' and 'extra' fields")
}
// Function to clean Minecraft formatting from a text
func cleanMinecraftFormatting(text string) string {
// Regex to match Minecraft formatting codes like § and &
regex := regexp.MustCompile(`(?i)§[0-9A-FK-OR]`)
return regex.ReplaceAllString(text, "")
}
// Custom unmarshaler for the TextComponent type
func (f *Favicon) UnmarshalJSON(data []byte) error {
// Try to unmarshal as a simple string
var text string
if err := json.Unmarshal(data, &text); err == nil {
if strings.HasPrefix(text, "data:image/png;base64,") {
trimmed := strings.TrimPrefix(text, "data:image/png;base64,")
pngDecoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(trimmed))
pngData, err := io.ReadAll(pngDecoder)
if err != nil {
return err
}
f.PngData = pngData
return nil
} else {
return fmt.Errorf("wrong prefix for favicon")
}
}
return errors.New("could not unmarshal Favicon")
}
type Response struct {
RawMessage string `json:"rawMessage,omitempty"`
Version struct {
Name string `json:"name"`
Protocol int32 `json:"protocol"`
} `json:"version"`
Players struct {
Max int32 `json:"max"`
Online int32 `json:"online"`
Sample []Player `json:"sample"`
} `json:"players"`
Description TextComponent `json:"description"` // Handles both string and object with "text" and "extra" fields
Favicon Favicon `json:"favicon"`
EnforcesSecureChat bool `json:"enforcesSecureChat"`
PreventsChatReports bool `json:"preventsChatReports"`
CompressionThreshold int32 `json:"compressionThreshold,omitempty"`
IsOfflineMode bool `json:"IsOfflineMode,omitempty"`
PluginDataSent map[string]string `json:"PluginDataSent,omitempty"`
FeatureFlags []string `json:"featureFlags,omitempty"`
EnabledDatapacks []DatapackInfo `json:"EnabledDatapacks,omitempty"`
Encryption bool `json:"Encryption,omitempty"`
Message TextComponent `json:"Message,omitempty"`
RegistryDatas []RegistryData `json:"RegistryDatas,omitempty"`
Tags []UpdateTag `json:"Tags,omitempty"`
Username string `json:"Username,omitempty"`
PlayerLoginInfo LoginInfo `json:"PlayerLoginInfo,omitempty"`
ServerDifficulty DifficultyObject `json:"ServerDifficulty,omitempty"`
PlayerAbilities PlayerAbilitiesObject `json:"PlayerAbilities,omitempty"`
PlayerSlot byte `json:"PlayerSlot,omitempty"`
PlayerLocation PlayerPosition `json:"PlayerLocation,omitempty"`
DefaultPositionSpawn DefaultSpawnPosition `json:"DefaultPositionSpawn,omitempty"`
TimeOfDay int64 `json:"TimeOfDay,omitempty"`
WorldAge int64 `json:"worldAge,omitempty"`
WorldBorder WorldBorderInfo `json:"worldBorder,omitempty"`
PlayersInfo []PlayerUpdate `json:"playersinfo,omitempty"`
}