initial: discord bot for mc server status (slp + gs4 query)
This commit is contained in:
commit
5f092578ff
9 changed files with 543 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
mcstatus
|
||||||
43
config.go
Normal file
43
config.go
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
BotToken string `yaml:"bot_token"`
|
||||||
|
ChannelID string `yaml:"channel_id"`
|
||||||
|
ServerHost string `yaml:"server_host"`
|
||||||
|
ServerPort int `yaml:"server_port"`
|
||||||
|
UpdateInterval int `yaml:"update_interval"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadConfig(path string) (*Config, error) {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var cfg Config
|
||||||
|
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if cfg.BotToken == "" {
|
||||||
|
return nil, fmt.Errorf("bot_token is required")
|
||||||
|
}
|
||||||
|
if cfg.ChannelID == "" {
|
||||||
|
return nil, fmt.Errorf("channel_id is required")
|
||||||
|
}
|
||||||
|
if cfg.ServerHost == "" {
|
||||||
|
return nil, fmt.Errorf("server_host is required")
|
||||||
|
}
|
||||||
|
if cfg.ServerPort == 0 {
|
||||||
|
cfg.ServerPort = 25565
|
||||||
|
}
|
||||||
|
if cfg.UpdateInterval == 0 {
|
||||||
|
cfg.UpdateInterval = 15
|
||||||
|
}
|
||||||
|
return &cfg, nil
|
||||||
|
}
|
||||||
5
config.yaml
Normal file
5
config.yaml
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
bot_token: ""
|
||||||
|
channel_id: ""
|
||||||
|
server_host: "localhost"
|
||||||
|
server_port: 25565
|
||||||
|
update_interval: 15
|
||||||
5
config.yaml.example
Normal file
5
config.yaml.example
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
bot_token: ""
|
||||||
|
channel_id: ""
|
||||||
|
server_host: "localhost"
|
||||||
|
server_port: 25565
|
||||||
|
update_interval: 15
|
||||||
14
go.mod
Normal file
14
go.mod
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
module mcstatus
|
||||||
|
|
||||||
|
go 1.26
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/bwmarrin/discordgo v0.29.0
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gorilla/websocket v1.4.2 // indirect
|
||||||
|
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect
|
||||||
|
)
|
||||||
16
go.sum
Normal file
16
go.sum
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
github.com/bwmarrin/discordgo v0.29.0 h1:FmWeXFaKUwrcL3Cx65c20bTRW+vOb6k8AnaP+EgjDno=
|
||||||
|
github.com/bwmarrin/discordgo v0.29.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
|
||||||
|
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||||
|
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg=
|
||||||
|
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
48
main.go
Normal file
48
main.go
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/bwmarrin/discordgo"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
cfg *Config
|
||||||
|
session *discordgo.Session
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var err error
|
||||||
|
cfg, err = LoadConfig("config.yaml")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error loading config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
session, err = discordgo.New("Bot " + cfg.BotToken)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error creating session: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
session.AddHandler(ready)
|
||||||
|
session.Identify.Intents = discordgo.IntentsGuilds
|
||||||
|
|
||||||
|
if err := session.Open(); err != nil {
|
||||||
|
log.Fatalf("Error connecting: %v", err)
|
||||||
|
}
|
||||||
|
defer session.Close()
|
||||||
|
|
||||||
|
log.Println("Bot started")
|
||||||
|
sc := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt)
|
||||||
|
<-sc
|
||||||
|
log.Println("Shutting down...")
|
||||||
|
}
|
||||||
|
|
||||||
|
func ready(s *discordgo.Session, r *discordgo.Ready) {
|
||||||
|
log.Printf("Logged in as %s", r.User.String())
|
||||||
|
monitor := NewStatusMonitor(s, cfg)
|
||||||
|
monitor.Start()
|
||||||
|
}
|
||||||
130
monitor.go
Normal file
130
monitor.go
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bwmarrin/discordgo"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StatusMonitor struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
session *discordgo.Session
|
||||||
|
channelID string
|
||||||
|
host string
|
||||||
|
port int
|
||||||
|
interval time.Duration
|
||||||
|
messageID string
|
||||||
|
onlineSince time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStatusMonitor(s *discordgo.Session, cfg *Config) *StatusMonitor {
|
||||||
|
return &StatusMonitor{
|
||||||
|
session: s,
|
||||||
|
channelID: cfg.ChannelID,
|
||||||
|
host: cfg.ServerHost,
|
||||||
|
port: cfg.ServerPort,
|
||||||
|
interval: time.Duration(cfg.UpdateInterval) * time.Second,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *StatusMonitor) Start() {
|
||||||
|
m.updateStatus()
|
||||||
|
|
||||||
|
m.findExistingMessage()
|
||||||
|
|
||||||
|
ticker := time.NewTicker(m.interval)
|
||||||
|
go func() {
|
||||||
|
for range ticker.C {
|
||||||
|
m.updateStatus()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *StatusMonitor) findExistingMessage() {
|
||||||
|
msgs, err := m.session.ChannelMessages(m.channelID, 50, "", "", "")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error fetching messages: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, msg := range msgs {
|
||||||
|
if msg.Author.ID == m.session.State.User.ID {
|
||||||
|
m.mu.Lock()
|
||||||
|
m.messageID = msg.ID
|
||||||
|
m.mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *StatusMonitor) formatMessage(status *ServerStatus) string {
|
||||||
|
if !status.Online {
|
||||||
|
return "**Сервер оффлайн** 🔴"
|
||||||
|
}
|
||||||
|
|
||||||
|
var b strings.Builder
|
||||||
|
|
||||||
|
b.WriteString("**Игроки:**\n```\n")
|
||||||
|
if len(status.Players) == 0 {
|
||||||
|
b.WriteString("Никого нет")
|
||||||
|
} else {
|
||||||
|
for i, name := range status.Players {
|
||||||
|
if i > 0 {
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
b.WriteString(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.WriteString("```\n")
|
||||||
|
|
||||||
|
fmt.Fprintf(&b, ":busts_in_silhouette: Кол-во игроков: %d\n", status.OnlineCount)
|
||||||
|
|
||||||
|
uptime := time.Since(m.onlineSince)
|
||||||
|
hours := uptime.Hours()
|
||||||
|
if hours < 1 {
|
||||||
|
fmt.Fprintf(&b, ":repeat: Аптайм сервера: %.0f мин.", uptime.Minutes())
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(&b, ":repeat: Аптайм сервера: %.1f ч.", hours)
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *StatusMonitor) updateStatus() {
|
||||||
|
status, err := queryServer(m.host, m.port)
|
||||||
|
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Server query failed: %v", err)
|
||||||
|
m.onlineSince = time.Time{}
|
||||||
|
status = &ServerStatus{Online: false}
|
||||||
|
} else {
|
||||||
|
if m.onlineSince.IsZero() {
|
||||||
|
m.onlineSince = time.Now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content := m.formatMessage(status)
|
||||||
|
|
||||||
|
if m.messageID != "" {
|
||||||
|
_, err := m.session.ChannelMessageEdit(m.channelID, m.messageID, content)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error editing message (will create new): %v", err)
|
||||||
|
m.messageID = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.messageID == "" {
|
||||||
|
msg, err := m.session.ChannelMessageSend(m.channelID, content)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error sending message: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.messageID = msg.ID
|
||||||
|
}
|
||||||
|
}
|
||||||
281
query.go
Normal file
281
query.go
Normal file
|
|
@ -0,0 +1,281 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math/rand"
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServerStatus struct {
|
||||||
|
Online bool
|
||||||
|
Players []string
|
||||||
|
OnlineCount int
|
||||||
|
MaxPlayers int
|
||||||
|
}
|
||||||
|
|
||||||
|
type slpVersion struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Protocol int `json:"protocol"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type slpSample struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type slpPlayers struct {
|
||||||
|
Max int `json:"max"`
|
||||||
|
Online int `json:"online"`
|
||||||
|
Sample []slpSample `json:"sample"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type slpResponse struct {
|
||||||
|
Version slpVersion `json:"version"`
|
||||||
|
Players slpPlayers `json:"players"`
|
||||||
|
Description any `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryServer(host string, port int) (*ServerStatus, error) {
|
||||||
|
status, err := querySLP(host, port)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If SLP returned no player names, try GS4 query
|
||||||
|
if len(status.Players) == 0 && status.OnlineCount > 0 {
|
||||||
|
players, gs4Err := queryGS4(host, port)
|
||||||
|
if gs4Err == nil && len(players) > 0 {
|
||||||
|
status.Players = players
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return status, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func querySLP(host string, port int) (*ServerStatus, error) {
|
||||||
|
addr := fmt.Sprintf("%s:%d", host, port)
|
||||||
|
conn, err := net.DialTimeout("tcp", addr, 10*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("connect: %w", err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
if err := conn.SetDeadline(time.Now().Add(10 * time.Second)); err != nil {
|
||||||
|
return nil, fmt.Errorf("set deadline: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handshake
|
||||||
|
{
|
||||||
|
var payload strings.Builder
|
||||||
|
writeVarInt(&payload, 767) // protocol version
|
||||||
|
writeString(&payload, host) // server address
|
||||||
|
_ = binary.Write(&payload, binary.BigEndian, uint16(port)) // server port
|
||||||
|
writeVarInt(&payload, 1) // next state: status
|
||||||
|
|
||||||
|
packet := buildPacket(0, payload.String())
|
||||||
|
if _, err := conn.Write([]byte(packet)); err != nil {
|
||||||
|
return nil, fmt.Errorf("handshake write: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status request
|
||||||
|
{
|
||||||
|
packet := buildPacket(0, "")
|
||||||
|
if _, err := conn.Write([]byte(packet)); err != nil {
|
||||||
|
return nil, fmt.Errorf("request write: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read response
|
||||||
|
reader := bufio.NewReader(conn)
|
||||||
|
data, err := readPacket(reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r := strings.NewReader(string(data))
|
||||||
|
packetID, err := readVarInt(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read packet id: %w", err)
|
||||||
|
}
|
||||||
|
if packetID != 0 {
|
||||||
|
return nil, fmt.Errorf("unexpected packet id: %d", packetID)
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonStr, err := readString(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read json: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp slpResponse
|
||||||
|
if err := json.Unmarshal([]byte(jsonStr), &resp); err != nil {
|
||||||
|
return nil, fmt.Errorf("parse json: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
status := &ServerStatus{
|
||||||
|
Online: true,
|
||||||
|
OnlineCount: resp.Players.Online,
|
||||||
|
MaxPlayers: resp.Players.Max,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range resp.Players.Sample {
|
||||||
|
status.Players = append(status.Players, s.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return status, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryGS4(host string, port int) ([]string, error) {
|
||||||
|
addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", host, port))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := net.DialUDP("udp", nil, addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
sessionID := int32(rand.Int31n(0x7FFFFFFF))
|
||||||
|
|
||||||
|
// Handshake: FE FD 09 <session_id>
|
||||||
|
conn.SetDeadline(time.Now().Add(10 * time.Second))
|
||||||
|
hs := []byte{0xFE, 0xFD, 0x09}
|
||||||
|
hs = binary.BigEndian.AppendUint32(hs, uint32(sessionID))
|
||||||
|
conn.Write(hs)
|
||||||
|
|
||||||
|
resp := make([]byte, 64)
|
||||||
|
n, err := conn.Read(resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("gs4 handshake: %w", err)
|
||||||
|
}
|
||||||
|
data := resp[:n]
|
||||||
|
|
||||||
|
tokenStr := strings.TrimRight(string(data[5:]), "\x00")
|
||||||
|
challengeToken, err := strconv.Atoi(tokenStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse challenge: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Full stat query: FE FD 00 <session_id> <challenge_token> 00 00 00 00
|
||||||
|
conn.SetDeadline(time.Now().Add(10 * time.Second))
|
||||||
|
q := []byte{0xFE, 0xFD, 0x00}
|
||||||
|
q = binary.BigEndian.AppendUint32(q, uint32(sessionID))
|
||||||
|
q = binary.BigEndian.AppendUint32(q, uint32(challengeToken))
|
||||||
|
q = append(q, 0x00, 0x00, 0x00, 0x00)
|
||||||
|
conn.Write(q)
|
||||||
|
|
||||||
|
resp = make([]byte, 65536)
|
||||||
|
n, err = conn.Read(resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("gs4 query: %w", err)
|
||||||
|
}
|
||||||
|
data = resp[:n]
|
||||||
|
|
||||||
|
// Skip: type(1) + sessionID(4) + padding(11) = 16 bytes
|
||||||
|
content := data[16:]
|
||||||
|
|
||||||
|
sepIdx := bytes.Index(content, []byte{0x00, 0x00, 0x01})
|
||||||
|
if sepIdx < 0 {
|
||||||
|
return nil, fmt.Errorf("gs4: player separator not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
playerData := content[sepIdx+3:]
|
||||||
|
playerData = bytes.TrimPrefix(playerData, []byte("player_\x00\x00"))
|
||||||
|
playerData = bytes.TrimRight(playerData, "\x00")
|
||||||
|
players := bytes.Split(playerData, []byte{0x00})
|
||||||
|
|
||||||
|
var result []string
|
||||||
|
for _, p := range players {
|
||||||
|
if len(p) > 0 {
|
||||||
|
result = append(result, string(p))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildPacket(id int, payload string) string {
|
||||||
|
var inner strings.Builder
|
||||||
|
writeVarInt(&inner, id)
|
||||||
|
inner.WriteString(payload)
|
||||||
|
|
||||||
|
var buf strings.Builder
|
||||||
|
writeVarInt(&buf, inner.Len())
|
||||||
|
buf.WriteString(inner.String())
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func readPacket(r *bufio.Reader) ([]byte, error) {
|
||||||
|
length, err := readVarInt(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read length: %w", err)
|
||||||
|
}
|
||||||
|
if length < 0 || length > 65536 {
|
||||||
|
return nil, fmt.Errorf("invalid packet length: %d", length)
|
||||||
|
}
|
||||||
|
buf := make([]byte, length)
|
||||||
|
if _, err := io.ReadFull(r, buf); err != nil {
|
||||||
|
return nil, fmt.Errorf("read payload: %w", err)
|
||||||
|
}
|
||||||
|
return buf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readVarInt(r interface{ ReadByte() (byte, error) }) (int, error) {
|
||||||
|
result := 0
|
||||||
|
shift := 0
|
||||||
|
for {
|
||||||
|
b, err := r.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
result |= int(b&0x7F) << shift
|
||||||
|
shift += 7
|
||||||
|
if shift > 35 {
|
||||||
|
return 0, fmt.Errorf("varint too long")
|
||||||
|
}
|
||||||
|
if b&0x80 == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeVarInt(w interface{ WriteByte(byte) error }, value int) {
|
||||||
|
for value > 0x7F {
|
||||||
|
w.WriteByte(byte(value&0x7F) | 0x80)
|
||||||
|
value >>= 7
|
||||||
|
}
|
||||||
|
w.WriteByte(byte(value & 0x7F))
|
||||||
|
}
|
||||||
|
|
||||||
|
func readString(r interface{ ReadByte() (byte, error) }) (string, error) {
|
||||||
|
length, err := readVarInt(r)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if length < 0 || length > 65536 {
|
||||||
|
return "", fmt.Errorf("invalid string length: %d", length)
|
||||||
|
}
|
||||||
|
buf := make([]byte, length)
|
||||||
|
if _, err := io.ReadFull(r.(io.Reader), buf); err != nil {
|
||||||
|
return "", fmt.Errorf("read string: %w", err)
|
||||||
|
}
|
||||||
|
return string(buf), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeString(w interface{ WriteByte(byte) error }, s string) {
|
||||||
|
writeVarInt(w, len(s))
|
||||||
|
for i := 0; i < len(s); i++ {
|
||||||
|
w.WriteByte(s[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue