Initial Funny

This commit is contained in:
murm 2022-06-22 20:07:14 -04:00
commit f8afdd8a77
10 changed files with 559 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
db.sqlite
secrets.go
tripwire

5
README.md Normal file
View File

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

255
db.go Normal file
View File

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

150
endpoints.go Normal file
View File

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

9
go.mod Normal file
View File

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

6
go.sum Normal file
View File

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

27
main.go Normal file
View File

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

6
secrets.go.example Normal file
View File

@ -0,0 +1,6 @@
package main
func getAdminToken() string {
adminToken := "admin token goes here"
return adminToken
}

54
types.go Normal file
View File

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

44
util.go Normal file
View File

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