complete yggdrasil package

This commit is contained in:
Tnze
2019-08-09 17:42:18 +08:00
parent cd852a59b5
commit 3aa48ab1be
8 changed files with 226 additions and 53 deletions

View File

@ -3,5 +3,20 @@
<option name="myName" value="Project Default" /> <option name="myName" value="Project Default" />
<inspection_tool class="GoCommentStart" enabled="true" level="WEAK WARNING" enabled_by_default="true" /> <inspection_tool class="GoCommentStart" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="GoExportedElementShouldHaveComment" enabled="true" level="WEAK WARNING" enabled_by_default="true" /> <inspection_tool class="GoExportedElementShouldHaveComment" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="GoUnhandledErrorResult" enabled="true" level="WARNING" enabled_by_default="true">
<methods>
<method importPath="hash" receiver="Hash" name="Write" />
<method importPath="strings" receiver="*Builder" name="Write" />
<method importPath="strings" receiver="*Builder" name="WriteByte" />
<method importPath="bytes" receiver="*Buffer" name="WriteRune" />
<method importPath="bytes" receiver="*Buffer" name="Write" />
<method importPath="bytes" receiver="*Buffer" name="WriteString" />
<method importPath="strings" receiver="*Builder" name="WriteString" />
<method importPath="bytes" receiver="*Buffer" name="WriteByte" />
<method importPath="strings" receiver="*Builder" name="WriteRune" />
<method importPath="math/rand" receiver="*Rand" name="Read" />
<method importPath="io" receiver="ReadCloser" name="Close" />
</methods>
</inspection_tool>
</profile> </profile>
</component> </component>

View File

@ -1,7 +1,6 @@
package yggdrasil package yggdrasil
import ( import (
"fmt"
"github.com/google/uuid" "github.com/google/uuid"
) )
@ -16,6 +15,16 @@ type agent struct {
Version int `json:"version"` Version int `json:"version"`
} }
type proof struct {
UserName string `json:"username"`
Password string `json:"password"`
}
type tokens struct {
AccessToken string `json:"accessToken"`
ClientToken string `json:"clientToken"`
}
var defaultAgent = agent{ var defaultAgent = agent{
Name: "Minecraft", Name: "Minecraft",
Version: 1, Version: 1,
@ -24,31 +33,17 @@ var defaultAgent = agent{
// authPayload is a yggdrasil request struct // authPayload is a yggdrasil request struct
type authPayload struct { type authPayload struct {
Agent agent `json:"agent"` Agent agent `json:"agent"`
UserName string `json:"username"` proof
Password string `json:"password"`
ClientToken string `json:"clientToken"` ClientToken string `json:"clientToken"`
RequestUser bool `json:"requestUser"` RequestUser bool `json:"requestUser"`
} }
// authResp is the response from Mojang's auth server // authResp is the response from Mojang's auth server
type authResp struct { type authResp struct {
Error string `json:"error"` tokens
ErrorMessage string `json:"errorMessage"` AvailableProfiles []Profile `json:"availableProfiles"` // only present if the agent field was received
Cause string `json:"cause"`
AccessToken string `json:"accessToken"` SelectedProfile Profile `json:"selectedProfile"` // only present if the agent field was received
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
SelectedProfile struct { // only present if the agent field was received
ID string `json:"id"`
Name string `json:"name"`
Legacy bool `json:"legacy"`
} `json:"selectedProfile"`
User struct { // only present if requestUser was true in the request authPayload User struct { // only present if requestUser was true in the request authPayload
ID string `json:"id"` // hexadecimal ID string `json:"id"` // hexadecimal
Properties []struct { Properties []struct {
@ -56,6 +51,14 @@ type authResp struct {
Value string `json:"value"` Value string `json:"value"`
} }
} `json:"user"` } `json:"user"`
*Error
}
type Profile struct {
ID string `json:"id"`
Name string `json:"name"`
//Legacy bool `json:"legacy"` // we don't care
} }
// Authenticate authenticates a user using their password. // Authenticate authenticates a user using their password.
@ -63,8 +66,10 @@ func Authenticate(user, password string) (*Access, error) {
// Payload // Payload
pl := authPayload{ pl := authPayload{
Agent: defaultAgent, Agent: defaultAgent,
proof: proof{
UserName: user, UserName: user,
Password: password, Password: password,
},
ClientToken: uuid.New().String(), ClientToken: uuid.New().String(),
RequestUser: true, RequestUser: true,
} }
@ -77,10 +82,8 @@ func Authenticate(user, password string) (*Access, error) {
return nil, err return nil, err
} }
if ar.Error != "" { if ar.Error != nil {
err = fmt.Errorf("auth fail: %s: %s, %s}", return nil, *ar.Error
ar.Error, ar.ErrorMessage, ar.Cause)
return nil, err
} }
return &Access{ar: ar, ct: pl.ClientToken}, nil return &Access{ar: ar, ct: pl.ClientToken}, nil
@ -93,3 +96,7 @@ func (a *Access) SelectedProfile() (ID, Name string) {
func (a *Access) AccessToken() string { func (a *Access) AccessToken() string {
return a.ar.AccessToken return a.ar.AccessToken
} }
func (a *Access) AvailableProfiles() []Profile {
return a.ar.AvailableProfiles
}

View File

@ -1,15 +0,0 @@
package yggdrasil
import (
"fmt"
)
func ExampleAuthenticate() {
resp, err := Authenticate("", "")
if err != nil {
panic(err)
}
fmt.Println(resp.SelectedProfile())
fmt.Println(resp.AccessToken())
}

39
yggdrasil/refresh.go Normal file
View File

@ -0,0 +1,39 @@
package yggdrasil
import "fmt"
type refreshPayload struct {
tokens
SelectedProfile *Profile `json:"selectedProfile,omitempty"`
RequestUser bool `json:"requestUser"`
}
// Refresh refreshes a valid accessToken.
//
// It can be used to keep a user logged in between
// gaming sessions and is preferred over storing
// the user's password in a file
func (a *Access) Refresh(profile *Profile) error {
pl := refreshPayload{
tokens: a.ar.tokens,
SelectedProfile: profile, //used to change profile, don't use now
RequestUser: true,
}
resp := struct {
*authResp
*Error
}{authResp: &a.ar}
err := post("/refresh", pl, &resp)
if err != nil {
return fmt.Errorf("post fail: %v", err)
}
if resp.Error != nil {
return resp.Error
}
return nil
}

30
yggdrasil/signout.go Normal file
View File

@ -0,0 +1,30 @@
package yggdrasil
import (
"encoding/json"
"fmt"
)
// SignOut invalidates accessTokens using an account's username and password.
func SignOut(user, password string) error {
pl := proof{
UserName: user,
Password: password,
}
resp, err := rowPost("/signout", pl)
if err != nil {
return fmt.Errorf("request fail: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != 204 {
var err Error
if err := json.NewDecoder(resp.Body).Decode(&err); err != nil {
return fmt.Errorf("unmarshal error fail: %v", err)
}
return fmt.Errorf("invalidate error: %v", err)
}
return nil
}

View File

@ -1,16 +1,13 @@
package yggdrasil package yggdrasil
import "fmt" import (
"fmt"
"io/ioutil"
)
// Validate checks if an accessToken is usable for authentication with a Minecraft server. // Validate checks if an accessToken is usable for authentication with a Minecraft server.
func (a *Access) Validate() (bool, error) { func (a *Access) Validate() (bool, error) {
pl := struct { pl := a.ar.tokens
AccessToken string `json:"accessToken"`
ClientToken string `json:"clientToken"`
}{
AccessToken: a.ar.AccessToken,
ClientToken: a.ar.ClientToken,
}
resp, err := rowPost("/validate", pl) resp, err := rowPost("/validate", pl)
if err != nil { if err != nil {
@ -19,3 +16,21 @@ func (a *Access) Validate() (bool, error) {
return resp.StatusCode == 204, resp.Body.Close() return resp.StatusCode == 204, resp.Body.Close()
} }
// Invalidate invalidates accessTokens using a client/access token pair.
func (a *Access) Invalidate() error {
pl := a.ar.tokens
resp, err := rowPost("/invalidate", pl)
if err != nil {
return fmt.Errorf("request fail: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != 204 {
content, _ := ioutil.ReadAll(resp.Body)
return fmt.Errorf("invalidate error: %v: %s", resp.Status, content)
}
return nil
}

View File

@ -14,6 +14,16 @@ import (
"net/http" "net/http"
) )
type Error struct {
Err string `json:"error"`
ErrMsg string `json:"errorMessage"`
Cause string `json:"cause"`
}
func (e Error) Error() string {
return e.Err + ": " + e.ErrMsg + ", " + e.Cause
}
var AuthURL = "https://authserver.mojang.com" var AuthURL = "https://authserver.mojang.com"
var client http.Client var client http.Client

View File

@ -0,0 +1,72 @@
package yggdrasil
import (
"fmt"
"os"
)
func ExampleAuthenticate() {
resp, err := Authenticate("", "")
if err != nil {
panic(err)
}
fmt.Println(resp.SelectedProfile())
fmt.Println(resp.AccessToken())
}
func Example(){
var user, password string// set your proof
// Sign in
resp, err := Authenticate(user, password)
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())
// Refresh access token
if err := resp.Refresh(nil); 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())
// Check access token
ok, err := resp.Validate()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
fmt.Println("at status: ", ok)
// Invalidate access token
err=resp.Invalidate()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// Check access token
ok, err = resp.Validate()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
fmt.Println("at status: ", ok)
// Sign out
err = SignOut(user, password)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}