212 lines
4.2 KiB
Go
212 lines
4.2 KiB
Go
package server
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
_ "embed"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
gonet "net"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
|
|
"github.com/Tnze/go-mc/chat"
|
|
"github.com/Tnze/go-mc/net"
|
|
pk "github.com/Tnze/go-mc/net/packet"
|
|
"github.com/google/uuid"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
//go:embed favicon.png
|
|
var favicon []byte
|
|
|
|
type Server struct {
|
|
privateKey *rsa.PrivateKey
|
|
publicKey []byte
|
|
favicon []byte
|
|
opts Options
|
|
}
|
|
|
|
type Options struct {
|
|
CommunityURL string
|
|
PingMessage string
|
|
FaviconPath string
|
|
Hooks []string
|
|
}
|
|
|
|
func New(opts Options) (*Server, error) {
|
|
private, err := rsa.GenerateKey(rand.Reader, 1024)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error generate private key: %v", err)
|
|
}
|
|
|
|
public, err := x509.MarshalPKIXPublicKey(private.Public())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error form public key to PKIX, ASN.1 DER: %v", err)
|
|
}
|
|
|
|
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{
|
|
privateKey: private,
|
|
publicKey: public,
|
|
favicon: icon,
|
|
opts: opts,
|
|
}, nil
|
|
}
|
|
|
|
func (s *Server) Start(port int) error {
|
|
l, err := net.ListenMC(fmt.Sprintf(":%d", port))
|
|
if err != nil {
|
|
return fmt.Errorf("listen error: %v", err)
|
|
}
|
|
|
|
for {
|
|
conn, err := l.Accept()
|
|
if err != nil {
|
|
log.Err(err).Msg("accept error")
|
|
}
|
|
go s.acceptConn(conn)
|
|
}
|
|
}
|
|
|
|
func (s *Server) acceptConn(conn net.Conn) {
|
|
defer conn.Close()
|
|
// handshake
|
|
_, intention, err := s.handshake(conn)
|
|
if err != nil {
|
|
log.Err(err).Msg("handshake error")
|
|
return
|
|
}
|
|
|
|
switch intention {
|
|
default:
|
|
log.Err(err).Msg("unknown handshake intention")
|
|
case 1:
|
|
s.acceptListPing(conn)
|
|
case 2:
|
|
s.handlePlaying(conn)
|
|
}
|
|
}
|
|
|
|
func (s *Server) handlePlaying(conn net.Conn) {
|
|
info, err := s.acceptLogin(conn)
|
|
if err != nil {
|
|
log.Err(err).Msg("login failed")
|
|
return
|
|
}
|
|
|
|
verify, err := s.encryptionRequest(conn)
|
|
if err != nil {
|
|
log.Err(err).Msg("could not send encryption request")
|
|
return
|
|
}
|
|
|
|
profile, secret, err := s.encryptionResponse(conn, info.Name, verify)
|
|
if err != nil {
|
|
log.Err(err).Msg("could not get encryption response")
|
|
return
|
|
}
|
|
|
|
profile.IP, _, err = gonet.SplitHostPort(conn.Socket.RemoteAddr().String())
|
|
if err != nil {
|
|
log.Err(err).Msg("could not get user IP")
|
|
return
|
|
}
|
|
|
|
payload, err := json.Marshal(profile)
|
|
if err != nil {
|
|
log.Err(err).Msg("could not marshal payload")
|
|
return
|
|
}
|
|
|
|
for _, hook := range s.opts.Hooks {
|
|
go func(h string) {
|
|
s := strings.Split(h, " ")
|
|
var args []string
|
|
if len(s) > 1 {
|
|
args = s[1:]
|
|
}
|
|
cmd := exec.Command(s[0], args...)
|
|
cmd.Stdin = bytes.NewReader(payload)
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
log.Err(err).Str("hook", h).Str("output", string(out)).Msg("could not run hook")
|
|
}
|
|
}(hook)
|
|
}
|
|
|
|
econn, err := encryptedConn(conn, secret)
|
|
if err != nil {
|
|
log.Err(err).Msg("could not create encrypted connection")
|
|
return
|
|
}
|
|
|
|
msg := fmt.Sprintf("Thanks for authenticating, %s!", profile.Name)
|
|
if s.opts.CommunityURL != "" {
|
|
msg += fmt.Sprintf("\n\nJoin the community\n%s", s.opts.CommunityURL)
|
|
}
|
|
packet := pk.Marshal(0x00,
|
|
chat.Message{Text: msg},
|
|
)
|
|
if err := packet.Pack(econn, 0); err != nil {
|
|
log.Err(err).Msg("could not disconnect player")
|
|
}
|
|
}
|
|
|
|
type PlayerInfo struct {
|
|
Name string
|
|
UUID uuid.UUID
|
|
OPLevel int
|
|
}
|
|
|
|
// acceptLogin check player's account
|
|
func (s *Server) acceptLogin(conn net.Conn) (info PlayerInfo, err error) {
|
|
//login start
|
|
var p pk.Packet
|
|
err = conn.ReadPacket(&p)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
err = p.Scan((*pk.String)(&info.Name)) //decode username as pk.String
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// handshake receive and parse Handshake packet
|
|
func (s *Server) handshake(conn net.Conn) (protocol, intention int32, err error) {
|
|
var (
|
|
p pk.Packet
|
|
Protocol, Intention pk.VarInt
|
|
ServerAddress pk.String // ignored
|
|
ServerPort pk.UnsignedShort // ignored
|
|
)
|
|
// receive handshake packet
|
|
if err = conn.ReadPacket(&p); err != nil {
|
|
return
|
|
}
|
|
err = p.Scan(&Protocol, &ServerAddress, &ServerPort, &Intention)
|
|
return int32(Protocol), int32(Intention), err
|
|
}
|