skin system (kinda), session system (kinda), config framework, much cleanup to be done tomorrow

This commit is contained in:
TaiAurori 2022-06-23 22:22:17 -04:00
parent 1b0d368d63
commit 27f4047cf7
14 changed files with 470 additions and 44 deletions

3
.gitignore vendored
View File

@ -1,3 +1,4 @@
db.sqlite
secrets.go
tripwire
tripwire
skins/

View File

@ -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")
}

28
config.go Normal file
View File

@ -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
}

3
config.yaml Normal file
View File

@ -0,0 +1,3 @@
baseUrl: "http://localhost:1000"
debugMode: true
maxSkinSize: 8096

29
db.go
View File

@ -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 = ?;

BIN
default.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 677 B

1
go.mod
View File

@ -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
)

3
go.sum
View File

@ -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=

12
main.go
View File

@ -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()

131
player.go Normal file
View File

@ -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
}

137
session.go Normal file
View File

@ -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 ""
}

39
sha.go Normal file
View File

@ -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
}

View File

@ -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."
}

15
util.go
View File

@ -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) {