Big update and refactorment.

This commit is contained in:
Daan Selen
2025-05-26 09:51:59 +02:00
parent c8d869508e
commit 42c0392a20
24 changed files with 757 additions and 91 deletions

52
server/cert/token_key.pem Normal file
View File

@@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
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
IiE+wFlvZCuJrNxLuyp69ECKSqYQPg==
-----END PRIVATE KEY-----

View File

@@ -1,8 +0,0 @@
[ghostserver]
address = 0.0.0.0:19070
secure = true
certfile = ./cert/cert.pem
keyfile = ./cert/key.pem
interval = 600

Binary file not shown.

View File

@@ -4,6 +4,7 @@ go 1.24.3
require (
github.com/gorilla/mux v1.8.1
github.com/mattn/go-sqlite3 v1.14.28
gopkg.in/ini.v1 v1.67.0
)

View File

@@ -2,6 +2,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=

View File

@@ -3,18 +3,31 @@ package main
import (
"flag"
"fmt"
"ghostrunner-server/modules/confread"
"ghostrunner-server/modules/database"
"ghostrunner-server/modules/restapi"
"ghostrunner-server/modules/timekeeper"
"ghostrunner-server/modules/utilities"
"log"
)
func main() {
// Begin by trying to find the configuration file.
confPtr := flag.String("conf", "./conf/ghostserver.conf", "Specify a config file location yourself. Relative to the program.")
config := confread.ReadConf(*confPtr)
cfgPtr := flag.String("conf", "./conf/ghostserver.conf", "Specify a config file location yourself. Relative to the program.")
cfg := utilities.ReadConf(*cfgPtr)
log.Println("Starting the API-Server backend.")
restapi.InitApiServer(config)
hmacKey, err := utilities.LoadHMACKey(cfg.TokenKeyFile)
if err != nil {
log.Println(utilities.ErrTag, err)
}
fmt.Scanln()
log.Println(utilities.InfoTag, "Starting the Sqlite3 database connection.")
database.InitSqlite(cfg.AdminToken, hmacKey)
log.Println(utilities.InfoTag, "Starting the API-Server backend.")
restapi.InitApiServer(cfg, hmacKey)
log.Println(utilities.InfoTag, "Components should have started.")
log.Println(utilities.InfoTag, "Letting TimeKeeper take over...")
log.Println(utilities.InfoTag, fmt.Sprintf("Interval set at: %d seconds.", cfg.Interval))
timekeeper.KeepTime(cfg.Interval)
}

View File

@@ -1,34 +0,0 @@
package confread
import (
"ghostrunner-server/modules/utilities"
"gopkg.in/ini.v1"
)
const (
configSection = "ghostserver"
)
func ReadConf(configPath string) ConfigStruct {
inidata, err := ini.Load(configPath)
utilities.HandleError(err, "Trying to load the ini config file!")
section := inidata.Section(configSection)
var config ConfigStruct
config.Address = section.Key("address").String()
utilities.HandleError(err, "Trying to parse apiport field into the struct!")
config.Secure, err = section.Key("secure").Bool()
utilities.HandleError(err, "Trying to parse https field into the struct!")
config.CertFile = section.Key("certfile").String()
config.KeyFile = section.Key("keyfile").String()
config.Interval, err = section.Key("interval").Int()
utilities.HandleError(err, "Trying to parse interval field into the struct!")
return config
}

View File

@@ -1,9 +0,0 @@
package confread
type ConfigStruct struct {
Address string
Secure bool
CertFile string
KeyFile string
Interval int
}

View File

@@ -0,0 +1,132 @@
package database
import (
"database/sql"
"encoding/json"
"errors"
"fmt"
"ghostrunner-server/modules/encrypt"
"ghostrunner-server/modules/utilities"
"log"
)
func insertAdminToken(token string, hmacKey []byte) error {
var adminTokenName string = "Self-Generated Admin Token"
hashedToken := encrypt.CreateHMAC(token, hmacKey)
_, err := db.Exec(declStat.AdminTokenCreate, adminTokenName, hashedToken)
if err != nil {
return fmt.Errorf("failed to insert admin token: %w", err)
}
return nil
}
func InsertToken(tokenName, securedToken string) error {
_, err := db.Exec(declStat.CreateToken, tokenName, securedToken)
return err
}
func RemoveToken(tokenName string) error {
var tokenID int
err := db.QueryRow(declStat.GetTokenID, tokenName).Scan(&tokenID)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return fmt.Errorf("token not found")
}
return fmt.Errorf("failed to retrieve token ID: %w", err)
}
if tokenID == 0 { //TRYING TO REMOVE THE ADMIN TOKEN! NOT ALLOWED!
return fmt.Errorf("not abiding the removal of the admin token, program resisted")
}
_, err = db.Exec(declStat.DeleteToken, tokenName)
if err != nil {
return fmt.Errorf("failed to delete token: %w", err)
}
return nil
}
func RetrieveTokens() []string {
rows, err := db.Query(declStat.RetrieveTokens)
if err != nil {
log.Println(utilities.ErrTag, err)
}
defer rows.Close()
var tokens []string
for rows.Next() {
var singleToken string
err = rows.Scan(&singleToken)
if err != nil {
log.Println(utilities.ErrTag, err)
}
tokens = append(tokens, singleToken)
}
return tokens
}
func InsertTask(name, command string, nodeids []string, date, status string) error {
pNodeIds, err := json.Marshal(nodeids)
if err != nil {
return err
}
_, err = db.Exec(declStat.CreateTask, name, command, string(pNodeIds), date, status)
if err != nil {
return fmt.Errorf("failed to create task: %w", err)
}
return nil
}
func RemoveTask(name string) error {
_, err := db.Exec(declStat.DeleteTask, name)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return fmt.Errorf("token not found")
}
return fmt.Errorf("failed to retrieve token ID: %w", err)
}
return nil
}
func RetrieveTasks() []utilities.TaskData {
rows, err := db.Query(declStat.ListAllTasks)
if err != nil {
log.Println("Query error:", err)
return nil
}
defer rows.Close()
var tasks []utilities.TaskData
for rows.Next() {
var task utilities.TaskData
var nodeidsStr string
err := rows.Scan(&task.Name, &task.Command, &nodeidsStr, &task.Creation, &task.Status)
if err != nil {
log.Println("Row scan error:", err)
continue
}
err = json.Unmarshal([]byte(nodeidsStr), &task.Nodeids)
if err != nil {
log.Println("Unmarshal error:", err)
continue
}
tasks = append(tasks, task)
}
if err := rows.Err(); err != nil {
log.Println("Rows error:", err)
}
return tasks
}

View File

@@ -1,5 +1,50 @@
package database
func InitSqlite() {
import (
"database/sql"
"errors"
"ghostrunner-server/modules/utilities"
"log"
_ "github.com/mattn/go-sqlite3"
)
const (
databaseDir = "./data"
fullDatabasePath = databaseDir + "/ghostserver.db"
)
var db *sql.DB
func InitSqlite(adminToken string, hmacKey []byte) {
utilities.CheckDatabaseRemnants(databaseDir, fullDatabasePath)
var err error
db, err = sql.Open("sqlite3", fullDatabasePath)
if err != nil {
log.Println(utilities.ErrTag, err)
}
_, err = db.Exec(declStat.SetupDatabase)
if err != nil {
log.Println(utilities.ErrTag, err)
}
err = db.Ping()
if err != nil {
log.Println(utilities.ErrTag, err)
}
var adminTokenID int
err = db.QueryRow("SELECT id FROM tokens WHERE id = '0'").Scan(&adminTokenID)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
log.Println(utilities.InfoTag, "No Admin token detected, inserting...")
insertAdminToken(adminToken, hmacKey)
} else {
log.Println(utilities.InfoTag, "Something else went wrong, doing nothing.")
}
} else {
log.Println(utilities.InfoTag, "An Admin token is already present, not re-inserting.")
}
}

View File

@@ -0,0 +1,55 @@
package database
type Statements struct {
SetupDatabase string
AdminTokenCreate string
GetTokenID string
CreateToken string
DeleteToken string
RetrieveTokens string
CreateTask string
DeleteTask string
ListAllTasks string
}
var declStat = Statements{
SetupDatabase: `
CREATE TABLE IF NOT EXISTS tokens (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
token TEXT UNIQUE NOT NULL
);
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL,
command TEXT NOT NULL,
nodeids TEXT NOT NULL,
creation TEXT NOT NULL,
status TEXT NOT NULL
);`,
AdminTokenCreate: `
INSERT INTO tokens (id, name, token)
VALUES (0, ?, ?)
ON CONFLICT (id) DO NOTHING;`,
GetTokenID: `
SELECT id FROM tokens WHERE name = ?`,
CreateToken: `
INSERT INTO tokens(name, token)
VALUES(?, ?);`,
DeleteToken: `
DELETE FROM tokens WHERE
name = ?;`,
RetrieveTokens: `
SELECT token FROM tokens`,
CreateTask: `
INSERT INTO tasks (name, command, nodeids, creation, status)
VALUES (?, ?, ?, ?, ?);`,
DeleteTask: `
DELETE FROM tasks WHERE name = ?;`,
ListAllTasks: `
Select name, command, nodeids, creation, status from tasks;`,
}

View File

@@ -0,0 +1,23 @@
package encrypt
import (
"crypto/hmac"
"crypto/sha512"
"encoding/base64"
"errors"
)
func CreateHMAC(token string, key []byte) string {
mac := hmac.New(sha512.New, key)
mac.Write([]byte(token))
return base64.StdEncoding.EncodeToString(mac.Sum(nil))
}
func ValidateHMAC(expectedHMAC, candToken string, key []byte) (bool, error) {
if len(candToken) == 0 {
return false, errors.New("candidate MAC is empty")
}
candMac := CreateHMAC(candToken, key)
return hmac.Equal([]byte(expectedHMAC), []byte(candMac)), nil
}

View File

@@ -1,14 +1,221 @@
package restapi
import (
"encoding/json"
"ghostrunner-server/modules/database"
"ghostrunner-server/modules/encrypt"
"ghostrunner-server/modules/utilities"
"log"
"net/http"
"time"
"github.com/gorilla/mux"
"slices"
)
func handleSchedule(w http.ResponseWriter, r *http.Request) {
action := mux.Vars(r)["action"]
const (
constCreationStatus string = "Created"
)
utilities.ConsoleLog("Funky Funky " + action)
func generalAuth(w http.ResponseWriter, securedCandidate string) bool {
tokens := database.RetrieveTokens()
if !slices.Contains(tokens, securedCandidate) {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return false
}
return true
}
func parseTokenAndAuth(w http.ResponseWriter, r *http.Request, hmacKey []byte) (utilities.TokenCreateBody, bool) {
var data utilities.TokenCreateBody
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
log.Println(utilities.ErrTag, "Decode error:", err)
http.Error(w, "Invalid request body", http.StatusBadRequest)
return data, false
}
if data.AuthToken == "" || data.Details.Name == "" {
log.Println("[ERROR] Missing required fields")
http.Error(w, "Missing required fields", http.StatusBadRequest)
return data, false
}
givenToken := data.AuthToken
securedCandidate := encrypt.CreateHMAC(givenToken, hmacKey)
return data, generalAuth(w, securedCandidate)
}
func parseTaskAndAuth(w http.ResponseWriter, r *http.Request, hmacKey []byte) (utilities.TaskBody, bool) {
var data utilities.TaskBody
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
log.Println(utilities.ErrTag, "Decode error:", err)
http.Error(w, "Invalid request body", http.StatusBadRequest)
return data, false
}
if data.AuthToken == "" || data.Details.Name == "" {
log.Println("[ERROR] Missing required fields")
http.Error(w, "Missing required fields", http.StatusBadRequest)
return data, false
}
givenToken := data.AuthToken
securedCandidate := encrypt.CreateHMAC(givenToken, hmacKey)
return data, generalAuth(w, securedCandidate)
}
/*
The following section portrains to Token creation and deletion.
*/
func createTokenHandler(hmacKey []byte) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
data, ok := parseTokenAndAuth(w, r, hmacKey)
if !ok {
return
}
token, err := createToken(data.Details.Name, hmacKey)
if err != nil {
log.Println(utilities.ErrTag, "createToken failed:", err)
http.Error(w, "Token creation failed", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(utilities.InfoResponse{
Status: http.StatusCreated,
Message: "Token Succesfully Created.",
Data: token,
})
}
}
func deleteTokenHandler(hmacKey []byte) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
data, ok := parseTokenAndAuth(w, r, hmacKey)
if !ok {
return
}
if err := deleteToken(data.Details.Name, hmacKey); err != nil {
log.Println(utilities.ErrTag, "deleteToken failed:", err)
http.Error(w, "Token deletion failed", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(utilities.InfoResponse{
Status: http.StatusOK,
Message: "Token Deleted Successfully",
})
}
}
func createToken(tokenName string, hmacKey []byte) (string, error) {
randomString := utilities.GenRandString(64)
securedString := encrypt.CreateHMAC(randomString, hmacKey)
if err := database.InsertToken(tokenName, securedString); err != nil {
return "", err
}
return randomString, nil
}
func deleteToken(candToken string, hmacKey []byte) error {
securedToken := encrypt.CreateHMAC(candToken, hmacKey)
return database.RemoveToken(securedToken)
}
/*
The following section portrains to Task creation and deletion.
*/
func createTaskHandler(hmacKey []byte) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
data, ok := parseTaskAndAuth(w, r, hmacKey)
if !ok {
return
}
if err := createTask(data.Details.Name, data.Details.Command, data.Details.Nodeids); err != nil {
log.Println(utilities.ErrTag, "createTask failed:", err)
http.Error(w, "Task creation failed", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(utilities.InfoResponse{
Status: http.StatusOK,
Message: "Task '" + data.Details.Name + "' Created Succesfully.",
})
}
}
func deleteTaskHandler(hmacKey []byte) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
data, ok := parseTaskAndAuth(w, r, hmacKey)
if !ok {
return
}
if err := deleteTask(data.Details.Name); err != nil {
log.Println(utilities.ErrTag, "createTask failed:", err)
http.Error(w, "Task deletion failed", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(utilities.InfoResponse{
Status: http.StatusOK,
Message: "Task '" + data.Details.Name + "' Deleted Succesfully.",
})
}
}
func listTasksHandler(hmacKey []byte) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "Missing Authorization header", http.StatusUnauthorized)
return
}
const prefix = "Bearer "
if len(authHeader) <= len(prefix) || authHeader[:len(prefix)] != prefix {
http.Error(w, "Invalid Authorization header format", http.StatusUnauthorized)
return
}
tokenCandidate := authHeader[len(prefix):]
securedCandidate := encrypt.CreateHMAC(tokenCandidate, hmacKey)
if !generalAuth(w, securedCandidate) {
return
}
data := database.RetrieveTasks()
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(utilities.InfoResponse{
Status: http.StatusOK,
Message: "Succesfully Retrieved Tasks",
Data: data,
})
}
}
func createTask(taskName, command string, nodeids []string) error {
creationDate := time.Now().Format("02-01-2006 15:04:05")
creationStatus := constCreationStatus
return database.InsertTask(taskName, command, nodeids, creationDate, creationStatus)
}
func deleteTask(taskName string) error {
return database.RemoveTask(taskName)
}

View File

@@ -2,8 +2,10 @@ package restapi
import (
"encoding/json"
"ghostrunner-server/modules/confread"
"errors"
"ghostrunner-server/modules/utilities"
"io"
"log"
"net/http"
"time"
@@ -19,43 +21,58 @@ func rootEndpointHandler(w http.ResponseWriter, r *http.Request) { // This endpo
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
utilities.ConsoleLog("ROOT HIT") //Comment out later, for debugging purposes
json.NewEncoder(w).Encode(infoResponse{
log.Println("Root HTTP API endpoint has been reached.") //Comment out later, for debugging purposes
json.NewEncoder(w).Encode(utilities.InfoResponse{
Status: http.StatusOK,
Message: defaultMessage,
})
}
func InitApiServer(cfg confread.ConfigStruct) {
rtr := createRouter()
func InitApiServer(cfg utilities.ConfigStruct, hmacKey []byte) {
rtr := createRouter(hmacKey)
srv := createServer(cfg, rtr)
// Following func can be goroutines.
go func() {
var err error
if cfg.Secure {
err = srv.ListenAndServeTLS(cfg.CertFile, cfg.KeyFile)
if utilities.StatPath(cfg.ApiCertFile) && utilities.StatPath(cfg.ApiKeyFile) {
err = srv.ListenAndServeTLS(cfg.ApiCertFile, cfg.ApiKeyFile)
} else {
err = errors.New("failed to find one or both certificate- and/or keyfile")
}
} else {
err = srv.ListenAndServe()
}
utilities.HandleError(err, "Initializing the HTTP REST API!")
if err != nil {
log.Println(utilities.ErrTag, err)
}
defer srv.Close()
}()
utilities.ConsoleLog("Successfully started the GhostServer goroutine.")
//utilities.ConsoleLog("Successfully started the GhostServer goroutine.")
}
func createRouter() *mux.Router {
func createRouter(hmacKey []byte) *mux.Router {
r := mux.NewRouter().StrictSlash(true)
r.HandleFunc("/", rootEndpointHandler).Methods("GET")
r.HandleFunc("/schedule/{action:register|deregister}", handleSchedule).Methods("POST")
r.HandleFunc("/token/create", createTokenHandler(hmacKey)).Methods("POST")
r.HandleFunc("/token/delete", deleteTokenHandler(hmacKey)).Methods("DELETE")
r.HandleFunc("/task/create", createTaskHandler(hmacKey)).Methods("POST")
r.HandleFunc("/task/delete", deleteTaskHandler(hmacKey)).Methods("DELETE")
r.HandleFunc("/task/list", listTasksHandler(hmacKey)).Methods("GET")
return r
}
func createServer(cfg confread.ConfigStruct, ghostHandler http.Handler) *http.Server {
func createServer(cfg utilities.ConfigStruct, ghostHandler http.Handler) *http.Server {
return &http.Server{
Addr: cfg.Address, // Specify the desired HTTPS port.
Handler: ghostHandler, // Specify the above created handler.
ReadTimeout: readWriteTimeout,
WriteTimeout: readWriteTimeout,
ErrorLog: log.New(io.Discard, "", 0),
}
}

View File

@@ -1,6 +0,0 @@
package restapi
type infoResponse struct {
Status int `json:"status"`
Message string `json:"message"`
}

View File

@@ -0,0 +1,18 @@
package timekeeper
import (
"ghostrunner-server/modules/utilities"
"log"
"time"
)
func KeepTime(interval int) {
transInterval := time.Duration(interval) * time.Second
ticker := time.NewTicker(transInterval)
defer ticker.Stop()
for t := range ticker.C {
log.Println(utilities.InfoTag, "Tick at:", t)
}
}

View File

@@ -0,0 +1,47 @@
package utilities
import (
"log"
"gopkg.in/ini.v1"
)
const (
configSection = "ghostserver"
)
func ReadConf(configPath string) ConfigStruct {
inidata, err := ini.Load(configPath)
if err != nil {
log.Println(ErrTag, err)
}
section := inidata.Section(configSection)
var config ConfigStruct
// System
config.Address = section.Key("address").String()
// Authentication
config.AdminToken = section.Key("admin_token").String()
config.TokenKeyFile = section.Key("token_key_file").String()
// Protocol
config.Secure, err = section.Key("secure").Bool()
if err != nil {
log.Println(ErrTag, err)
}
// API Protocol Certificate
config.ApiCertFile = section.Key("api_cert_file").String()
config.ApiKeyFile = section.Key("api_key_file").String()
// Service Configuration
config.Interval, err = section.Key("interval").Int()
if err != nil {
log.Println(ErrTag, err)
}
return config
}

View File

@@ -0,0 +1,43 @@
package utilities
type ConfigStruct struct {
Address string
AdminToken string
TokenKeyFile string
Secure bool
ApiCertFile string
ApiKeyFile string
Interval int
}
type InfoResponse struct {
Status int `json:"status"`
Message string `json:"message"`
Data any `json:"data,omitempty"`
}
type TokenCreateDetails struct {
Name string `json:"name"`
}
type TokenCreateBody struct {
AuthToken string `json:"authtoken"`
Details TokenCreateDetails `json:"details"`
}
type TokenListBody struct {
AuthToken string `json:"authtoken"`
}
type TaskData struct {
Name string `json:"name"`
Command string `json:"command"`
Nodeids []string `json:"nodeids"`
Creation string `json:"creation"`
Status string `json:"status"`
}
type TaskBody struct {
AuthToken string `json:"authtoken"`
Details TaskData `json:"details"`
}

View File

@@ -1,24 +1,58 @@
package utilities
import (
"crypto/tls"
"crypto/rand"
"encoding/base64"
"fmt"
"log"
"os"
)
func HandleError(err error, task string) {
if err != nil {
log.Fatal("The program crashed unexpectedly while doing: "+task+"\nThe following exception occured:", err)
const (
InfoTag = "[INFO]"
WarnTag = "[WARN]"
ErrTag = "[ERROR]"
)
func CheckDatabaseRemnants(databaseDir, fullDatabasePath string) {
remnantDir := StatPath(databaseDir)
if !remnantDir {
log.Println(InfoTag, "Creating database folder...")
os.Mkdir(databaseDir, os.FileMode(0755))
}
remnantFile := StatPath(fullDatabasePath)
if !remnantFile {
log.Println(InfoTag, "Creating database file...")
os.Create(fullDatabasePath)
}
}
func ConsoleLog(message string) {
log.Println(message)
func GenRandString(size int) string {
randBytes := make([]byte, size)
_, err := rand.Read(randBytes)
if err != nil {
fmt.Println(err)
}
randString := base64.URLEncoding.EncodeToString(randBytes)
return randString
}
func LoadCertificate(certFile, keyFile string) tls.Certificate {
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
log.Fatal(err)
func StatPath(path string) bool {
_, err := os.Stat(path)
if err == nil {
return true
}
return cert
if os.IsNotExist(err) {
return false
}
return false
}
func LoadHMACKey(keyfile string) ([]byte, error) {
key, err := os.ReadFile(keyfile)
if err != nil {
return nil, fmt.Errorf("failed to read HMAC key: %w", err)
}
return key, nil
}

View File

@@ -0,0 +1,11 @@
[ghostserver]
address =
admin_token =
token_key_file =
secure =
api_cert_file =
api_key_file =
interval =