forked from TripwireTeam/tripwire
pushing unfinished work for the night
This commit is contained in:
parent
27f4047cf7
commit
7e089795c7
14 changed files with 760 additions and 156 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -2,3 +2,4 @@ db.sqlite
|
||||||
secrets.go
|
secrets.go
|
||||||
tripwire
|
tripwire
|
||||||
skins/
|
skins/
|
||||||
|
capes/
|
||||||
|
|
91
auth.go
91
auth.go
|
@ -1,10 +1,7 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
@ -43,12 +40,12 @@ func authenticateEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||||
handleError(w, err)
|
handleError(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
playeruuid, err := getPlayerUUID(authPayload.Username)
|
player, err := getPlayerByUsername(authPayload.Username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(w, err)
|
handleError(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
profile := MCProfile{authPayload.Username, playeruuid}
|
profile := MCProfile{authPayload.Username, player.UUID}
|
||||||
authResponse := AuthResponse{
|
authResponse := AuthResponse{
|
||||||
ClientToken: clientToken,
|
ClientToken: clientToken,
|
||||||
AccessToken: authToken,
|
AccessToken: authToken,
|
||||||
|
@ -155,81 +152,13 @@ func invalidateEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||||
sendEmpty(w)
|
sendEmpty(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
func registerAuthEndpoints(r *mux.Router) {
|
||||||
r.HandleFunc("/", rootEndpoint)
|
prefix := "/authserver"
|
||||||
r.HandleFunc("/authenticate", authenticateEndpoint).Methods("POST")
|
r.HandleFunc(prefix+"/", rootEndpoint)
|
||||||
r.HandleFunc("/refresh", refreshTokenEndpoint).Methods("POST")
|
r.HandleFunc(prefix+"/authenticate", authenticateEndpoint).Methods("POST")
|
||||||
r.HandleFunc("/signout", signoutEndpoint).Methods("POST")
|
r.HandleFunc(prefix+"/refresh", refreshTokenEndpoint).Methods("POST")
|
||||||
r.HandleFunc("/invalidate", invalidateEndpoint).Methods("POST")
|
r.HandleFunc(prefix+"/signout", signoutEndpoint).Methods("POST")
|
||||||
r.HandleFunc("/validate", validateEndpoint).Methods("POST")
|
r.HandleFunc(prefix+"/invalidate", invalidateEndpoint).Methods("POST")
|
||||||
r.HandleFunc("/admin/addUser", addUserEndpoint).Methods("POST")
|
r.HandleFunc(prefix+"/validate", validateEndpoint).Methods("POST")
|
||||||
r.HandleFunc("/getSkin/{uuid}", getSkinEndpoint).Methods("GET")
|
r.HandleFunc(prefix+"/admin/addUser", addUserEndpoint).Methods("POST")
|
||||||
r.HandleFunc("/setSkin/{uuid}", setSkinEndpoint).Methods("POST")
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
type Configuration struct {
|
type Configuration struct {
|
||||||
BaseUrl string `yaml:"baseUrl"`
|
BaseUrl string `yaml:"baseUrl"`
|
||||||
DebugMode bool `yaml:"debugMode"`
|
DebugMode bool `yaml:"debugMode"`
|
||||||
MaxSkinSize int `yaml:"maxSkinSize"`
|
MaxTextureSize int `yaml:"maxTextureSize"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var config Configuration
|
var config Configuration
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
baseUrl: "http://localhost:1000"
|
# The URL the Tripwire instance is hosted on.
|
||||||
|
baseUrl: "http://localhost:10000"
|
||||||
|
|
||||||
|
# Show debug output (if unsure, set to false)
|
||||||
debugMode: true
|
debugMode: true
|
||||||
maxSkinSize: 8096
|
|
||||||
|
# The maximum size (in bytes) of a skin/cape that can be uploaded.
|
||||||
|
maxTextureSize: 8096
|
48
db.go
48
db.go
|
@ -31,6 +31,7 @@ func createDatabase() error {
|
||||||
uuid VARCHAR(255) NOT NULL,
|
uuid VARCHAR(255) NOT NULL,
|
||||||
client_token VARCHAR(255) NOT NULL,
|
client_token VARCHAR(255) NOT NULL,
|
||||||
auth_token VARCHAR(255) NOT NULL,
|
auth_token VARCHAR(255) NOT NULL,
|
||||||
|
web_token VARCHAR(255) NOT NULL,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
`
|
`
|
||||||
|
@ -44,15 +45,16 @@ func createDatabase() error {
|
||||||
func insertUser(username string, password string) error {
|
func insertUser(username string, password string) error {
|
||||||
playeruuid := uuid.New().String()
|
playeruuid := uuid.New().String()
|
||||||
sqlStatement := `
|
sqlStatement := `
|
||||||
INSERT INTO users (username, password, uuid, client_token, auth_token) VALUES (?, ?, ?, ? ,?);
|
INSERT INTO users (username, password, uuid, client_token, auth_token, web_token) VALUES (?, ?, ?, ?, ?, ?);
|
||||||
`
|
`
|
||||||
_, err := DB.Exec(sqlStatement, username, password, playeruuid, "", "")
|
_, err := DB.Exec(sqlStatement, username, password, playeruuid, "", "", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// todo: deduplicate these functions
|
||||||
func getAuthToken(username string, password string) (string, error) {
|
func getAuthToken(username string, password string) (string, error) {
|
||||||
sqlStatement := `
|
sqlStatement := `
|
||||||
SELECT auth_token FROM users WHERE username = ? AND password = ?;
|
SELECT auth_token FROM users WHERE username = ? AND password = ?;
|
||||||
|
@ -84,6 +86,37 @@ func getAuthToken(username string, password string) (string, error) {
|
||||||
return authToken, nil
|
return authToken, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getWebToken(username string, password string) (string, error) {
|
||||||
|
sqlStatement := `
|
||||||
|
SELECT web_token FROM users WHERE username = ? AND password = ?;
|
||||||
|
`
|
||||||
|
row := DB.QueryRow(sqlStatement, username, password)
|
||||||
|
|
||||||
|
// get web token
|
||||||
|
var webToken string
|
||||||
|
err := row.Scan(&webToken)
|
||||||
|
if err != nil {
|
||||||
|
if err != sql.ErrNoRows {
|
||||||
|
return "", err
|
||||||
|
} else {
|
||||||
|
return "", &NotFoundError{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if webToken == "" {
|
||||||
|
// generate new webToken
|
||||||
|
webToken = uuid.New().String()
|
||||||
|
// update authToken
|
||||||
|
sqlStatement := `
|
||||||
|
UPDATE users SET web_token = ? WHERE username = ? AND password = ?;
|
||||||
|
`
|
||||||
|
_, err := DB.Exec(sqlStatement, webToken, username, password)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return webToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
// func checkClientToken(clientToken string, userName string) (string, error) {
|
// func checkClientToken(clientToken string, userName string) (string, error) {
|
||||||
// // assumes user is already logged in
|
// // assumes user is already logged in
|
||||||
// sqlStatement := `
|
// sqlStatement := `
|
||||||
|
@ -147,6 +180,17 @@ func checkClientToken(clientToken string, userName string) (string, error) {
|
||||||
// return nil
|
// return nil
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
func resetTokens(username string, password string) error {
|
||||||
|
sqlStatement := `
|
||||||
|
UPDATE users SET web_token = "", auth_token = "" WHERE username = ? AND password = ?;
|
||||||
|
`
|
||||||
|
_, err := DB.Exec(sqlStatement, username, password)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func createUser(username string, adminToken string) (string, error) {
|
func createUser(username string, adminToken string) (string, error) {
|
||||||
// check if adminToken is valid
|
// check if adminToken is valid
|
||||||
if validateAdminToken(adminToken) {
|
if validateAdminToken(adminToken) {
|
||||||
|
|
28
main.go
28
main.go
|
@ -13,14 +13,41 @@ import (
|
||||||
// sendError(w, err)
|
// sendError(w, err)
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
func logger(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if config.DebugMode {
|
||||||
|
log.Printf("Got hit at %s", r.URL.Path)
|
||||||
|
}
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func notfoundlogger(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if config.DebugMode {
|
||||||
|
log.Printf("Got hit at %s (NOT FOUND)", r.URL.Path)
|
||||||
|
}
|
||||||
|
sendError(w, YggError{
|
||||||
|
Code: 404,
|
||||||
|
Error: "Not Found",
|
||||||
|
ErrorMessage: "The server has not found anything matching the request URI",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func handleRequests() {
|
func handleRequests() {
|
||||||
r := mux.NewRouter().StrictSlash(true)
|
r := mux.NewRouter().StrictSlash(true)
|
||||||
|
r.Use(logger)
|
||||||
|
r.NotFoundHandler = http.HandlerFunc(notfoundlogger)
|
||||||
|
|
||||||
err := loadConfig()
|
err := loadConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln("Failed to load config.yaml:", err)
|
log.Fatalln("Failed to load config.yaml:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// todo: make this cleaner if possible
|
||||||
registerAuthEndpoints(r)
|
registerAuthEndpoints(r)
|
||||||
registerSessionEndpoints(r)
|
registerSessionEndpoints(r)
|
||||||
|
registerWebEndpoints(r)
|
||||||
|
|
||||||
log.Println("Tripwire started.")
|
log.Println("Tripwire started.")
|
||||||
log.Fatal(http.ListenAndServe(":10000", r))
|
log.Fatal(http.ListenAndServe(":10000", r))
|
||||||
}
|
}
|
||||||
|
@ -28,6 +55,7 @@ func handleRequests() {
|
||||||
func main() {
|
func main() {
|
||||||
log.Println("Tripwire initializing...")
|
log.Println("Tripwire initializing...")
|
||||||
os.Mkdir("skins", 0755)
|
os.Mkdir("skins", 0755)
|
||||||
|
os.Mkdir("capes", 0755)
|
||||||
initDB()
|
initDB()
|
||||||
|
|
||||||
handleRequests()
|
handleRequests()
|
||||||
|
|
130
player.go
130
player.go
|
@ -1,20 +1,22 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
// todo: make a consistent "player" type and have these functions return it
|
// todo: make a consistent "player" type and have these functions return it
|
||||||
|
// todo: dont manually patch column name into various internal funcs (would make the extra funcs unnecessary)
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
|
"log"
|
||||||
)
|
)
|
||||||
|
|
||||||
func playerExistsByUsername(username string) (bool, error) {
|
func _playerExistsBy(query string, value any) (bool, error) {
|
||||||
sqlStatement := `
|
sqlStatement := `
|
||||||
SELECT username FROM users WHERE username = ?;
|
SELECT username FROM users WHERE ` + query + ` = ?;
|
||||||
`
|
`
|
||||||
var x string
|
var x string
|
||||||
err := DB.QueryRow(sqlStatement, username).Scan(&x)
|
err := DB.QueryRow(sqlStatement, value).Scan(&x)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
return false, nil
|
return false, nil
|
||||||
|
@ -23,52 +25,55 @@ func playerExistsByUsername(username string) (bool, error) {
|
||||||
}
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func playerExistsByUUID(uuid string) (bool, error) {
|
func playerExistsByUUID(uuid string) (bool, error) {
|
||||||
|
return _playerExistsBy("uuid", uuid)
|
||||||
|
}
|
||||||
|
func playerExistsByUsername(username string) (bool, error) {
|
||||||
|
return _playerExistsBy("username", username)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _getPlayerBy(query string, value any) (PlayerData, error) {
|
||||||
|
// todo: make this function less repetitive if possible
|
||||||
|
|
||||||
sqlStatement := `
|
sqlStatement := `
|
||||||
SELECT uuid FROM users WHERE uuid = ?;
|
SELECT id, username, password, uuid, client_token, auth_token, web_token, created_at FROM users WHERE ` + query + ` = ?;
|
||||||
`
|
`
|
||||||
var x string
|
var player PlayerData
|
||||||
err := DB.QueryRow(sqlStatement, uuid).Scan(&x)
|
err := DB.QueryRow(sqlStatement, value).Scan(
|
||||||
|
&player.Id,
|
||||||
|
&player.Username,
|
||||||
|
&player.Password,
|
||||||
|
&player.UUID,
|
||||||
|
&player.ClientToken,
|
||||||
|
&player.AuthToken,
|
||||||
|
&player.WebToken,
|
||||||
|
&player.CreatedAt,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
return false, nil
|
return player, nil
|
||||||
}
|
}
|
||||||
return false, err
|
return player, err
|
||||||
}
|
}
|
||||||
return true, nil
|
return player, nil
|
||||||
|
}
|
||||||
|
func getPlayerByUUID(uuid string) (PlayerData, error) {
|
||||||
|
return _getPlayerBy("uuid", uuid)
|
||||||
|
}
|
||||||
|
func getPlayerByUsername(username string) (PlayerData, error) {
|
||||||
|
return _getPlayerBy("username", username)
|
||||||
|
}
|
||||||
|
func getPlayerByAuthToken(auth string) (PlayerData, error) {
|
||||||
|
log.Println(auth)
|
||||||
|
return _getPlayerBy("auth_token", auth)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPlayerUUID(username string) (string, error) {
|
// todo: dedupe skin and cape functions if possible
|
||||||
sqlStatement := `
|
func getPlayerTexture(textureId string, uuid string) ([]byte, error) {
|
||||||
SELECT uuid FROM users WHERE username = ?;
|
if !(textureId == "skin" || textureId == "cape") {
|
||||||
`
|
return []byte{}, &InvalidDataError{}
|
||||||
row := DB.QueryRow(sqlStatement, username)
|
|
||||||
|
|
||||||
var uuid string
|
|
||||||
err := row.Scan(&uuid)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
}
|
||||||
return uuid, nil
|
tex, err := os.ReadFile(textureId + "s/" + uuid + ".png")
|
||||||
}
|
|
||||||
|
|
||||||
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 err != nil {
|
||||||
if errors.Is(err, fs.ErrNotExist) {
|
if errors.Is(err, fs.ErrNotExist) {
|
||||||
return []byte{}, &NotFoundError{}
|
return []byte{}, &NotFoundError{}
|
||||||
|
@ -76,13 +81,17 @@ func getPlayerSkinByUUID(uuid string) ([]byte, error) {
|
||||||
return []byte{}, err
|
return []byte{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return skin, nil
|
return tex, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func setPlayerSkinByUUID(uuid string, skin []byte) error {
|
func setPlayerTexture(textureId string, uuid string, tex []byte) error {
|
||||||
if len(skin) > config.MaxSkinSize {
|
if !(textureId == "skin" || textureId == "cape") {
|
||||||
|
return &InvalidDataError{}
|
||||||
|
}
|
||||||
|
if len(tex) > config.MaxTextureSize {
|
||||||
return &TooLargeError{}
|
return &TooLargeError{}
|
||||||
}
|
}
|
||||||
|
|
||||||
exists, err := playerExistsByUUID(uuid)
|
exists, err := playerExistsByUUID(uuid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -91,7 +100,7 @@ func setPlayerSkinByUUID(uuid string, skin []byte) error {
|
||||||
return &NotFoundError{}
|
return &NotFoundError{}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = os.WriteFile("skins/"+uuid+".png", skin, 0644)
|
err = os.WriteFile(textureId+"s/"+uuid+".png", tex, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -99,25 +108,26 @@ func setPlayerSkinByUUID(uuid string, skin []byte) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func findPlayerUUIDByAuthToken(token string) (string, error) {
|
func playerHasTexture(textureId string, uuid string) (bool, error) {
|
||||||
sqlStatement := `
|
if !(textureId == "skin" || textureId == "cape") {
|
||||||
SELECT uuid FROM users WHERE auth_token = ?;
|
return false, &InvalidDataError{}
|
||||||
`
|
|
||||||
row := DB.QueryRow(sqlStatement, token)
|
|
||||||
|
|
||||||
var uuid string
|
|
||||||
err := row.Scan(&uuid)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
}
|
||||||
return uuid, nil
|
|
||||||
|
_, err := os.Stat(textureId + "s/" + uuid + ".png")
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, fs.ErrNotExist) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkPlayerPassByUUID(uuid string, password string) (bool, error) {
|
func _checkPlayerPassBy(query string, value any, password string) (bool, error) {
|
||||||
sqlStatement := `
|
sqlStatement := `
|
||||||
SELECT password FROM users WHERE auth_token = ?;
|
SELECT password FROM users WHERE ` + query + ` = ?;
|
||||||
`
|
`
|
||||||
row := DB.QueryRow(sqlStatement, token)
|
row := DB.QueryRow(sqlStatement, value)
|
||||||
|
|
||||||
var pass string
|
var pass string
|
||||||
err := row.Scan(&pass)
|
err := row.Scan(&pass)
|
||||||
|
@ -129,3 +139,9 @@ func checkPlayerPassByUUID(uuid string, password string) (bool, error) {
|
||||||
}
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
func checkPlayerPassByUUID(uuid string, password string) (bool, error) {
|
||||||
|
return _checkPlayerPassBy("uuid", uuid, password)
|
||||||
|
}
|
||||||
|
func checkPlayerPassByUsername(username string, password string) (bool, error) {
|
||||||
|
return _checkPlayerPassBy("username", username, password)
|
||||||
|
}
|
||||||
|
|
37
session.go
37
session.go
|
@ -4,7 +4,9 @@ import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
"log"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
@ -26,13 +28,13 @@ func profileEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
username, err := getPlayerUsername(uuid)
|
player, err := getPlayerByUUID(uuid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(w, err)
|
handleError(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response, err := generateProfileResponse(uuid, username)
|
response, err := generateProfileResponse(uuid, player.Username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(w, err)
|
handleError(w, err)
|
||||||
return
|
return
|
||||||
|
@ -48,13 +50,13 @@ func hasJoinedEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
uuid, err := getPlayerUUID(params.Get("username"))
|
player, err := getPlayerByUsername(params.Get("username"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(w, err)
|
handleError(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response, err := generateProfileResponse(params.Get("username"), uuid)
|
response, err := generateProfileResponse(params.Get("username"), player.UUID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(w, err)
|
handleError(w, err)
|
||||||
return
|
return
|
||||||
|
@ -64,8 +66,10 @@ func hasJoinedEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func joinEndpoint(w http.ResponseWriter, r *http.Request) {
|
func joinEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||||
params := r.URL.Query()
|
var payload JoinPayload
|
||||||
if !params.Has("accessToken") || !params.Has("selectedProfile") || !params.Has("serverId") {
|
unmarshalTo(r, &payload)
|
||||||
|
|
||||||
|
if payload.AccessToken == "" || payload.SelectedProfile == "" || payload.ServerId == "" {
|
||||||
sendError(w, YggError{
|
sendError(w, YggError{
|
||||||
Code: 400,
|
Code: 400,
|
||||||
Error: "IllegalArgumentException",
|
Error: "IllegalArgumentException",
|
||||||
|
@ -73,31 +77,35 @@ func joinEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
uuid, err := findPlayerUUIDByAuthToken(params.Get("accessToken"))
|
player, err := getPlayerByAuthToken(payload.AccessToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleError(w, err)
|
handleError(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if params.Get("selectedProfile") != shrinkUUID(uuid) {
|
log.Println(payload.SelectedProfile, ",", player.UUID)
|
||||||
|
if payload.SelectedProfile != shrinkUUID(player.UUID) {
|
||||||
sendError(w, YggError{
|
sendError(w, YggError{
|
||||||
Code: 400,
|
Code: 400,
|
||||||
Error: "Bad Request",
|
Error: "Bad Request",
|
||||||
ErrorMessage: "The request data is malformed.",
|
ErrorMessage: "The request data is malformed.",
|
||||||
})
|
})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
sendEmpty(w)
|
sendEmpty(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
func registerSessionEndpoints(r *mux.Router) {
|
func registerSessionEndpoints(r *mux.Router) {
|
||||||
r.HandleFunc("/session/minecraft/profile/{uuid}", profileEndpoint).Methods("GET")
|
prefix := "/sessionserver"
|
||||||
r.HandleFunc("/session/minecraft/hasJoined", hasJoinedEndpoint).Methods("GET")
|
r.HandleFunc(prefix+"/session/minecraft/profile/{uuid}", profileEndpoint).Methods("GET")
|
||||||
|
r.HandleFunc(prefix+"/session/minecraft/join", joinEndpoint).Methods("POST")
|
||||||
|
r.HandleFunc(prefix+"/session/minecraft/hasJoined", hasJoinedEndpoint).Methods("GET")
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateProfileResponse(uuid string, username string) (ProfileResponse, error) {
|
func generateProfileResponse(uuid string, username string) (ProfileResponse, error) {
|
||||||
// todo: make this more visually appealing if possible
|
// todo: make this more visually appealing if possible
|
||||||
clearUUID := strings.Join(strings.Split(uuid, "-"), "")
|
clearUUID := strings.Join(strings.Split(uuid, "-"), "")
|
||||||
skin := SkinTexture{}
|
skin := SkinTexture{}
|
||||||
skin.Url = config.BaseUrl + "/getSkin/" + uuid
|
skin.Url = config.BaseUrl + "/getTexture/" + uuid + "?type=skin"
|
||||||
skin.Metadata = SkinMetadata{}
|
skin.Metadata = SkinMetadata{}
|
||||||
skin.Metadata.Model = "default"
|
skin.Metadata.Model = "default"
|
||||||
|
|
||||||
|
@ -107,6 +115,13 @@ func generateProfileResponse(uuid string, username string) (ProfileResponse, err
|
||||||
textures.Textures = ProfileTextures{}
|
textures.Textures = ProfileTextures{}
|
||||||
textures.Textures.Skin = skin
|
textures.Textures.Skin = skin
|
||||||
|
|
||||||
|
_, err := os.Stat("capes/" + uuid + ".png")
|
||||||
|
if err == nil {
|
||||||
|
cape := &Texture{}
|
||||||
|
cape.Url = config.BaseUrl + "/getTexture/" + uuid + "?type=cape"
|
||||||
|
textures.Textures.Cape = cape
|
||||||
|
}
|
||||||
|
|
||||||
marshalledTextures, err := json.Marshal(textures)
|
marshalledTextures, err := json.Marshal(textures)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ProfileResponse{}, err
|
return ProfileResponse{}, err
|
||||||
|
|
36
types.go
36
types.go
|
@ -1,5 +1,7 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
type YggError struct {
|
type YggError struct {
|
||||||
Code int `json:"-"`
|
Code int `json:"-"`
|
||||||
Error string `json:"error"`
|
Error string `json:"error"`
|
||||||
|
@ -31,6 +33,18 @@ type AuthResponse struct {
|
||||||
SelectedProfile MCProfile `json:"selectedProfile"`
|
SelectedProfile MCProfile `json:"selectedProfile"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type WebLogInResponse struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
UUID string `json:"uuid"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JoinPayload struct {
|
||||||
|
AccessToken string `json:"accessToken"`
|
||||||
|
SelectedProfile string `json:"selectedProfile"`
|
||||||
|
ServerId string `json:"serverId"`
|
||||||
|
}
|
||||||
|
|
||||||
type RefreshPayload struct {
|
type RefreshPayload struct {
|
||||||
AccessToken string `json:"accessToken"`
|
AccessToken string `json:"accessToken"`
|
||||||
ClientToken string `json:"clientToken"`
|
ClientToken string `json:"clientToken"`
|
||||||
|
@ -41,6 +55,17 @@ type UserCredentials struct {
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PlayerData struct {
|
||||||
|
Id int `json:"id"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
UUID string `json:"uuid"`
|
||||||
|
ClientToken string `json:"clientToken"`
|
||||||
|
AuthToken string `json:"authToken"`
|
||||||
|
WebToken string `json:"webToken"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
}
|
||||||
|
|
||||||
type YggdrasilInfo struct {
|
type YggdrasilInfo struct {
|
||||||
Status string `json:"Status"`
|
Status string `json:"Status"`
|
||||||
RuntimeMode string `json:"Runtime-Mode"`
|
RuntimeMode string `json:"Runtime-Mode"`
|
||||||
|
@ -75,12 +100,17 @@ type SkinTexture struct {
|
||||||
Metadata SkinMetadata `json:"metadata"`
|
Metadata SkinMetadata `json:"metadata"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Texture struct {
|
||||||
|
Url string `json:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
type SkinMetadata struct {
|
type SkinMetadata struct {
|
||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProfileTextures struct {
|
type ProfileTextures struct {
|
||||||
Skin SkinTexture `json:"SKIN"`
|
Skin SkinTexture `json:"SKIN"`
|
||||||
|
Cape *Texture `json:"CAPE"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Empty struct{}
|
type Empty struct{}
|
||||||
|
@ -108,3 +138,9 @@ type TooLargeError struct{}
|
||||||
func (m *TooLargeError) Error() string {
|
func (m *TooLargeError) Error() string {
|
||||||
return "The sent payload is too large."
|
return "The sent payload is too large."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type InvalidDataError struct{}
|
||||||
|
|
||||||
|
func (m *InvalidDataError) Error() string {
|
||||||
|
return "The request data is malformed."
|
||||||
|
}
|
||||||
|
|
24
util.go
24
util.go
|
@ -4,6 +4,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
)
|
)
|
||||||
|
@ -14,6 +15,7 @@ func sendJSON(w http.ResponseWriter, jsonMessage interface{}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendError(w http.ResponseWriter, err YggError) {
|
func sendError(w http.ResponseWriter, err YggError) {
|
||||||
|
log.Printf("Send error: %s\n", err.ErrorMessage)
|
||||||
w.WriteHeader(err.Code)
|
w.WriteHeader(err.Code)
|
||||||
sendJSON(w, err)
|
sendJSON(w, err)
|
||||||
}
|
}
|
||||||
|
@ -41,3 +43,25 @@ func handleError(w http.ResponseWriter, err error) {
|
||||||
func sendEmpty(w http.ResponseWriter) {
|
func sendEmpty(w http.ResponseWriter) {
|
||||||
w.WriteHeader(http.StatusNoContent)
|
w.WriteHeader(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func recievePNG(filename string, w http.ResponseWriter, r *http.Request) ([]byte, *multipart.FileHeader, error) {
|
||||||
|
file, header, err := r.FormFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return []byte{}, header, err
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes, err := ioutil.ReadAll(file)
|
||||||
|
if err != nil {
|
||||||
|
return bytes, header, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(bytes) < 4 ||
|
||||||
|
bytes[0] != 0x89 ||
|
||||||
|
bytes[1] != 0x50 ||
|
||||||
|
bytes[2] != 0x4e ||
|
||||||
|
bytes[3] != 0x47 {
|
||||||
|
return bytes, header, &InvalidDataError{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytes, header, nil
|
||||||
|
}
|
||||||
|
|
201
web.go
Normal file
201
web.go
Normal file
|
@ -0,0 +1,201 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"image"
|
||||||
|
_ "image/png"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
)
|
||||||
|
|
||||||
|
func logInEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var details UserCredentials
|
||||||
|
err := unmarshalTo(r, &details)
|
||||||
|
if err != nil {
|
||||||
|
handleError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidCreds := YggError{
|
||||||
|
Code: 401,
|
||||||
|
Error: "Unauthorized",
|
||||||
|
ErrorMessage: "Invalid credentials.",
|
||||||
|
}
|
||||||
|
exists, err := playerExistsByUsername(details.Username)
|
||||||
|
if err != nil {
|
||||||
|
handleError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
sendError(w, invalidCreds)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
correct, err := checkPlayerPassByUsername(details.Username, details.Password)
|
||||||
|
if err != nil {
|
||||||
|
handleError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if correct {
|
||||||
|
response := WebLogInResponse{}
|
||||||
|
webToken, err := getWebToken(details.Username, details.Password)
|
||||||
|
if err != nil {
|
||||||
|
handleError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
player, err := getPlayerByUsername(details.Username)
|
||||||
|
if err != nil {
|
||||||
|
handleError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Token = webToken
|
||||||
|
response.UUID = player.UUID
|
||||||
|
response.Username = player.Username
|
||||||
|
|
||||||
|
sendJSON(w, response)
|
||||||
|
} else {
|
||||||
|
sendError(w, YggError{
|
||||||
|
Code: 401,
|
||||||
|
Error: "Unauthorized",
|
||||||
|
ErrorMessage: "Invalid credentials.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getResourceEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||||
|
uuid := mux.Vars(r)["uuid"]
|
||||||
|
query := r.URL.Query()
|
||||||
|
if !query.Has("type") {
|
||||||
|
sendError(w, YggError{
|
||||||
|
Code: 400,
|
||||||
|
Error: "Bad Request",
|
||||||
|
ErrorMessage: "Must specify texture type. Add ?type=skin or ?type=cape to the URL.",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
textureId := query.Get("type")
|
||||||
|
if !(textureId == "skin" || textureId == "cape") {
|
||||||
|
sendError(w, YggError{
|
||||||
|
Code: 400,
|
||||||
|
Error: "Bad Request",
|
||||||
|
ErrorMessage: "Invalid texture type.",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
skin, err := getPlayerTexture(textureId, 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 setResourceEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||||
|
uuid := mux.Vars(r)["uuid"]
|
||||||
|
query := r.URL.Query()
|
||||||
|
if !query.Has("type") {
|
||||||
|
sendError(w, YggError{Code: 400, Error: "Bad Request", ErrorMessage: "Must specify texture type. Add ?type=skin or ?type=cape to the URL."})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
textureId := query.Get("type")
|
||||||
|
if !(textureId == "skin" || textureId == "cape") {
|
||||||
|
sendError(w, YggError{
|
||||||
|
Code: 400,
|
||||||
|
Error: "Bad Request",
|
||||||
|
ErrorMessage: "Invalid texture type.",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.ParseMultipartForm(int64(config.MaxTextureSize))
|
||||||
|
|
||||||
|
token := r.FormValue("token")
|
||||||
|
|
||||||
|
player, err := getPlayerByUUID(uuid)
|
||||||
|
if err != nil {
|
||||||
|
handleError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if token != player.WebToken {
|
||||||
|
sendError(w, YggError{Code: 400, Error: "Bad Request", ErrorMessage: "Invalid credentials."})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
file, _, err := recievePNG("file", w, r)
|
||||||
|
if err != nil {
|
||||||
|
handleError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reader := bytes.NewReader(file)
|
||||||
|
img, _, err := image.Decode(reader)
|
||||||
|
if err != nil {
|
||||||
|
sendError(w, YggError{
|
||||||
|
Code: 400,
|
||||||
|
Error: "Bad Request",
|
||||||
|
ErrorMessage: "The request data is malformed.",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bounds := img.Bounds()
|
||||||
|
targetX := 64
|
||||||
|
targetY := 64
|
||||||
|
if textureId == "cape" {
|
||||||
|
targetY = 32
|
||||||
|
}
|
||||||
|
|
||||||
|
if bounds.Dx() != targetX || bounds.Dy() != targetY {
|
||||||
|
sendError(w, YggError{
|
||||||
|
Code: 400,
|
||||||
|
Error: "Bad Request",
|
||||||
|
ErrorMessage: "The request data is malformed.",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = setPlayerTexture(textureId, uuid, file)
|
||||||
|
if err != nil {
|
||||||
|
handleError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sendEmpty(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func rootRedirect(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if strings.HasPrefix(r.Header.Get("User-Agent"), "Java") {
|
||||||
|
rootEndpoint(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.Redirect(w, r, "/web", http.StatusMovedPermanently)
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerWebEndpoints(r *mux.Router) {
|
||||||
|
webDir := "/web"
|
||||||
|
r.HandleFunc("/", rootRedirect)
|
||||||
|
r.HandleFunc("/webapi/logIn", logInEndpoint).Methods("POST")
|
||||||
|
r.PathPrefix(webDir).
|
||||||
|
Handler(http.StripPrefix(webDir, http.FileServer(http.Dir("."+webDir))))
|
||||||
|
|
||||||
|
r.HandleFunc("/getTexture/{uuid}", getResourceEndpoint).Methods("GET")
|
||||||
|
r.HandleFunc("/setTexture/{uuid}", setResourceEndpoint).Methods("POST")
|
||||||
|
}
|
15
web/index.html
Normal file
15
web/index.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset='utf-8'>
|
||||||
|
<title>Tripwire Web Panel</title>
|
||||||
|
<meta name='viewport' content='width=device-width, initial-scale=1'>
|
||||||
|
<link rel='stylesheet' type='text/css' media='screen' href='./web/main.css'>
|
||||||
|
<script src="https://unpkg.com/mithril/mithril.js"></script>
|
||||||
|
<script src="https://unpkg.com/js-cookie"></script>
|
||||||
|
<script src='./web/main.js' type="module"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>JavaScript must be enabled to use this application.</noscript>
|
||||||
|
</body>
|
||||||
|
</html>
|
90
web/main.css
Normal file
90
web/main.css
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
:root {
|
||||||
|
background-color: #222;
|
||||||
|
color: #CCC;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
|
||||||
|
--border-radius: 6px;
|
||||||
|
--header-height: 50px;
|
||||||
|
--accent-color: #5f00aa;
|
||||||
|
--accent-color-secondary: #4f0099;
|
||||||
|
--error-color: #cc4444;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: var(--header-height);
|
||||||
|
line-height: var(--header-height);
|
||||||
|
background-color: #141414;
|
||||||
|
align-items: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
padding: 0px 15px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
#header-spacer {
|
||||||
|
padding-bottom: var(--header-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
#header-title {
|
||||||
|
font-size: 1.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.align-right {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
button, a.button {
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
color: inherit;
|
||||||
|
border: 0;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
padding: 8px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
button:hover, a.button:hover {
|
||||||
|
background-color: var(--accent-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
display: block;
|
||||||
|
background-color: #141414;
|
||||||
|
color: inherit;
|
||||||
|
border: 0;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
padding: 8px 10px;
|
||||||
|
color: white;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%,-50%);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.errorText {
|
||||||
|
color: var(--error-color);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.errorInput {
|
||||||
|
outline: 1px solid var(--error-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#playerHead {
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-top: -4px;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
200
web/main.js
Normal file
200
web/main.js
Normal file
|
@ -0,0 +1,200 @@
|
||||||
|
import htm from 'https://unpkg.com/htm?module'
|
||||||
|
const html = htm.bind(m);
|
||||||
|
var root = document.body
|
||||||
|
|
||||||
|
var currentUser = null;
|
||||||
|
try {
|
||||||
|
currentUser = JSON.parse(Cookies.get("currentUser"))
|
||||||
|
} catch {}
|
||||||
|
var loggingIn = false;
|
||||||
|
var logInError = ""
|
||||||
|
|
||||||
|
var PlayerHead = {
|
||||||
|
oncreate: function (vnode) {
|
||||||
|
const ctx = vnode.dom.getContext('2d');
|
||||||
|
ctx.imageSmoothingEnabled = false
|
||||||
|
|
||||||
|
let img = new Image();
|
||||||
|
img.src = `/getTexture/${currentUser.uuid}?type=skin`
|
||||||
|
|
||||||
|
img.onload = function(){
|
||||||
|
ctx.drawImage(img, 8, 8, 8, 8, 0, 0, 32, 32);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
view: function(vnode) {
|
||||||
|
return html `
|
||||||
|
<canvas id="playerHead" width="32" height="32" />
|
||||||
|
`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var Header = {
|
||||||
|
view: function() {
|
||||||
|
return html`
|
||||||
|
<header>
|
||||||
|
<a href="#!/" id="header-title">Tripwire</span>
|
||||||
|
<div class="align-right">
|
||||||
|
${currentUser ?
|
||||||
|
html`
|
||||||
|
<${PlayerHead} />
|
||||||
|
<span>Hello, ${currentUser.username}</span>
|
||||||
|
` :
|
||||||
|
html`<a class="button" href="#!/login">Log In</a>`
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div id="header-spacer"></div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrorText = {
|
||||||
|
view: function(vnode) {
|
||||||
|
return html`
|
||||||
|
<span class="errorText">${vnode.attrs.error.errorMessage}</span>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.route(root, "/", {
|
||||||
|
"/": {
|
||||||
|
view: function() {
|
||||||
|
return html`
|
||||||
|
<${Header} />
|
||||||
|
${currentUser ?
|
||||||
|
html`
|
||||||
|
<div class="center">
|
||||||
|
<a class="button" href="#!/skinchanger">Change skin</a>
|
||||||
|
</div>
|
||||||
|
` :
|
||||||
|
html`<span>Please log in to continue.</span>`
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/skinchanger": function SkinChanger() {
|
||||||
|
var pageError = null;
|
||||||
|
var success = false;
|
||||||
|
return {
|
||||||
|
view: function() {
|
||||||
|
if (!currentUser) {
|
||||||
|
m.route.set("/login")
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<${Header} />
|
||||||
|
${currentUser ?
|
||||||
|
success ?
|
||||||
|
html `
|
||||||
|
<div class="center">
|
||||||
|
<span style="font-size: 1.5em;">Texture successfully uploaded!</span><br/><br/>
|
||||||
|
<a class="button" href="#!/">Go back</a>
|
||||||
|
</div>
|
||||||
|
` :
|
||||||
|
html`
|
||||||
|
<div class="center">
|
||||||
|
<span style="font-size: 1.5em;">Select Skin/Cape </span>
|
||||||
|
<span>(only .PNG supported)</span><br/><br/>
|
||||||
|
<input style="display: inline;"
|
||||||
|
type="radio"
|
||||||
|
id="skinRadio"
|
||||||
|
name="textureId"
|
||||||
|
value="skin"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
<label for="skinRadio">Skin</label>
|
||||||
|
<input style="display: inline;"
|
||||||
|
type="radio"
|
||||||
|
id="capeRadio"
|
||||||
|
name="textureId"
|
||||||
|
value="cape"
|
||||||
|
/>
|
||||||
|
<label for="capeRadio">Cape</label>
|
||||||
|
<input id="textureFile"
|
||||||
|
style="width: 100%"
|
||||||
|
type="file"
|
||||||
|
accept="image/png"
|
||||||
|
onchange=${() => {
|
||||||
|
pageError = null
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
${pageError && html`<${ErrorText} error=${pageError}/>`}
|
||||||
|
<button onclick=${() => {
|
||||||
|
let input = document.getElementById("textureFile")
|
||||||
|
if (input.files.length == 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let file = input.files[0]
|
||||||
|
|
||||||
|
let body = new FormData()
|
||||||
|
body.append("file", file)
|
||||||
|
body.append("token", currentUser.token)
|
||||||
|
m.request({
|
||||||
|
method: "POST",
|
||||||
|
url: "/setTexture/"+currentUser.uuid+"?type=" +
|
||||||
|
(document.getElementById("skinRadio").checked ? "skin" : "cape"),
|
||||||
|
body: body,
|
||||||
|
}).then((res) => {
|
||||||
|
success = true
|
||||||
|
}, (res) => {
|
||||||
|
pageError = res.response
|
||||||
|
})
|
||||||
|
}}>Let it rip</button>
|
||||||
|
</div>
|
||||||
|
` :
|
||||||
|
html`<span>Please log in to continue.</span>`
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/login": {
|
||||||
|
view: function() {
|
||||||
|
if (currentUser) {
|
||||||
|
m.route.set("/")
|
||||||
|
}
|
||||||
|
let oninput = () => {
|
||||||
|
logInError = null
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<${Header} />
|
||||||
|
<div class="center">
|
||||||
|
<input id="usernameLogin"
|
||||||
|
placeholder="Username"
|
||||||
|
type="text"
|
||||||
|
class=${logInError && "errorInput"}
|
||||||
|
oninput=${oninput}
|
||||||
|
/>
|
||||||
|
<input id="passwordLogin"
|
||||||
|
placeholder="Password"
|
||||||
|
type="text"
|
||||||
|
class=${logInError && "errorInput"}
|
||||||
|
oninput=${oninput}
|
||||||
|
/>
|
||||||
|
${logInError && html`
|
||||||
|
<${ErrorText} error=${logInError} />
|
||||||
|
`}
|
||||||
|
<button disabled=${loggingIn} onclick=${function() {
|
||||||
|
loggingIn = true;
|
||||||
|
m.request({
|
||||||
|
method: "POST",
|
||||||
|
url: "/webapi/logIn",
|
||||||
|
body: {
|
||||||
|
username: document.getElementById("usernameLogin").value,
|
||||||
|
password: document.getElementById("passwordLogin").value,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(function(result) {
|
||||||
|
currentUser = result
|
||||||
|
Cookies.set("currentUser", JSON.stringify(result))
|
||||||
|
loggingIn = false
|
||||||
|
}, function(result) {
|
||||||
|
logInError = result.response
|
||||||
|
loggingIn = false
|
||||||
|
})
|
||||||
|
}}>Log In</button>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
Loading…
Reference in a new issue