From f8afdd8a7753f8ff6c14796c6b0da3e06ce15862 Mon Sep 17 00:00:00 2001 From: murm Date: Wed, 22 Jun 2022 20:07:14 -0400 Subject: [PATCH] Initial Funny --- .gitignore | 3 + README.md | 5 + db.go | 255 +++++++++++++++++++++++++++++++++++++++++++++ endpoints.go | 150 ++++++++++++++++++++++++++ go.mod | 9 ++ go.sum | 6 ++ main.go | 27 +++++ secrets.go.example | 6 ++ types.go | 54 ++++++++++ util.go | 44 ++++++++ 10 files changed, 559 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 db.go create mode 100644 endpoints.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 secrets.go.example create mode 100644 types.go create mode 100644 util.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..48702fc --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +db.sqlite +secrets.go +tripwire \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..34ebdc5 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# Tripwire + +A replacement for *Yggdrasil* (Minecraft's legacy auth server) written in GoLang + +Should (mostly) meet the requirements laid out by [this wiki](https://wiki.vg/Authentication) \ No newline at end of file diff --git a/db.go b/db.go new file mode 100644 index 0000000..78f46f8 --- /dev/null +++ b/db.go @@ -0,0 +1,255 @@ +package main + +import ( + "database/sql" + + "github.com/google/uuid" + _ "github.com/mattn/go-sqlite3" +) + +var DB *sql.DB + +func initDB() error { + db, err := sql.Open("sqlite3", "./db.sqlite") + if err != nil { + return err + } + DB = db + err = createDatabase() + if err != nil { + return err + } + return nil +} + +func createDatabase() error { + sqlStatement := ` + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username VARCHAR(255) NOT NULL, + password VARCHAR(255) NOT NULL, + uuid VARCHAR(255) NOT NULL, + client_token VARCHAR(255) NOT NULL, + auth_token VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + ` + _, err := DB.Exec(sqlStatement) + if err != nil { + return err + } + return nil +} + +func insertUser(username string, password string) error { + playeruuid := uuid.New().String() + sqlStatement := ` + INSERT INTO users (username, password, uuid, client_token, auth_token) VALUES (?, ?, ?, ? ,?); + ` + _, err := DB.Exec(sqlStatement, username, password, playeruuid, "", "") + if err != nil { + return err + } + return nil +} + +func getAuthToken(username string, password string) (string, error) { + sqlStatement := ` + SELECT auth_token FROM users WHERE username = ? AND password = ?; + ` + rows, err := DB.Query(sqlStatement, username, password) + if err != nil { + return "", err + } + defer rows.Close() + + // check amount of rows + if rows.Next() { + // get auth token + var authToken string + rows.Scan(&authToken) + rows.Close() + if authToken == "" { + // generate new authToken + authToken = uuid.New().String() + // update authToken + sqlStatement := ` + UPDATE users SET auth_token = ? WHERE username = ? AND password = ?; + ` + _, err := DB.Exec(sqlStatement, authToken, username, password) + if err != nil { + return "", err + } + } + return authToken, nil + } else { + return "", &NotFoundError{} + } +} + +func checkClientToken(clientToken string, userName string) (string, error) { + // assumes user is already logged in + sqlStatement := ` + SELECT id FROM users WHERE client_token = ? AND username = ?; + ` + rows, err := DB.Query(sqlStatement, clientToken, userName) + if err != nil { + return "", err + } + defer rows.Close() + + // check amount of rows + if rows.Next() { + return clientToken, nil + } else { + clientToken = uuid.New().String() + sqlStatement := ` + UPDATE users SET client_token = ? WHERE username = ?; + ` + _, err := DB.Exec(sqlStatement, clientToken, userName) + if err != nil { + return "", err + } + clearAuthToken(userName) + return clientToken, nil + } +} + +func clearAuthToken(username string) error { + // runs when user logs out + sqlStatement := ` + UPDATE users SET auth_token = ? WHERE username = ?; + ` + _, err := DB.Exec(sqlStatement, "", username) + if err != nil { + return err + } + return nil +} + +// func insertAuthToken(authToken string, userName string) error { +// sqlStatement := ` +// UPDATE users SET auth_token = ? WHERE username = ?; +// ` +// _, err := DB.Exec(sqlStatement, authToken, userName) +// if err != nil { +// return err +// } +// return nil +// } + +func createUser(username string, adminToken string) (string, error) { + // check if adminToken is valid + if validateAdminToken(adminToken) { + password := uuid.New().String() + insertUser(username, password) + return password, nil + } else { + return "", &InvalidCredentialsError{} + } +} + +func getPlayerUUID(username string) (string, error) { + sqlStatement := ` + SELECT uuid FROM users WHERE username = ?; + ` + rows, err := DB.Query(sqlStatement, username) + if err != nil { + return "", err + } + defer rows.Close() + + // check amount of rows + if rows.Next() { + // get uuid + var uuid string + err = rows.Scan(&uuid) + if err != nil { + return "", err + } + return uuid, nil + } else { + return "", &NotFoundError{} + } +} + +func refreshTokens(refresh RefreshPayload) (RefreshPayload, error) { + sqlStatement := ` + SELECT id FROM users WHERE auth_token = ? and client_token = ?; + ` + rows, err := DB.Query(sqlStatement, refresh.AccessToken, refresh.ClientToken) + if err != nil { + return RefreshPayload{}, err + } + if rows.Next() { + // get id + var id int + err = rows.Scan(&id) + rows.Close() + if err != nil { + return RefreshPayload{}, err + } + // generate new authToken + authToken := uuid.New().String() + // update authToken + sqlStatement := ` + UPDATE users SET auth_token = ? WHERE id = ?; + ` + _, err := DB.Exec(sqlStatement, authToken, id) + if err != nil { + return RefreshPayload{}, err + } + // generate new clientToken + clientToken := uuid.New().String() + // update clientToken + sqlStatement = ` + UPDATE users SET client_token = ? WHERE id = ?; + ` + _, err = DB.Exec(sqlStatement, clientToken, id) + if err != nil { + return RefreshPayload{}, err + } + refresh.AccessToken = authToken + refresh.ClientToken = clientToken + return refresh, nil + } else { + return refresh, nil + } +} + +func validateTokens(authToken string, clientToken string) (bool, error) { + sqlStatement := ` + SELECT id FROM users WHERE auth_token = ? and client_token = ?; + ` + rows, err := DB.Query(sqlStatement, authToken, clientToken) + if err != nil { + return false, err + } + if rows.Next() { + return true, nil + } else { + return false, nil + } +} + +func invalidateTokens(authToken string, clientToken string) error { + sqlStatement := ` + UPDATE users SET auth_token = ?, client_token = ? WHERE auth_token = ? and client_token = ?; + ` + _, err := DB.Exec(sqlStatement, "", "", authToken, clientToken) + if err != nil { + return err + } + return nil +} + +func invalidateTokensWithLogin(username string, password string) error { + sqlStatement := ` + UPDATE users SET auth_token = ?, client_token = ? WHERE username = ? AND password = ?; + ` + _, err := DB.Exec(sqlStatement, "", "", username, password) + if err != nil { + return err + } + return nil +} diff --git a/endpoints.go b/endpoints.go new file mode 100644 index 0000000..18e9a7e --- /dev/null +++ b/endpoints.go @@ -0,0 +1,150 @@ +package main + +import ( + "net/http" + + "github.com/gorilla/mux" +) + +func authenticateEndpoint(w http.ResponseWriter, r *http.Request) { + var authPayload AuthPayload + err := unmarshalTo(r, &authPayload) + if err != nil { + handleError(w, err) + return + } + + // checks username and password + authToken, err := getAuthToken(authPayload.Username, authPayload.Password) + if err != nil { + err := YggError{Code: 401, Error: "Unauthorized", ErrorMessage: "The username or password is incorrect"} + sendError(w, err) + return + } + // authenticated at this point + clientToken, err := checkClientToken(authPayload.ClientToken, authPayload.Username) + if err != nil { + handleError(w, err) + return + } + playeruuid, err := getPlayerUUID(authPayload.Username) + if err != nil { + handleError(w, err) + return + } + profile := MCProfile{authPayload.Username, playeruuid} + authResponse := AuthResponse{ + ClientToken: clientToken, + AccessToken: authToken, + AvailableProfiles: []MCProfile{ + profile, + }, + SelectedProfile: profile, + } + + sendJSON(w, authResponse) +} + +func addUserEndpoint(w http.ResponseWriter, r *http.Request) { + var user UserCredentials + err := unmarshalTo(r, &user) + if err != nil { + handleError(w, err) + return + } + + // add user to db + newPassword, err := createUser(user.Username, user.Password) + if err != nil { + handleError(w, err) + return + } + // in this case, password is the admin token, not the password to assign + // send response + respAccount := UserCredentials{ + Username: user.Username, + Password: newPassword, + } + sendJSON(w, respAccount) +} + +func refreshTokenEndpoint(w http.ResponseWriter, r *http.Request) { + var refreshPayload RefreshPayload + err := unmarshalTo(r, &refreshPayload) + if err != nil { + handleError(w, err) + return + } + + responsePayload, err := refreshTokens(refreshPayload) + if err != nil { + handleError(w, err) + return + } + if refreshPayload == responsePayload { + err := YggError{Code: 400, Error: "Bad Request", ErrorMessage: "The access token is invalid or has expired"} + sendError(w, err) + return + } + sendJSON(w, responsePayload) +} + +func validateEndpoint(w http.ResponseWriter, r *http.Request) { + var refreshPayload RefreshPayload + err := unmarshalTo(r, &refreshPayload) + if err != nil { + handleError(w, err) + return + } + isValid, err := validateTokens(refreshPayload.AccessToken, refreshPayload.ClientToken) + if err != nil { + handleError(w, err) + return + } + if !isValid { + err := YggError{Code: 403, Error: "Bad Request", ErrorMessage: "The access token is invalid or has expired"} + sendError(w, err) + return + } + sendEmpty(w) +} + +func signoutEndpoint(w http.ResponseWriter, r *http.Request) { + var creds UserCredentials + err := unmarshalTo(r, &creds) + if err != nil { + handleError(w, err) + return + } + err = invalidateTokensWithLogin(creds.Username, creds.Password) + if err != nil { + handleError(w, err) + return + } + sendEmpty(w) +} + +func invalidateEndpoint(w http.ResponseWriter, r *http.Request) { + var refreshPayload RefreshPayload + err := unmarshalTo(r, &refreshPayload) + if err != nil { + handleError(w, err) + return + } + err = invalidateTokens(refreshPayload.AccessToken, refreshPayload.ClientToken) + if err != nil { + handleError(w, err) + return + } + sendEmpty(w) +} + +func registerEndpoints(r *mux.Router) { + r.HandleFunc("/", notFoundStub) + r.HandleFunc("/authenticate", authenticateEndpoint).Methods("POST") + r.HandleFunc("/refresh", refreshTokenEndpoint).Methods("POST") + r.HandleFunc("/signout", signoutEndpoint).Methods("POST") + r.HandleFunc("/invalidate", invalidateEndpoint).Methods("POST") + r.HandleFunc("/validate", validateEndpoint).Methods("POST") + r.HandleFunc("/admin/addUser", addUserEndpoint).Methods("POST") +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..18f5e9a --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module p.enisf.art/tripwire + +go 1.18 + +require ( + github.com/google/uuid v1.3.0 + github.com/gorilla/mux v1.8.0 + github.com/mattn/go-sqlite3 v1.14.13 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..cb65676 --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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= diff --git a/main.go b/main.go new file mode 100644 index 0000000..e454fb4 --- /dev/null +++ b/main.go @@ -0,0 +1,27 @@ +package main + +import ( + "log" + "net/http" + + "github.com/gorilla/mux" +) + +func notFoundStub(w http.ResponseWriter, r *http.Request) { + err := YggError{Code: 404, Error: "Not Found", ErrorMessage: "The server has not found anything matching the request URI"} + sendError(w, err) +} + +func handleRequests() { + r := mux.NewRouter().StrictSlash(true) + registerEndpoints(r) + log.Fatal(http.ListenAndServe(":10000", r)) +} + +func main() { + initDB() + + handleRequests() + + defer DB.Close() +} diff --git a/secrets.go.example b/secrets.go.example new file mode 100644 index 0000000..1cb31ef --- /dev/null +++ b/secrets.go.example @@ -0,0 +1,6 @@ +package main + +func getAdminToken() string { + adminToken := "admin token goes here" + return adminToken +} diff --git a/types.go b/types.go new file mode 100644 index 0000000..a079f05 --- /dev/null +++ b/types.go @@ -0,0 +1,54 @@ +package main + +type YggError struct { + Code int `json:"-"` + Error string `json:"error"` + ErrorMessage string `json:"errorMessage"` + // Cause string `json:"cause"` +} + +type MCProfile struct { + Name string `json:"name"` + Id string `json:"id"` +} +type MCAgent struct { + Name string `json:"name"` + Version int `json:"version"` +} + +type AuthPayload struct { + Agent MCAgent `json:"agent"` + Username string `json:"username"` + Password string `json:"password"` + ClientToken string `json:"clientToken"` + RequestUser bool `json:"requestUser"` +} + +type AuthResponse struct { + ClientToken string `json:"clientToken"` + AccessToken string `json:"accessToken"` + AvailableProfiles []MCProfile `json:"availableProfiles"` + SelectedProfile MCProfile `json:"selectedProfile"` +} + +type RefreshPayload struct { + AccessToken string `json:"accessToken"` + ClientToken string `json:"clientToken"` +} + +type UserCredentials struct { + Username string `json:"username"` + Password string `json:"password"` +} + +type NotFoundError struct{} + +func (m *NotFoundError) Error() string { + return "Not found" +} + +type InvalidCredentialsError struct{} + +func (m *InvalidCredentialsError) Error() string { + return "Invalid credentials" +} diff --git a/util.go b/util.go new file mode 100644 index 0000000..d6f9e14 --- /dev/null +++ b/util.go @@ -0,0 +1,44 @@ +package main + +import ( + "encoding/json" + "io/ioutil" + "log" + "net/http" +) + +func sendJSON(w http.ResponseWriter, jsonMessage interface{}) { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(jsonMessage) +} + +func sendError(w http.ResponseWriter, err YggError) { + w.WriteHeader(err.Code) + sendJSON(w, err) +} + +func unmarshalTo(r *http.Request, v any) error { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + return err + } + err = json.Unmarshal(body, &v) + if err != nil { + return err + } + return nil +} + +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."}) + default: + sendError(w, YggError{Code: 500, Error: "Unspecified error", ErrorMessage: "An error has occured handling your request."}) + } + log.Println("error processing:", err) +} + +func sendEmpty(w http.ResponseWriter) { + w.WriteHeader(http.StatusNoContent) +}