forked from TripwireTeam/tripwire
skin system (kinda), session system (kinda), config framework, much cleanup to be done tomorrow
This commit is contained in:
parent
1b0d368d63
commit
27f4047cf7
14 changed files with 470 additions and 44 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,3 +1,4 @@
|
|||
db.sqlite
|
||||
secrets.go
|
||||
tripwire
|
||||
tripwire
|
||||
skins/
|
||||
|
|
|
@ -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
28
config.go
Normal 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
3
config.yaml
Normal file
|
@ -0,0 +1,3 @@
|
|||
baseUrl: "http://localhost:1000"
|
||||
debugMode: true
|
||||
maxSkinSize: 8096
|
29
db.go
29
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 = ?;
|
||||
|
|
BIN
default.png
Normal file
BIN
default.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 677 B |
1
go.mod
1
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
|
||||
)
|
||||
|
|
3
go.sum
3
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=
|
||||
|
|
12
main.go
12
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()
|
||||
|
|
131
player.go
Normal file
131
player.go
Normal 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
137
session.go
Normal 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
39
sha.go
Normal 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
|
||||
}
|
39
types.go
39
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."
|
||||
}
|
||||
|
|
15
util.go
15
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) {
|
||||
|
|
Loading…
Reference in a new issue