mineauth/server/server.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
}