Initial Commit

Signed-off-by: jolheiser <john.olheiser@gmail.com>
main
jolheiser 2021-12-06 16:43:07 -06:00
parent 6155f28671
commit eb9162d44b
Signed by: jolheiser
GPG Key ID: B853ADA5DA7BBF7A
7 changed files with 153 additions and 33 deletions

View File

@ -1,4 +1,4 @@
Copyright 2020 Etzelia Copyright 2021 John Olheiser
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

67
_hooks/mcm/mcm.go 100644
View File

@ -0,0 +1,67 @@
package main
import (
"encoding/json"
"flag"
"fmt"
"net/http"
"net/url"
"os"
"strings"
"github.com/peterbourgon/ff/v3"
)
type profile struct {
ID string `json:"id"`
Name string `json:"name"`
IP string `json:"ip"`
}
func (p *profile) UUID() string {
return fmt.Sprintf("%s-%s-%s-%s-%s", p.ID[:8], p.ID[8:12], p.ID[12:16], p.ID[16:20], p.ID[20:32])
}
func main() {
fs := flag.NewFlagSet("mcm", flag.ExitOnError)
baseFlag := fs.String("base", "", "Base URL for MCM API")
tokenFlag := fs.String("token", "", "MCM API Token")
if err := ff.Parse(fs, os.Args[1:],
ff.WithEnvVarPrefix("MCM_REGISTER"),
); err != nil {
panic(err)
}
var profile profile
if err := json.NewDecoder(os.Stdin).Decode(&profile); err != nil {
panic(err)
}
baseURL := strings.TrimSuffix(*baseFlag, "/")
u := fmt.Sprintf("%s/plugin/register", baseURL)
resp, err := http.PostForm(u, url.Values{
"api": []string{*tokenFlag},
"uuid": []string{profile.UUID()},
"username": []string{profile.Name},
"ip": []string{profile.IP},
})
if err != nil {
panic(err)
}
if resp.StatusCode != 200 {
panic("invalid response from MCM")
}
s := struct {
Status bool `json:"status"`
Message string `json:"message"`
}{}
if err := json.NewDecoder(resp.Body).Decode(&s); err != nil {
panic(err)
}
defer resp.Body.Close()
fmt.Println(s.Message)
}

4
go.mod
View File

@ -1,4 +1,4 @@
module git.canopymc.net/Etzelia/mineauth module git.jojodev.com/Canopy/mineauth
go 1.16 go 1.16
@ -6,5 +6,5 @@ require (
github.com/Tnze/go-mc v1.17.1-0.20210806203433-99081e1b9cfb github.com/Tnze/go-mc v1.17.1-0.20210806203433-99081e1b9cfb
github.com/google/uuid v1.1.1 github.com/google/uuid v1.1.1
github.com/peterbourgon/ff/v3 v3.1.0 github.com/peterbourgon/ff/v3 v3.1.0
go.jolheiser.com/beaver v1.1.2 github.com/rs/zerolog v1.26.0
) )

33
go.sum
View File

@ -2,8 +2,10 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Tnze/go-mc v1.17.1-0.20210806203433-99081e1b9cfb h1:jf5lM8mkIpYLFF2cORRGYVK9Mv/xuomG1hmR0b4EzXU= github.com/Tnze/go-mc v1.17.1-0.20210806203433-99081e1b9cfb h1:jf5lM8mkIpYLFF2cORRGYVK9Mv/xuomG1hmR0b4EzXU=
github.com/Tnze/go-mc v1.17.1-0.20210806203433-99081e1b9cfb/go.mod h1:t0AI38F1BEmmy8/uLhr9RCOUeDbBj3oUNQH9akjzMc0= github.com/Tnze/go-mc v1.17.1-0.20210806203433-99081e1b9cfb/go.mod h1:t0AI38F1BEmmy8/uLhr9RCOUeDbBj3oUNQH9akjzMc0=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/iancoleman/strcase v0.1.3 h1:dJBk1m2/qjL1twPLf68JND55vvivMupZ4wIzE8CTdBw= github.com/iancoleman/strcase v0.1.3 h1:dJBk1m2/qjL1twPLf68JND55vvivMupZ4wIzE8CTdBw=
@ -14,12 +16,35 @@ github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzI
github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
github.com/peterbourgon/ff/v3 v3.1.0 h1:5JAeDK5j/zhKFjyHEZQXwXBoDijERaos10RE+xamOsY= github.com/peterbourgon/ff/v3 v3.1.0 h1:5JAeDK5j/zhKFjyHEZQXwXBoDijERaos10RE+xamOsY=
github.com/peterbourgon/ff/v3 v3.1.0/go.mod h1:XNJLY8EIl6MjMVjBS4F0+G0LYoAqs0DTa4rmHHukKDE= github.com/peterbourgon/ff/v3 v3.1.0/go.mod h1:XNJLY8EIl6MjMVjBS4F0+G0LYoAqs0DTa4rmHHukKDE=
go.jolheiser.com/beaver v1.1.2 h1:X8voMSTy+8QUFzHlvG89EUVZY/xPQe6fQLVjUPjTvWY= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
go.jolheiser.com/beaver v1.1.2/go.mod h1:7X4F5+XOGSC3LejTShoBdqtRCnPWcnRgmYGmG3EKW8g= github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= github.com/rs/zerolog v1.26.0 h1:ORM4ibhEZeTeQlCojCK2kPz1ogAY4bGs4tD+SaAdGaE=
github.com/rs/zerolog v1.26.0/go.mod h1:yBiM87lvSqX8h0Ww4sdzNSkVYZ8dL2xjZJG1lAuGZEo=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

26
main.go
View File

@ -2,14 +2,16 @@ package main
import ( import (
"flag" "flag"
"git.canopymc.net/Etzelia/mineauth/server"
"net/http" "net/http"
"os" "os"
"time" "time"
"git.jojodev.com/Canopy/mineauth/server"
"github.com/peterbourgon/ff/v3" "github.com/peterbourgon/ff/v3"
"github.com/peterbourgon/ff/v3/fftoml" "github.com/peterbourgon/ff/v3/fftoml"
"go.jolheiser.com/beaver" "github.com/rs/zerolog"
"github.com/rs/zerolog/log"
) )
func main() { func main() {
@ -18,23 +20,30 @@ func main() {
timeoutFlag := fs.Int("timeout", 15, "HTTP timeout") timeoutFlag := fs.Int("timeout", 15, "HTTP timeout")
pingMessageFlag := fs.String("ping", "Login to authenticate!", "Message for the server list") pingMessageFlag := fs.String("ping", "Login to authenticate!", "Message for the server list")
communityFlag := fs.String("community", "", "URL to community") communityFlag := fs.String("community", "", "URL to community")
faviconFlag := fs.String("favicon", "", "Path to a favicon image for server list ping")
hooksFlag := make([]string, 0) hooksFlag := make([]string, 0)
fs.Func("hook", "Hook to run", func(hook string) error { fs.Func("hook", "Hook to run", func(hook string) error {
hooksFlag = append(hooksFlag, hook) hooksFlag = append(hooksFlag, hook)
return nil return nil
}) })
debugFlag := fs.Bool("debug", false, "Debug Logging") debugFlag := fs.Bool("debug", false, "Debug Logging")
jsonFlag := fs.Bool("json", false, "JSON Logging")
if err := ff.Parse(fs, os.Args[1:], if err := ff.Parse(fs, os.Args[1:],
ff.WithEnvVarPrefix("MCM_REGISTER"), ff.WithEnvVarPrefix("MINEAUTH"),
ff.WithConfigFileFlag("config"), ff.WithConfigFileFlag("config"),
ff.WithAllowMissingConfigFile(true), ff.WithAllowMissingConfigFile(true),
ff.WithConfigFileParser(fftoml.New().Parse), ff.WithConfigFileParser(fftoml.New().Parse),
); err != nil { ); err != nil {
beaver.Fatal(err) panic(err)
return return
} }
if *debugFlag { if *debugFlag {
beaver.Console.Level = beaver.DEBUG zerolog.SetGlobalLevel(zerolog.DebugLevel)
}
if !*jsonFlag {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout})
} }
http.DefaultClient.Timeout = time.Second * time.Duration(*timeoutFlag) http.DefaultClient.Timeout = time.Second * time.Duration(*timeoutFlag)
@ -42,15 +51,16 @@ func main() {
serv, err := server.New(server.Options{ serv, err := server.New(server.Options{
PingMessage: *pingMessageFlag, PingMessage: *pingMessageFlag,
CommunityURL: *communityFlag, CommunityURL: *communityFlag,
FaviconPath: *faviconFlag,
Hooks: hooksFlag, Hooks: hooksFlag,
}) })
if err != nil { if err != nil {
beaver.Error(err) log.Err(err).Msg("")
return return
} }
beaver.Infof("Listening on http://localhost:%d", *portFlag) log.Info().Msgf("Listening on http://localhost:%d", *portFlag)
if err := serv.Start(*portFlag); err != nil { if err := serv.Start(*portFlag); err != nil {
beaver.Error(err) log.Err(err).Msg("")
} }
} }

View File

@ -10,7 +10,7 @@ import (
"github.com/Tnze/go-mc/net" "github.com/Tnze/go-mc/net"
pk "github.com/Tnze/go-mc/net/packet" pk "github.com/Tnze/go-mc/net/packet"
"github.com/google/uuid" "github.com/google/uuid"
"go.jolheiser.com/beaver" "github.com/rs/zerolog/log"
) )
func (s *Server) acceptListPing(conn net.Conn) { func (s *Server) acceptListPing(conn net.Conn) {
@ -64,7 +64,7 @@ func (s *Server) listResp() string {
data, err := json.Marshal(list) data, err := json.Marshal(list)
if err != nil { if err != nil {
beaver.Errorf("could not marshal JSON: %v", err) log.Err(err).Msg("could not marshal JSON")
} }
return string(data) return string(data)
} }

View File

@ -8,14 +8,17 @@ import (
_ "embed" _ "embed"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
gonet "net"
"os"
"os/exec"
"strings"
"github.com/Tnze/go-mc/chat" "github.com/Tnze/go-mc/chat"
"github.com/Tnze/go-mc/net" "github.com/Tnze/go-mc/net"
pk "github.com/Tnze/go-mc/net/packet" pk "github.com/Tnze/go-mc/net/packet"
"github.com/google/uuid" "github.com/google/uuid"
"go.jolheiser.com/beaver" "github.com/rs/zerolog/log"
gonet "net"
"os/exec"
"strings"
) )
//go:embed favicon.png //go:embed favicon.png
@ -24,12 +27,14 @@ var favicon []byte
type Server struct { type Server struct {
privateKey *rsa.PrivateKey privateKey *rsa.PrivateKey
publicKey []byte publicKey []byte
favicon []byte
opts Options opts Options
} }
type Options struct { type Options struct {
CommunityURL string CommunityURL string
PingMessage string PingMessage string
FaviconPath string
Hooks []string Hooks []string
} }
@ -46,9 +51,23 @@ func New(opts Options) (*Server, error) {
private.Precompute() private.Precompute()
icon := favicon
if opts.FaviconPath != "" {
fi, err := os.Open(opts.FaviconPath)
if err != nil {
return nil, err
}
defer fi.Close()
icon, err = io.ReadAll(fi)
if err != nil {
return nil, err
}
}
return &Server{ return &Server{
privateKey: private, privateKey: private,
publicKey: public, publicKey: public,
favicon: icon,
opts: opts, opts: opts,
}, nil }, nil
} }
@ -62,7 +81,7 @@ func (s *Server) Start(port int) error {
for { for {
conn, err := l.Accept() conn, err := l.Accept()
if err != nil { if err != nil {
beaver.Errorf("Accept error: %v", err) log.Err(err).Msg("accept error")
} }
go s.acceptConn(conn) go s.acceptConn(conn)
} }
@ -73,13 +92,13 @@ func (s *Server) acceptConn(conn net.Conn) {
// handshake // handshake
_, intention, err := s.handshake(conn) _, intention, err := s.handshake(conn)
if err != nil { if err != nil {
beaver.Errorf("Handshake error: %v", err) log.Err(err).Msg("handshake error")
return return
} }
switch intention { switch intention {
default: default:
beaver.Errorf("Unknown handshake intention: %v", intention) log.Err(err).Msg("unknown handshake intention")
case 1: case 1:
s.acceptListPing(conn) s.acceptListPing(conn)
case 2: case 2:
@ -90,31 +109,31 @@ func (s *Server) acceptConn(conn net.Conn) {
func (s *Server) handlePlaying(conn net.Conn) { func (s *Server) handlePlaying(conn net.Conn) {
info, err := s.acceptLogin(conn) info, err := s.acceptLogin(conn)
if err != nil { if err != nil {
beaver.Errorf("login failed: %v", err) log.Err(err).Msg("login failed")
return return
} }
verify, err := s.encryptionRequest(conn) verify, err := s.encryptionRequest(conn)
if err != nil { if err != nil {
beaver.Errorf("could not send encryption request: %v", err) log.Err(err).Msg("could not send encryption request")
return return
} }
profile, secret, err := s.encryptionResponse(conn, info.Name, verify) profile, secret, err := s.encryptionResponse(conn, info.Name, verify)
if err != nil { if err != nil {
beaver.Errorf("could not get encryption response: %v", err) log.Err(err).Msg("could not get encryption response")
return return
} }
profile.IP, _, err = gonet.SplitHostPort(conn.Socket.RemoteAddr().String()) profile.IP, _, err = gonet.SplitHostPort(conn.Socket.RemoteAddr().String())
if err != nil { if err != nil {
beaver.Errorf("could not get user IP: %v", err) log.Err(err).Msg("could not get user IP")
return return
} }
payload, err := json.Marshal(profile) payload, err := json.Marshal(profile)
if err != nil { if err != nil {
beaver.Errorf("could not marshal payload: %v", err) log.Err(err).Msg("could not marshal payload")
return return
} }
@ -129,15 +148,14 @@ func (s *Server) handlePlaying(conn net.Conn) {
cmd.Stdin = bytes.NewReader(payload) cmd.Stdin = bytes.NewReader(payload)
out, err := cmd.CombinedOutput() out, err := cmd.CombinedOutput()
if err != nil { if err != nil {
beaver.Errorf("could not run hook `%s`: %v", h, err) log.Err(err).Str("hook", h).Str("output", string(out)).Msg("could not run hook")
beaver.Warn(string(out))
} }
}(hook) }(hook)
} }
econn, err := encryptedConn(conn, secret) econn, err := encryptedConn(conn, secret)
if err != nil { if err != nil {
beaver.Errorf("could not create encrypted connection: %v", err) log.Err(err).Msg("could not create encrypted connection")
return return
} }
@ -149,7 +167,7 @@ func (s *Server) handlePlaying(conn net.Conn) {
chat.Message{Text: msg}, chat.Message{Text: msg},
) )
if err := packet.Pack(econn, 0); err != nil { if err := packet.Pack(econn, 0); err != nil {
beaver.Errorf("could not disconnect player: %v", err) log.Err(err).Msg("could not disconnect player")
} }
} }