Big update and refactorment.
This commit is contained in:
52
server/cert/token_key.pem
Normal file
52
server/cert/token_key.pem
Normal file
@@ -0,0 +1,52 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
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
|
||||
IiE+wFlvZCuJrNxLuyp69ECKSqYQPg==
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -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.
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package confread
|
||||
|
||||
type ConfigStruct struct {
|
||||
Address string
|
||||
Secure bool
|
||||
CertFile string
|
||||
KeyFile string
|
||||
Interval int
|
||||
}
|
||||
132
server/src/modules/database/handlers.go
Normal file
132
server/src/modules/database/handlers.go
Normal 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
|
||||
}
|
||||
@@ -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.")
|
||||
}
|
||||
}
|
||||
|
||||
55
server/src/modules/database/statements.go
Normal file
55
server/src/modules/database/statements.go
Normal 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;`,
|
||||
}
|
||||
23
server/src/modules/encrypt/hmac.go
Normal file
23
server/src/modules/encrypt/hmac.go
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
package restapi
|
||||
|
||||
type infoResponse struct {
|
||||
Status int `json:"status"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
18
server/src/modules/timekeeper/timekeeper.go
Normal file
18
server/src/modules/timekeeper/timekeeper.go
Normal 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)
|
||||
}
|
||||
}
|
||||
47
server/src/modules/utilities/confread.go
Normal file
47
server/src/modules/utilities/confread.go
Normal 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
|
||||
}
|
||||
43
server/src/modules/utilities/structs.go
Normal file
43
server/src/modules/utilities/structs.go
Normal 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"`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
11
server/templates/ghostrunner.conf.template
Normal file
11
server/templates/ghostrunner.conf.template
Normal file
@@ -0,0 +1,11 @@
|
||||
[ghostserver]
|
||||
address =
|
||||
|
||||
admin_token =
|
||||
token_key_file =
|
||||
|
||||
secure =
|
||||
api_cert_file =
|
||||
api_key_file =
|
||||
|
||||
interval =
|
||||
Reference in New Issue
Block a user