commit f8afdd8a7753f8ff6c14796c6b0da3e06ce15862 Author: murm Date: Wed Jun 22 20:07:14 2022 -0400 Initial Funny 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) +}