diff --git a/cmd/luncher/luncher.go b/cmd/luncher/luncher.go new file mode 100644 index 0000000..9411bb8 --- /dev/null +++ b/cmd/luncher/luncher.go @@ -0,0 +1,25 @@ +package main + +import ( + "flag" + "fmt" + "github.com/Tnze/go-mc/yggdrasil" + "os" +) + +var user = flag.String("user", "", "Can be an email address or player name for unmigrated accounts") +var pswd = flag.String("password", "", "Your password") + +func main() { + flag.Parse() + + resp, err := yggdrasil.Authenticate(*user, *pswd) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + id, name := resp.SelectedProfile() + fmt.Println("user:", name) + fmt.Println("uuid:", id) + fmt.Println("astk:", resp.AccessToken()) +} diff --git a/cmd/substitute_server/substitute.go b/cmd/substitute_server/substitute.go index 164284f..ee2eeb9 100644 --- a/cmd/substitute_server/substitute.go +++ b/cmd/substitute_server/substitute.go @@ -374,7 +374,7 @@ func (c *Client) encryptionResponse() ([]byte, []byte, error) { return nil, nil, err } if p.ID != 0x01 { - return nil, nil, fmt.Errorf("0x%02X is not Encryption AuthResp", p.ID) + return nil, nil, fmt.Errorf("0x%02X is not Encryption authResp", p.ID) } var ( diff --git a/yggdrasil/authenticate.go b/yggdrasil/authenticate.go index ac8918f..8d76d27 100644 --- a/yggdrasil/authenticate.go +++ b/yggdrasil/authenticate.go @@ -1,83 +1,32 @@ package yggdrasil import ( - "bytes" - "encoding/json" "fmt" - "io/ioutil" - "net/http" + "github.com/google/uuid" ) -// Agent is a struct of auth -type Agent struct { +type Access struct { + ar authResp + ct string +} + +// agent is a struct of auth +type agent struct { Name string `json:"name"` Version int `json:"version"` } -// AuthPayload is a yggdrasil request struct -type AuthPayload struct { - Agent Agent `json:"agent"` +// authPayload is a yggdrasil request struct +type authPayload struct { + Agent agent `json:"agent"` UserName string `json:"username"` Password string `json:"password"` ClientToken string `json:"clientToken"` RequestUser bool `json:"requestUser"` } -// Authenticate authenticates a user using their password. -func Authenticate(user, password string) (respData AuthResp, err error) { - j, err := json.Marshal(AuthPayload{ - Agent: Agent{ - Name: "Minecraft", - Version: 1, - }, - UserName: user, - Password: password, - ClientToken: "go-mc", - RequestUser: true, - }) - - if err != nil { - err = fmt.Errorf("encoding json fail: %v", err) - return - } - - //Post - client := http.Client{} - PostRequest, err := http.NewRequest(http.MethodPost, "https://authserver.mojang.com/yggdrasil", - bytes.NewReader(j)) - if err != nil { - err = fmt.Errorf("make request error: %v", err) - return - } - PostRequest.Header.Set("User-Agent", "go-mc") - PostRequest.Header.Set("Connection", "keep-alive") - PostRequest.Header.Set("Content-Type", "application/json") - resp, err := client.Do(PostRequest) - if err != nil { - err = fmt.Errorf("post yggdrasil fail: %v", err) - return - } - defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - err = fmt.Errorf("read yggdrasil resp fail: %v", err) - return - } - err = json.Unmarshal(body, &respData) - if err != nil { - err = fmt.Errorf("unmarshal json data fail: %v", err) - return - } - if respData.Error != "" { - err = fmt.Errorf("yggdrasil fail: {error: %q, errorMessage: %q, cause: %q}", - respData.Error, respData.ErrorMessage, respData.Cause) - return - } - return -} - -// AuthResp is the response from Mojang's auth server -type AuthResp struct { +// authResp is the response from Mojang's auth server +type authResp struct { Error string `json:"error"` ErrorMessage string `json:"errorMessage"` Cause string `json:"cause"` @@ -85,21 +34,60 @@ type AuthResp struct { AccessToken string `json:"accessToken"` ClientToken string `json:"clientToken"` // identical to the one received AvailableProfiles []struct { - ID string `json:"ID"` // hexadecimal - Name string `json:"name"` - Legacy bool `json:"legacy"` // In practice, this field only appears in the response if true. Default to false. - } `json:"availableProfiles"` // only present if the Agent field was received + ID uuid.UUID `json:"ID"` // hexadecimal + Name string `json:"name"` + Legacy bool `json:"legacy"` // In practice, this field only appears in the response if true. Default to false. + } `json:"availableProfiles"` // only present if the agent field was received - SelectedProfile struct { // only present if the Agent field was received - ID string `json:"id"` - Name string `json:"name"` - Legacy bool `json:"legacy"` + SelectedProfile struct { // only present if the agent field was received + ID uuid.UUID `json:"id"` + Name string `json:"name"` + Legacy bool `json:"legacy"` } `json:"selectedProfile"` - User struct { // only present if requestUser was true in the request AuthPayload - ID string `json:"id"` // hexadecimal + User struct { // only present if requestUser was true in the request authPayload + ID uuid.UUID `json:"id"` // hexadecimal Properties []struct { Name string `json:"name"` Value string `json:"value"` } } `json:"user"` } + +// Authenticate authenticates a user using their password. +func Authenticate(user, password string) (*Access, error) { + // Payload + pl := authPayload{ + Agent: agent{ + Name: "Minecraft", + Version: 1, + }, + UserName: user, + Password: password, + ClientToken: uuid.New().String(), + RequestUser: true, + } + // Resp + var ar authResp + + // Request + err := post("/authenticate", pl, &ar) + if err != nil { + return nil, err + } + + if ar.Error != "" { + err = fmt.Errorf("auth fail: %s: %s, %s}", + ar.Error, ar.ErrorMessage, ar.Cause) + return nil, err + } + + return &Access{ar: ar, ct: pl.ClientToken}, nil +} + +func (a *Access) SelectedProfile() (ID uuid.UUID, Name string) { + return a.ar.SelectedProfile.ID, a.ar.SelectedProfile.Name +} + +func (a *Access) AccessToken()string{ + return a.ar.AccessToken +} \ No newline at end of file diff --git a/yggdrasil/authenticate_test.go b/yggdrasil/authenticate_test.go index 396555b..c2e7445 100644 --- a/yggdrasil/authenticate_test.go +++ b/yggdrasil/authenticate_test.go @@ -7,12 +7,12 @@ import ( ) func TestEncodingPayload(t *testing.T) { - j, err := json.Marshal(AuthPayload{ - Agent: Agent{ + j, err := json.Marshal(authPayload{ + Agent: agent{ Name: "Minecraft", Version: 1, }, - UserName: "mojang account name", + UserName: "mojang account email or name", Password: "mojang account password", ClientToken: "client identifier", RequestUser: true, diff --git a/yggdrasil/validate.go b/yggdrasil/validate.go new file mode 100644 index 0000000..2ee79f4 --- /dev/null +++ b/yggdrasil/validate.go @@ -0,0 +1,21 @@ +package yggdrasil + +import "fmt" + +// Validate checks if an accessToken is usable for authentication with a Minecraft server. +func (a *Access) Validate() (bool, error) { + pl := struct { + AccessToken string `json:"accessToken"` + ClientToken string `json:"clientToken"` + }{ + AccessToken: a.ar.AccessToken, + ClientToken: a.ar.ClientToken, + } + + resp, err := rowPost("/validate", pl) + if err != nil { + return false, fmt.Errorf("request fail: %v", err) + } + + return resp.StatusCode == 204, resp.Body.Close() +} diff --git a/yggdrasil/yggdrasil.go b/yggdrasil/yggdrasil.go index 18887db..19320b4 100644 --- a/yggdrasil/yggdrasil.go +++ b/yggdrasil/yggdrasil.go @@ -7,4 +7,50 @@ // but credentials should never be collected from users. ----- https://wiki.vg package yggdrasil -var Server = "" +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" +) + +var AuthURL = "https://authserver.mojang.com" + +var client http.Client + +func post(endpoint string, payload interface{}, resp interface{}) error { + rowResp,err:=rowPost(endpoint,payload) + if err != nil { + return fmt.Errorf("request fail: %v",err) + } + defer rowResp.Body.Close() + + err = json.NewDecoder(rowResp.Body).Decode(resp) + if err != nil { + return fmt.Errorf("parse resp fail: %v", err) + } + + return nil +} + +func rowPost(endpoint string, payload interface{}) (*http.Response, error) { + data, err := json.Marshal(payload) + if err != nil { + return nil, fmt.Errorf("marshal payload fail: %v", err) + } + + PostRequest, err := http.NewRequest( + http.MethodPost, + AuthURL+endpoint, + bytes.NewReader(data)) + if err != nil { + return nil, fmt.Errorf("make request error: %v", err) + } + + PostRequest.Header.Set("User-agent", "go-mc") + PostRequest.Header.Set("Connection", "keep-alive") + PostRequest.Header.Set("Content-Type", "application/json") + + // Do + return client.Do(PostRequest) +}