From 27f4047cf79933af9be2ec093e188deffe4dd3b3 Mon Sep 17 00:00:00 2001 From: TaiAurori <31465218+TaiAurori@users.noreply.github.com> Date: Thu, 23 Jun 2022 22:22:17 -0400 Subject: [PATCH] skin system (kinda), session system (kinda), config framework, much cleanup to be done tomorrow --- .gitignore | 3 +- endpoints.go => auth.go | 74 +++++++++++++++++++++- config.go | 28 ++++++++ config.yaml | 3 + db.go | 29 --------- default.png | Bin 0 -> 677 bytes go.mod | 1 + go.sum | 3 + main.go | 12 +++- player.go | 131 ++++++++++++++++++++++++++++++++++++++ session.go | 137 ++++++++++++++++++++++++++++++++++++++++ sha.go | 39 ++++++++++++ types.go | 39 ++++++++++++ util.go | 15 ++--- 14 files changed, 470 insertions(+), 44 deletions(-) rename endpoints.go => auth.go (72%) create mode 100644 config.go create mode 100644 config.yaml create mode 100644 default.png create mode 100644 player.go create mode 100644 session.go create mode 100644 sha.go diff --git a/.gitignore b/.gitignore index 48702fc..3728ecb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ db.sqlite secrets.go -tripwire \ No newline at end of file +tripwire +skins/ diff --git a/endpoints.go b/auth.go similarity index 72% rename from endpoints.go rename to auth.go index 124c698..7a973b6 100644 --- a/endpoints.go +++ b/auth.go @@ -1,7 +1,10 @@ package main import ( + "errors" + "io/ioutil" "net/http" + "os" "github.com/gorilla/mux" ) @@ -152,7 +155,74 @@ func invalidateEndpoint(w http.ResponseWriter, r *http.Request) { sendEmpty(w) } -func registerEndpoints(r *mux.Router) { +func getSkinEndpoint(w http.ResponseWriter, r *http.Request) { + uuid := mux.Vars(r)["uuid"] + skin, err := getPlayerSkinByUUID(uuid) + if err != nil { + if !errors.Is(err, &NotFoundError{}) { + handleError(w, err) + return + } + + skin, err = os.ReadFile("default.png") + if err != nil { + handleError(w, err) + return + } + } + w.Header().Set("Content-Type", "image/png") + w.Write(skin) +} + +func setSkinEndpoint(w http.ResponseWriter, r *http.Request) { + uuid := mux.Vars(r)["uuid"] + + r.ParseMultipartForm(int64(config.MaxSkinSize)) + + pass := r.FormValue("password") + + correct, err := checkPlayerPassByUUID(uuid, pass) + if err != nil { + handleError(w, err) + return + } + + if !correct { + sendError(w, YggError{Code: 400, Error: "Bad Request", ErrorMessage: "Invalid credentials."}) + return + } + + file, _, err := r.FormFile("skinfile") + if err != nil { + handleError(w, err) + return + } + + skin, err := ioutil.ReadAll(file) + if err != nil { + handleError(w, err) + return + } + + if len(skin) < 4 || + skin[0] != 0x89 || + skin[1] != 0x50 || + skin[2] != 0x4e || + skin[3] != 0x47 { + sendError(w, YggError{Code: 400, Error: "Bad Request", ErrorMessage: "The request data is malformed."}) + return + } + + err = setPlayerSkinByUUID(uuid, skin) + if err != nil { + handleError(w, err) + return + } + + sendEmpty(w) +} + +func registerAuthEndpoints(r *mux.Router) { r.HandleFunc("/", rootEndpoint) r.HandleFunc("/authenticate", authenticateEndpoint).Methods("POST") r.HandleFunc("/refresh", refreshTokenEndpoint).Methods("POST") @@ -160,4 +230,6 @@ func registerEndpoints(r *mux.Router) { r.HandleFunc("/invalidate", invalidateEndpoint).Methods("POST") r.HandleFunc("/validate", validateEndpoint).Methods("POST") r.HandleFunc("/admin/addUser", addUserEndpoint).Methods("POST") + r.HandleFunc("/getSkin/{uuid}", getSkinEndpoint).Methods("GET") + r.HandleFunc("/setSkin/{uuid}", setSkinEndpoint).Methods("POST") } diff --git a/config.go b/config.go new file mode 100644 index 0000000..6007867 --- /dev/null +++ b/config.go @@ -0,0 +1,28 @@ +package main + +import ( + "os" + + "gopkg.in/yaml.v3" +) + +type Configuration struct { + BaseUrl string `yaml:"baseUrl"` + DebugMode bool `yaml:"debugMode"` + MaxSkinSize int `yaml:"maxSkinSize"` +} + +var config Configuration + +func loadConfig() error { + configFile, err := os.ReadFile("config.yaml") + if err != nil { + return err + } + + err = yaml.Unmarshal(configFile, &config) + if err != nil { + return err + } + return nil +} diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..2b267a3 --- /dev/null +++ b/config.yaml @@ -0,0 +1,3 @@ +baseUrl: "http://localhost:1000" +debugMode: true +maxSkinSize: 8096 \ No newline at end of file diff --git a/db.go b/db.go index 5274800..2f5afab 100644 --- a/db.go +++ b/db.go @@ -166,35 +166,6 @@ func createUser(username string, adminToken string) (string, error) { } } -func playerExistsByUsername(username string) (bool, error) { - sqlStatement := ` - SELECT username FROM users WHERE username = ?; - ` - var x string - err := DB.QueryRow(sqlStatement, username).Scan(&x) - if err != nil { - if err == sql.ErrNoRows { - return false, nil - } - return false, err - } - return true, nil -} - -func getPlayerUUID(username string) (string, error) { - sqlStatement := ` - SELECT uuid FROM users WHERE username = ?; - ` - row := DB.QueryRow(sqlStatement, username) - - var uuid string - err := row.Scan(&uuid) - if err != nil { - return "", err - } - return uuid, nil -} - func refreshTokens(refresh RefreshPayload) (RefreshPayload, error) { sqlStatement := ` SELECT id FROM users WHERE auth_token = ? and client_token = ?; diff --git a/default.png b/default.png new file mode 100644 index 0000000000000000000000000000000000000000..d4b16fcb21a22ac5bd5397e44140a554d39f6a54 GIT binary patch literal 677 zcmV;W0$TlvP)@Q(WBUHa~8`tO1I@PYdAo%-^f`tzOp?SlL8cKh&w`|yJM@s|7Y zmizLh`}3vz;dcD+cKz&j{qI=)@L~P%Y5ubX{?85m>~{X_cmD8L{_th~@N53@YtHAC zumAu60d!JMQvg8b*k%9#0qRLaK~z}7?UsRZn=lN9vvqOfSVk!}Zku(1>ir+G`i)7` zTpLKZ%WiA_99S{vlQ06YSQM>RHtSJHC9*hI)*b@rasU z>p++dIKG5W0gQF${nDjYq}>)hsV(`i$nJwa+(w-7j!*~LuCunUYD+>E=@ftu0Uq&! zdeeFxQ$AL!n8yGH-e%)H;)UqQQH-#4Bhv8{fcF6J6HbRaeh+GMjlvuNW(1&{1}3NB zgClZk*Zm8sJ_`_(pnwPf)HFns0l0*a!&Ag1WB@fI8Q6CK7LG!Qb%>O0_){~s2F~0S zK)!XP-XYptVXXdjlTHJiGtUKpLK4n}C^8gMGZfM%0DT53ts8?8JCw4BMeHXvP|L8} zF9A2*^snmY^+Pt_ah>(WbdQ(IXjlGE0ezFs-8y&dFPrKqK;Q0pCP25%r2ta^-fzss zfSiT*=CGZd4^VR^;O}K%X94bvn~_14(z9`H1_0j){g>(*<{01&Q4MuKHGQX{00000 LNkvXXu0mjf4zWq- literal 0 HcmV?d00001 diff --git a/go.mod b/go.mod index 18f5e9a..aa5f3c1 100644 --- a/go.mod +++ b/go.mod @@ -6,4 +6,5 @@ require ( github.com/google/uuid v1.3.0 github.com/gorilla/mux v1.8.0 github.com/mattn/go-sqlite3 v1.14.13 + gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index cb65676..60a3404 100644 --- a/go.sum +++ b/go.sum @@ -4,3 +4,6 @@ github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/mattn/go-sqlite3 v1.14.13 h1:1tj15ngiFfcZzii7yd82foL+ks+ouQcj8j/TPq3fk1I= github.com/mattn/go-sqlite3 v1.14.13/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index dbc625f..10358f8 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main import ( "log" "net/http" + "os" "github.com/gorilla/mux" ) @@ -14,16 +15,21 @@ import ( func handleRequests() { r := mux.NewRouter().StrictSlash(true) - registerEndpoints(r) + err := loadConfig() + if err != nil { + log.Fatalln("Failed to load config.yaml:", err) + } + registerAuthEndpoints(r) + registerSessionEndpoints(r) + log.Println("Tripwire started.") log.Fatal(http.ListenAndServe(":10000", r)) } func main() { log.Println("Tripwire initializing...") + os.Mkdir("skins", 0755) initDB() - log.Println("Tripwire started.") - handleRequests() defer DB.Close() diff --git a/player.go b/player.go new file mode 100644 index 0000000..deae929 --- /dev/null +++ b/player.go @@ -0,0 +1,131 @@ +package main + +// todo: make a consistent "player" type and have these functions return it + +import ( + "database/sql" + "errors" + "io/fs" + "os" +) + +func playerExistsByUsername(username string) (bool, error) { + sqlStatement := ` + SELECT username FROM users WHERE username = ?; + ` + var x string + err := DB.QueryRow(sqlStatement, username).Scan(&x) + if err != nil { + if err == sql.ErrNoRows { + return false, nil + } + return false, err + } + return true, nil +} + +func playerExistsByUUID(uuid string) (bool, error) { + sqlStatement := ` + SELECT uuid FROM users WHERE uuid = ?; + ` + var x string + err := DB.QueryRow(sqlStatement, uuid).Scan(&x) + if err != nil { + if err == sql.ErrNoRows { + return false, nil + } + return false, err + } + return true, nil +} + +func getPlayerUUID(username string) (string, error) { + sqlStatement := ` + SELECT uuid FROM users WHERE username = ?; + ` + row := DB.QueryRow(sqlStatement, username) + + var uuid string + err := row.Scan(&uuid) + if err != nil { + return "", err + } + return uuid, nil +} + +func getPlayerUsername(uuid string) (string, error) { + sqlStatement := ` + SELECT username FROM users WHERE uuid = ?; + ` + row := DB.QueryRow(sqlStatement, uuid) + + var username string + err := row.Scan(&username) + if err != nil { + return "", err + } + return username, nil +} + +func getPlayerSkinByUUID(uuid string) ([]byte, error) { + skin, err := os.ReadFile("skins/" + uuid + ".png") + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + return []byte{}, &NotFoundError{} + } + return []byte{}, err + } + + return skin, nil +} + +func setPlayerSkinByUUID(uuid string, skin []byte) error { + if len(skin) > config.MaxSkinSize { + return &TooLargeError{} + } + exists, err := playerExistsByUUID(uuid) + if err != nil { + return err + } + if !exists { + return &NotFoundError{} + } + + err = os.WriteFile("skins/"+uuid+".png", skin, 0644) + if err != nil { + return err + } + + return nil +} + +func findPlayerUUIDByAuthToken(token string) (string, error) { + sqlStatement := ` + SELECT uuid FROM users WHERE auth_token = ?; + ` + row := DB.QueryRow(sqlStatement, token) + + var uuid string + err := row.Scan(&uuid) + if err != nil { + return "", err + } + return uuid, nil +} + +func checkPlayerPassByUUID(uuid string, password string) (bool, error) { + sqlStatement := ` + SELECT password FROM users WHERE auth_token = ?; + ` + row := DB.QueryRow(sqlStatement, token) + + var pass string + err := row.Scan(&pass) + if err != nil { + return false, err + } + if pass != password { + return false, nil + } + return true, nil +} diff --git a/session.go b/session.go new file mode 100644 index 0000000..6a4f510 --- /dev/null +++ b/session.go @@ -0,0 +1,137 @@ +package main + +import ( + "encoding/base64" + "encoding/json" + "net/http" + "strings" + + "github.com/gorilla/mux" +) + +func profileEndpoint(w http.ResponseWriter, r *http.Request) { + uuid := mux.Vars(r)["uuid"] + + exists, err := playerExistsByUUID(uuid) + if err != nil { + handleError(w, err) + return + } + if !exists { + sendError(w, YggError{ + Code: 400, + Error: "Bad Request", + ErrorMessage: "The user does not exist.", + }) + return + } + + username, err := getPlayerUsername(uuid) + if err != nil { + handleError(w, err) + return + } + + response, err := generateProfileResponse(uuid, username) + if err != nil { + handleError(w, err) + return + } + + sendJSON(w, response) +} + +func hasJoinedEndpoint(w http.ResponseWriter, r *http.Request) { + params := r.URL.Query() + if !params.Has("username") || !params.Has("serverId") { + sendJSON(w, &Empty{}) + return + } + + uuid, err := getPlayerUUID(params.Get("username")) + if err != nil { + handleError(w, err) + return + } + + response, err := generateProfileResponse(params.Get("username"), uuid) + if err != nil { + handleError(w, err) + return + } + + sendJSON(w, response) +} + +func joinEndpoint(w http.ResponseWriter, r *http.Request) { + params := r.URL.Query() + if !params.Has("accessToken") || !params.Has("selectedProfile") || !params.Has("serverId") { + sendError(w, YggError{ + Code: 400, + Error: "IllegalArgumentException", + ErrorMessage: "A required field is not present.", + }) + return + } + uuid, err := findPlayerUUIDByAuthToken(params.Get("accessToken")) + if err != nil { + handleError(w, err) + return + } + if params.Get("selectedProfile") != shrinkUUID(uuid) { + sendError(w, YggError{ + Code: 400, + Error: "Bad Request", + ErrorMessage: "The request data is malformed.", + }) + } + sendEmpty(w) +} + +func registerSessionEndpoints(r *mux.Router) { + r.HandleFunc("/session/minecraft/profile/{uuid}", profileEndpoint).Methods("GET") + r.HandleFunc("/session/minecraft/hasJoined", hasJoinedEndpoint).Methods("GET") +} + +func generateProfileResponse(uuid string, username string) (ProfileResponse, error) { + // todo: make this more visually appealing if possible + clearUUID := strings.Join(strings.Split(uuid, "-"), "") + skin := SkinTexture{} + skin.Url = config.BaseUrl + "/getSkin/" + uuid + skin.Metadata = SkinMetadata{} + skin.Metadata.Model = "default" + + textures := ProfileTextureMetadata{} + textures.Id = clearUUID + textures.Name = username + textures.Textures = ProfileTextures{} + textures.Textures.Skin = skin + + marshalledTextures, err := json.Marshal(textures) + if err != nil { + return ProfileResponse{}, err + } + encodedTextures := base64.StdEncoding.EncodeToString(marshalledTextures) + + response := ProfileResponse{} + response.Id = clearUUID + response.Name = username + response.Properties = []Property{ + { + Name: "textures", + Value: encodedTextures, + }, + } + return response, nil +} + +func shrinkUUID(uuid string) string { + return strings.Join(strings.Split(uuid, "-"), "") +} + +func growUUID(uuid string) string { + if len(uuid) == 32 { + return uuid[0:7] + "-" + uuid[8:11] + "-" + uuid[12:15] + "-" + uuid[16:19] + "-" + uuid[20:31] + } + return "" +} diff --git a/sha.go b/sha.go new file mode 100644 index 0000000..03c2b91 --- /dev/null +++ b/sha.go @@ -0,0 +1,39 @@ +// Ripped from https://gist.github.com/toqueteos/5372776 +package main + +import ( + "crypto/sha1" + "encoding/hex" + "io" + "strings" +) + +func authDigest(s string) string { + h := sha1.New() + io.WriteString(h, s) + hash := h.Sum(nil) + + negative := (hash[0] & 0x80) == 0x80 + if negative { + hash = twosComplement(hash) + } + + res := strings.TrimLeft(hex.EncodeToString(hash), "0") + if negative { + res = "-" + res + } + + return res +} + +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/types.go b/types.go index 3320731..b72f9ba 100644 --- a/types.go +++ b/types.go @@ -52,6 +52,39 @@ type YggdrasilInfo struct { AppOwner string `json:"Application-Owner"` } +type Property struct { + Name string `json:"name"` + Value string `json:"value"` +} + +type ProfileResponse struct { + Id string `json:"id"` + Name string `json:"name"` + Properties []Property `json:"properties"` +} + +type ProfileTextureMetadata struct { + Timestamp int `json:"timestamp"` + Id string `json:"profileId"` + Name string `json:"profileName"` + Textures ProfileTextures `json:"textures"` +} + +type SkinTexture struct { + Url string `json:"url"` + Metadata SkinMetadata `json:"metadata"` +} + +type SkinMetadata struct { + Model string `json:"model"` +} + +type ProfileTextures struct { + Skin SkinTexture `json:"SKIN"` +} + +type Empty struct{} + type NotFoundError struct{} func (m *NotFoundError) Error() string { @@ -69,3 +102,9 @@ type AlreadyExistsError struct{} func (m *AlreadyExistsError) Error() string { return "The specified item already exists" } + +type TooLargeError struct{} + +func (m *TooLargeError) Error() string { + return "The sent payload is too large." +} diff --git a/util.go b/util.go index 52db0e3..771cb6b 100644 --- a/util.go +++ b/util.go @@ -5,6 +5,7 @@ import ( "io/ioutil" "log" "net/http" + "runtime/debug" ) func sendJSON(w http.ResponseWriter, jsonMessage interface{}) { @@ -30,17 +31,11 @@ func unmarshalTo(r *http.Request, v any) error { } func handleError(w http.ResponseWriter, err error) { - switch err.Error() { - case "unexpected end of JSON input": - sendError(w, YggError{Code: 400, Error: "Bad Request", ErrorMessage: "The request data is malformed."}) - case "The specified item already exists": - sendError(w, YggError{Code: 400, Error: "Bad Request", ErrorMessage: "The specified item already exists."}) - case "Invalid credentials": - sendError(w, YggError{Code: 400, Error: "Bad Request", ErrorMessage: "Invalid credentials"}) - default: - sendError(w, YggError{Code: 500, Error: "Unspecified error", ErrorMessage: "An error has occured handling your request."}) - } + sendError(w, YggError{Code: 500, Error: "Unspecified error", ErrorMessage: "An error has occured handling your request."}) log.Println("error processing:", err) + if config.DebugMode { + debug.PrintStack() + } } func sendEmpty(w http.ResponseWriter) {