diff --git a/authenticate/authenticate.go b/authenticate/authenticate.go index 07c8ecb..1ab7ae7 100644 --- a/authenticate/authenticate.go +++ b/authenticate/authenticate.go @@ -51,7 +51,7 @@ func Authenticate(user, passwd string) (respData Response, err error) { err = fmt.Errorf("make request error: %v", err) return } - PostRequest.Header.Set("User-Agent", "gomcbot") + PostRequest.Header.Set("User-Agent", "go-mc") PostRequest.Header.Set("Connection", "keep-alive") resp, err := client.Do(PostRequest) if err != nil { diff --git a/bot/mcbot.go b/bot/mcbot.go index 38eb67d..a06f9db 100644 --- a/bot/mcbot.go +++ b/bot/mcbot.go @@ -152,3 +152,9 @@ func (c *Client) JoinServer(addr string, port int) (err error) { } } } + +// Conn return the MCConn of the Client. +// Only used when you want to handle the packets by yourself +func (c *Client) Conn() *net.Conn { + return c.conn +} diff --git a/cmd/daze/daze.go b/cmd/daze/daze.go index 057b9ec..289ad87 100644 --- a/cmd/daze/daze.go +++ b/cmd/daze/daze.go @@ -12,11 +12,11 @@ func main() { // For online-mode, you need login your Mojang account // and load your Name and UUID to client: // - // auth, err := authenticate.Authenticate(Your E-mail", "Your Password") + // auth, err := authenticate.Authenticate("Your E-mail", "Your Password") // if err != nil { // panic(err) // } - // c.Name, c.AsTk = auth.SelectedProfile.Name, auth.SelectedProfile.ID + // c.Name, c.Auth.UUID, c.AsTk = auth.SelectedProfile.Name, auth.SelectedProfile.ID, auth.AccessToken //Login err := c.JoinServer("localhost", 25565) diff --git a/cmd/substitute_server/substitute.go b/cmd/substitute_server/substitute.go new file mode 100644 index 0000000..5fb9715 --- /dev/null +++ b/cmd/substitute_server/substitute.go @@ -0,0 +1,454 @@ +package main + +import ( + "bytes" + "crypto/aes" + "crypto/rand" + "crypto/rsa" + "crypto/sha1" + "crypto/x509" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "strings" + + "github.com/Tnze/go-mc/authenticate" + "github.com/Tnze/go-mc/bot" + "github.com/Tnze/go-mc/chat" + "github.com/Tnze/go-mc/net" + "github.com/Tnze/go-mc/net/CFB8" + pk "github.com/Tnze/go-mc/net/packet" + "github.com/satori/go.uuid" +) + +func main() { + listener, err := net.ListenMC(fmt.Sprintf("%s:%d", ServerID, ServerPort)) + if err != nil { + panic(err) + } + for { + conn, err := listener.Accept() + if err != nil { + panic(err) + } + go Handle(conn) + } +} + +const ( + //Threshold 指定了数据传输时最小压缩包大小 + Threshold = 256 + + ServerID = "" + ServerPort = 25565 +) + +//Client 封装了与客户端之间的底层的网络交互 +type Client struct { + net.Conn + + ProtocolVersion int32 + Name string + ID uuid.UUID + skin string +} + +//OnPlayer 在正版玩家连接时被调用,其返回值将被作为断开连接的原因被发送给客户端 +//返回值应当为一个JSON Chat值,例如`"msg"` +var OnPlayer func(name string, UUID uuid.UUID, protocol int32) string + +//Handle 接受客户端连接 +func Handle(conn net.Conn) { + defer conn.Close() + c := Client{Conn: conn} + + nextState, err := c.handshake() + if err != nil { + // log.Println(err) + return + } + + const ( + CheckState = 1 + PlayerLogin = 2 + ) + switch nextState { + case CheckState: + c.status() + case PlayerLogin: + signal := make(chan int) + client := bot.NewClient() + auth, err := authenticate.Authenticate("Email", "Paswd") + if err != nil { + panic(err) + } + client.Name, client.Auth.UUID, client.AsTk = auth.SelectedProfile.Name, auth.SelectedProfile.ID, auth.AccessToken + go func() { + err := client.JoinServer("localhost", 25565) + if err != nil { + log.Fatal(err) + } + signal <- 1 + }() + + log.Println(c.Conn.Socket.RemoteAddr(), "协议号", c.ProtocolVersion) + + err = c.login() + if err != nil { + msg := chat.Message{Translate: ("multiplayer.disconnect." + err.Error())} + jmsg, err := json.Marshal(msg) + if err != nil { + return + } + packet := pk.Packet{ID: 0x00, Data: pk.String(string(jmsg)).Encode()} + c.WritePacket(packet) + return + } + <-signal + fmt.Println("start proxy") + conn := client.Conn() + go func() { + for { + p, err := c.ReadPacket() + if err != nil { + fmt.Println(err) + return + } + if err := conn.WritePacket(p); err != nil { + fmt.Println(err) + return + } + } + }() + for { + p, err := conn.ReadPacket() + if err != nil { + fmt.Println(err) + return + } + if err := c.WritePacket(p); err != nil { + fmt.Println(err) + return + } + } + + } +} + +func (c *Client) handshake() (nextState int32, err error) { + p, err := c.ReadPacket() + if err != nil { + return -1, err + } + if p.ID != 0 { + return -1, fmt.Errorf("packet ID 0x%X is not handshake", p.ID) + } + + var ( + sid pk.String + spt pk.Short + ) + if err := p.Scan( + (*pk.VarInt)(&c.ProtocolVersion), + &sid, &spt, + (*pk.VarInt)(&nextState)); err != nil { + return -1, err + } + + //检查服务器ID和端口是否匹配 + // if sid != ServerID || uint16(spt) != ServerPort { + // return -1, fmt.Errorf("server address rejected") + // } + + return nextState, nil +} + +func (c *Client) status() { + for i := 0; i < 2; i++ { + p, err := c.ReadPacket() + if err != nil { + break + } + + switch p.ID { + case 0x00: + respPack := getStatus() + c.WritePacket(respPack) + case 0x01: + c.WritePacket(p) + } + } +} + +func getStatus() pk.Packet { + return pk.Packet{ + ID: 0x00, + Data: pk.String(` + { + "version": { + "name": "1.14.1", + "protocol": 480 + }, + "players": { + "max": 1, + "online": 0, + "sample": [] + }, + "description": { + "text": "军刀破服" + } + } + `).Encode(), + } +} + +func disconnectID(protocal int32) byte { + switch protocal { + case 404: + return 0x1B + case 477, 480: + return 0x1A + default: + return 0x1A + } +} + +const verifyTokenLen = 16 +const OnlineMode = true + +func (c *Client) login() (err error) { + c.Name, err = c.loginStart() + if err != nil { + return fmt.Errorf("unexpected_query_response") + } + + if Threshold >= 0 { + err = c.setCompression(Threshold) + if err != nil { + return fmt.Errorf("unexpected_query_response") + } + } + + if OnlineMode { + key, err := rsa.GenerateKey(rand.Reader, 1024) + if err != nil { + return fmt.Errorf("unexpected_query_response") + } + + publicKey, err := x509.MarshalPKIXPublicKey(&key.PublicKey) + if err != nil { + return fmt.Errorf("generic") + } + + VT1, err := c.encryptionRequest(publicKey) + if err != nil { + return fmt.Errorf("generic") + } + + ESharedSecret, EVerifyToken, err := c.encryptionResponse() + if err != nil { + return fmt.Errorf("generic") + } + + SharedSecret, err := rsa.DecryptPKCS1v15(rand.Reader, key, ESharedSecret) + if err != nil { + return fmt.Errorf("generic") + } + VT2, err := rsa.DecryptPKCS1v15(rand.Reader, key, EVerifyToken) + if err != nil { + return fmt.Errorf("generic") + } + + if !bytes.Equal(VT1, VT2) { + return fmt.Errorf("generic") + } + + b, err := aes.NewCipher(SharedSecret) + if err != nil { + return fmt.Errorf("generic") + } + //启用加密 + c.SetCipher( + CFB8.NewCFB8Encrypt(b, SharedSecret), + CFB8.NewCFB8Decrypt(b, SharedSecret), + ) + hash := authDigest("", SharedSecret, publicKey) + resp, err := c.authentication(hash) + if err != nil { + return fmt.Errorf("authservers_down") + } + + c.ID, err = uuid.FromString(resp.ID) + if err != nil { + return fmt.Errorf("authservers_down") + } + + if c.Name != resp.Name { + return fmt.Errorf("unverified_username") + } + + c.skin = resp.Properties[0].Value + + } + + err = c.loginSuccess() + if err != nil { + return fmt.Errorf("generic") + } + return +} + +func (c *Client) loginStart() (string, error) { + loginStart, err := c.ReadPacket() + if err != nil { + return "", err + } + if loginStart.ID != 0x00 { + return "", fmt.Errorf("0x%02X is not LoginStart packet's ID", loginStart.ID) + } + var name pk.String + err = loginStart.Scan(&name) + return string(name), err +} + +func (c *Client) setCompression(threshold int) error { + sc := pk.Packet{ + ID: 0x03, + Data: pk.VarInt(threshold).Encode(), + } + err := c.WritePacket(sc) + c.SetThreshold(threshold) + return err +} + +func (c *Client) loginSuccess() error { + ls := pk.Packet{ID: 0x02} + ls.Data = append(ls.Data, pk.String(c.ID.String()).Encode()...) + ls.Data = append(ls.Data, pk.String(c.Name).Encode()...) + err := c.WritePacket(ls) + return err +} + +func (c *Client) encryptionRequest(publicKey []byte) ([]byte, error) { + var verifyToken [verifyTokenLen]byte + _, err := rand.Read(verifyToken[:]) + if err != nil { + return nil, err + } + + er := pk.Packet{ID: 0x01} + er.Data = append(er.Data, pk.String("").Encode()...) + er.Data = append(er.Data, pk.VarInt(len(publicKey)).Encode()...) + er.Data = append(er.Data, publicKey...) + er.Data = append(er.Data, pk.VarInt(verifyTokenLen).Encode()...) + er.Data = append(er.Data, verifyToken[:]...) + + err = c.WritePacket(er) + return verifyToken[:], err +} + +func (c *Client) encryptionResponse() ([]byte, []byte, error) { + p, err := c.ReadPacket() + if err != nil { + return nil, nil, err + } + if p.ID != 0x01 { + return nil, nil, fmt.Errorf("0x%02X is not Encryption Response", p.ID) + } + + var ( + SharedSecret ByteArray + VerifyToken ByteArray + ) + if err := p.Scan(&SharedSecret, &VerifyToken); err != nil { + return nil, nil, err + } + return SharedSecret, VerifyToken, nil +} + +//ByteArray is []byte with perfix VarInt as length +type ByteArray []byte + +// Decode a ByteArray +func (b *ByteArray) Decode(r pk.DecodeReader) error { + var Len pk.VarInt + if err := Len.Decode(r); err != nil { + return err + } + *b = make([]byte, Len) + _, err := r.Read(*b) + return err +} + +type authResp struct { + ID, Name string + Properties [1]struct { + Name, Value, Signature string + } +} + +func (c *Client) authentication(hash string) (*authResp, error) { + resp, err := http.Get(fmt.Sprintf("https://sessionserver.mojang.com/session/minecraft/hasJoined?username=%s&serverId=%s", + c.Name, hash)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var Resp authResp + + err = json.Unmarshal(body, &Resp) + if err != nil { + return nil, err + } + + return &Resp, nil +} + +// authDigest computes a special SHA-1 digest required for Minecraft web +// authentication on Premium servers (online-mode=true). +// Source: http://wiki.vg/Protocol_Encryption#Server +// +// Also many, many thanks to SirCmpwn and his wonderful gist (C#): +// https://gist.github.com/SirCmpwn/404223052379e82f91e6 +func authDigest(serverID string, sharedSecret, publicKey []byte) string { + h := sha1.New() + h.Write([]byte(serverID)) + h.Write(sharedSecret) + h.Write(publicKey) + hash := h.Sum(nil) + + // Check for negative hashes + negative := (hash[0] & 0x80) == 0x80 + if negative { + hash = twosComplement(hash) + } + + // Trim away zeroes + res := strings.TrimLeft(fmt.Sprintf("%x", hash), "0") + if negative { + res = "-" + res + } + + return res +} + +// little endian +func twosComplement(p []byte) []byte { + carry := true + for i := len(p) - 1; i >= 0; i-- { + p[i] = byte(^p[i]) + if carry { + carry = p[i] == 0xff + p[i]++ + } + } + return p +} diff --git a/go.mod b/go.mod index 9817515..cbbde25 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module github.com/Tnze/go-mc go 1.12 + +require github.com/satori/go.uuid v1.2.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a9a7620 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= diff --git a/net/conn.go b/net/conn.go index c3b9cd4..5b47eb3 100644 --- a/net/conn.go +++ b/net/conn.go @@ -10,8 +10,31 @@ import ( pk "github.com/Tnze/go-mc/net/packet" ) +// A Listener is a minecraft Listener +type Listener struct{ net.Listener } + +//ListenMC listen as TCP but Accept a mc Conn +func ListenMC(addr string) (*Listener, error) { + l, err := net.Listen("tcp", addr) + if err != nil { + return nil, err + } + return &Listener{l}, nil +} + +//Accept a miencraft Conn +func (l Listener) Accept() (Conn, error) { + conn, err := l.Listener.Accept() + return Conn{ + Socket: conn, + ByteReader: bufio.NewReader(conn), + Writer: conn, + }, err +} + +//Conn is a minecraft Connection type Conn struct { - socket net.Conn + Socket net.Conn io.ByteReader io.Writer @@ -19,19 +42,18 @@ type Conn struct { } // DialMC create a Minecraft connection -func DialMC(addr string) (conn *Conn, err error) { - conn = new(Conn) - conn.socket, err = net.Dial("tcp", addr) - if err != nil { - return - } - - conn.ByteReader = bufio.NewReader(conn.socket) - conn.Writer = conn.socket - - return +func DialMC(addr string) (*Conn, error) { + conn, err := net.Dial("tcp", addr) + return &Conn{ + Socket: conn, + ByteReader: bufio.NewReader(conn), + Writer: conn, + }, err } +//Close close the connection +func (c *Conn) Close() error { return c.Socket.Close() } + // ReadPacket read a Packet from Conn. func (c *Conn) ReadPacket() (pk.Packet, error) { p, err := pk.RecvPacket(c.ByteReader, c.threshold > 0) @@ -52,11 +74,11 @@ func (c *Conn) SetCipher(encoStream, decoStream cipher.Stream) { //加密连接 c.ByteReader = bufio.NewReader(cipher.StreamReader{ //Set reciver for AES S: decoStream, - R: c.socket, + R: c.Socket, }) c.Writer = cipher.StreamWriter{ S: encoStream, - W: c.socket, + W: c.Socket, } }