forked from TripwireTeam/tripwire
Initial Funny
This commit is contained in:
commit
f8afdd8a77
10 changed files with 559 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
db.sqlite
|
||||
secrets.go
|
||||
tripwire
|
5
README.md
Normal file
5
README.md
Normal 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
255
db.go
Normal 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
150
endpoints.go
Normal 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
9
go.mod
Normal 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
6
go.sum
Normal 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
27
main.go
Normal 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
6
secrets.go.example
Normal file
|
@ -0,0 +1,6 @@
|
|||
package main
|
||||
|
||||
func getAdminToken() string {
|
||||
adminToken := "admin token goes here"
|
||||
return adminToken
|
||||
}
|
54
types.go
Normal file
54
types.go
Normal 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
44
util.go
Normal 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)
|
||||
}
|
Loading…
Reference in a new issue