2024-09-28 19:41:34 +01:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2024-10-15 19:06:05 +01:00
|
|
|
library "git.ailur.dev/ailur/fg-library/v2"
|
2024-10-29 11:20:01 +00:00
|
|
|
|
|
|
|
"errors"
|
2024-09-28 19:41:34 +01:00
|
|
|
"io"
|
2024-10-20 19:51:16 +01:00
|
|
|
"io/fs"
|
2024-09-28 19:41:34 +01:00
|
|
|
"log"
|
|
|
|
"os"
|
|
|
|
"plugin"
|
|
|
|
"sort"
|
2024-10-04 18:30:17 +01:00
|
|
|
"strings"
|
2024-10-03 18:33:41 +01:00
|
|
|
"sync"
|
2024-09-28 19:41:34 +01:00
|
|
|
"time"
|
|
|
|
|
2024-10-29 11:20:01 +00:00
|
|
|
"compress/gzip"
|
2024-09-28 19:41:34 +01:00
|
|
|
"database/sql"
|
|
|
|
"encoding/json"
|
|
|
|
"log/slog"
|
|
|
|
"net/http"
|
|
|
|
"path/filepath"
|
|
|
|
|
2024-10-29 11:20:01 +00:00
|
|
|
"github.com/andybalholm/brotli"
|
2024-09-29 16:06:28 +01:00
|
|
|
"github.com/go-chi/chi/v5"
|
2024-10-15 18:29:16 +01:00
|
|
|
"github.com/go-chi/hostrouter"
|
2024-09-28 19:41:34 +01:00
|
|
|
"github.com/go-playground/validator/v10"
|
|
|
|
"github.com/google/uuid"
|
2024-10-29 11:20:01 +00:00
|
|
|
"github.com/klauspost/compress/zstd"
|
2024-09-28 19:41:34 +01:00
|
|
|
|
|
|
|
_ "github.com/lib/pq"
|
2024-10-20 18:39:20 +01:00
|
|
|
_ "github.com/mattn/go-sqlite3"
|
2024-09-28 19:41:34 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
type Config struct {
|
|
|
|
Global struct {
|
2024-10-29 11:20:01 +00:00
|
|
|
IP string `json:"ip" validate:"required,ip_addr"`
|
|
|
|
Port string `json:"port" validate:"required"`
|
|
|
|
ServiceDirectory string `json:"serviceDirectory" validate:"required"`
|
|
|
|
ResourceDirectory string `json:"resourceDirectory" validate:"required"`
|
|
|
|
Compression string `json:"compression" validate:"omitempty,oneof=gzip brotli zstd"`
|
|
|
|
CompressionLevelJN json.Number `json:"compressionLevel" validate:"required_with=Compression"`
|
|
|
|
CompressionLevel int
|
2024-09-28 19:41:34 +01:00
|
|
|
} `json:"global" validate:"required"`
|
|
|
|
Logging struct {
|
|
|
|
Enabled bool `json:"enabled"`
|
|
|
|
File string `json:"file" validate:"required_if=Enabled true"`
|
|
|
|
} `json:"logging"`
|
|
|
|
Database struct {
|
|
|
|
DatabaseType string `json:"databaseType" validate:"required,oneof=sqlite postgres"`
|
|
|
|
ConnectionString string `json:"connectionString" validate:"required_if=DatabaseType postgres"`
|
2024-10-13 19:20:19 +01:00
|
|
|
DatabasePath string `json:"databasePath" validate:"required_if=DatabaseType sqlite"`
|
2024-09-28 19:41:34 +01:00
|
|
|
} `json:"database" validate:"required"`
|
2024-10-24 19:27:25 +01:00
|
|
|
Static []struct {
|
|
|
|
Subdomain string `json:"subdomain"`
|
|
|
|
Directory string `json:"directory" validate:"required,isDirectory"`
|
|
|
|
Pattern string `json:"pattern"`
|
|
|
|
} `json:"static"`
|
2024-09-28 19:41:34 +01:00
|
|
|
Services map[string]interface{} `json:"services"`
|
|
|
|
}
|
|
|
|
|
2024-10-04 19:37:05 +01:00
|
|
|
type Service struct {
|
2024-10-15 19:17:29 +01:00
|
|
|
ServiceID uuid.UUID
|
|
|
|
ServiceMetadata library.Service
|
|
|
|
Inbox chan library.InterServiceMessage
|
2024-09-28 19:41:34 +01:00
|
|
|
}
|
|
|
|
|
2024-10-29 11:20:01 +00:00
|
|
|
type ResponseWriterWrapper struct {
|
|
|
|
http.ResponseWriter
|
|
|
|
io.Writer
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *ResponseWriterWrapper) WriteHeader(statusCode int) {
|
|
|
|
w.ResponseWriter.WriteHeader(statusCode)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *ResponseWriterWrapper) Write(p []byte) (int, error) {
|
|
|
|
return w.Writer.Write(p)
|
|
|
|
}
|
|
|
|
|
2024-09-28 19:41:34 +01:00
|
|
|
var (
|
|
|
|
logger = func(next http.Handler) http.Handler {
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
next.ServeHTTP(w, r)
|
|
|
|
slog.Info(r.Method + " " + r.URL.Path)
|
|
|
|
})
|
|
|
|
}
|
2024-10-24 19:39:45 +01:00
|
|
|
serverChanger = func(next http.Handler) http.Handler {
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
w.Header().Set("Server", "Fulgens HTTP Server")
|
2024-10-24 19:41:37 +01:00
|
|
|
next.ServeHTTP(w, r)
|
2024-10-24 19:39:45 +01:00
|
|
|
})
|
|
|
|
}
|
2024-10-29 11:20:01 +00:00
|
|
|
gzipHandler = func(next http.Handler) http.Handler {
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
|
|
|
gzipWriter, err := gzip.NewWriterLevel(w, config.Global.CompressionLevel)
|
|
|
|
if err != nil {
|
|
|
|
slog.Error("Error creating gzip writer: ", err)
|
|
|
|
next.ServeHTTP(w, r)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if w.Header().Get("Content-Encoding") != "" {
|
|
|
|
w.Header().Set("Content-Encoding", w.Header().Get("Content-Encoding")+", gzip")
|
|
|
|
} else {
|
|
|
|
w.Header().Set("Content-Encoding", "gzip")
|
|
|
|
}
|
|
|
|
defer func() {
|
|
|
|
w.Header().Del("Content-Length")
|
|
|
|
err := gzipWriter.Close()
|
|
|
|
if errors.Is(err, http.ErrBodyNotAllowed) {
|
|
|
|
// This is fine, all it means is that they have it cached, and we don't need to send it
|
|
|
|
return
|
|
|
|
} else if err != nil {
|
|
|
|
slog.Error("Error closing gzip writer: ", err)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
gzipResponseWriter := &ResponseWriterWrapper{ResponseWriter: w, Writer: gzipWriter}
|
|
|
|
next.ServeHTTP(gzipResponseWriter, r)
|
|
|
|
} else {
|
|
|
|
next.ServeHTTP(w, r)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
brotliHandler = func(next http.Handler) http.Handler {
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
if strings.Contains(r.Header.Get("Accept-Encoding"), "br") {
|
|
|
|
brotliWriter := brotli.NewWriterV2(w, config.Global.CompressionLevel)
|
|
|
|
if w.Header().Get("Content-Encoding") != "" {
|
|
|
|
w.Header().Set("Content-Encoding", w.Header().Get("Content-Encoding")+", br")
|
|
|
|
} else {
|
|
|
|
w.Header().Set("Content-Encoding", "br")
|
|
|
|
}
|
|
|
|
defer func() {
|
|
|
|
w.Header().Del("Content-Length")
|
|
|
|
err := brotliWriter.Close()
|
|
|
|
if errors.Is(err, http.ErrBodyNotAllowed) {
|
|
|
|
// This is fine, all it means is that they have it cached, and we don't need to send it
|
|
|
|
return
|
|
|
|
} else if err != nil {
|
|
|
|
slog.Error("Error closing Brotli writer: ", err)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
brotliResponseWriter := &ResponseWriterWrapper{ResponseWriter: w, Writer: brotliWriter}
|
|
|
|
next.ServeHTTP(brotliResponseWriter, r)
|
|
|
|
} else {
|
|
|
|
next.ServeHTTP(w, r)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
zStandardHandler = func(next http.Handler) http.Handler {
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
if strings.Contains(r.Header.Get("Accept-Encoding"), "zstd") {
|
|
|
|
zStandardWriter, err := zstd.NewWriter(w, zstd.WithEncoderLevel(zstd.EncoderLevelFromZstd(config.Global.CompressionLevel)))
|
|
|
|
if err != nil {
|
|
|
|
slog.Error("Error creating ZStandard writer: ", err)
|
|
|
|
next.ServeHTTP(w, r)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if w.Header().Get("Content-Encoding") != "" {
|
|
|
|
w.Header().Set("Content-Encoding", w.Header().Get("Content-Encoding")+", zstd")
|
|
|
|
} else {
|
|
|
|
w.Header().Set("Content-Encoding", "zstd")
|
|
|
|
}
|
|
|
|
defer func() {
|
|
|
|
w.Header().Del("Content-Length")
|
|
|
|
err := zStandardWriter.Close()
|
|
|
|
if err != nil {
|
|
|
|
if errors.Is(err, http.ErrBodyNotAllowed) {
|
|
|
|
// This is fine, all it means is that they have it cached, and we don't need to send it
|
|
|
|
return
|
|
|
|
} else {
|
|
|
|
slog.Error("Error closing ZStandard writer: ", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
gzipResponseWriter := &ResponseWriterWrapper{ResponseWriter: w, Writer: zStandardWriter}
|
|
|
|
next.ServeHTTP(gzipResponseWriter, r)
|
|
|
|
} else {
|
|
|
|
next.ServeHTTP(w, r)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
2024-10-15 19:06:05 +01:00
|
|
|
validate *validator.Validate
|
|
|
|
services = make(map[uuid.UUID]Service)
|
|
|
|
lock sync.RWMutex
|
|
|
|
hostRouter = hostrouter.New()
|
2024-10-29 11:20:01 +00:00
|
|
|
config Config
|
2024-09-28 19:41:34 +01:00
|
|
|
)
|
|
|
|
|
2024-10-29 11:20:01 +00:00
|
|
|
func processInterServiceMessage(channel chan library.InterServiceMessage) {
|
2024-09-28 19:41:34 +01:00
|
|
|
for {
|
|
|
|
message := <-channel
|
|
|
|
if message.ForServiceID == uuid.MustParse("00000000-0000-0000-0000-000000000000") {
|
|
|
|
// Broadcast message
|
2024-10-04 19:37:05 +01:00
|
|
|
for _, service := range services {
|
2024-10-15 19:17:29 +01:00
|
|
|
service.Inbox <- message
|
2024-09-28 19:41:34 +01:00
|
|
|
}
|
|
|
|
} else if message.ForServiceID == uuid.MustParse("00000000-0000-0000-0000-000000000001") {
|
|
|
|
// Service initialization service
|
|
|
|
switch message.MessageType {
|
|
|
|
case 0:
|
2024-10-15 19:17:29 +01:00
|
|
|
// This has been deprecated, ignore it
|
|
|
|
// Send "true" back
|
|
|
|
services[message.ServiceID].Inbox <- library.InterServiceMessage{
|
2024-09-28 19:41:34 +01:00
|
|
|
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
|
|
|
|
ForServiceID: message.ServiceID,
|
|
|
|
MessageType: 0,
|
|
|
|
SentAt: time.Now(),
|
|
|
|
Message: true,
|
|
|
|
}
|
|
|
|
case 1:
|
|
|
|
// Service database initialization message
|
|
|
|
// Check if the service has the necessary permissions
|
2024-10-04 19:37:05 +01:00
|
|
|
if services[message.ServiceID].ServiceMetadata.Permissions.Database {
|
2024-09-28 19:41:34 +01:00
|
|
|
// Check if we are using sqlite or postgres
|
|
|
|
if config.Database.DatabaseType == "sqlite" {
|
|
|
|
// Open the database and return the connection
|
2024-10-20 18:39:20 +01:00
|
|
|
pluginConn, err := sql.Open("sqlite3", filepath.Join(config.Database.DatabasePath, message.ServiceID.String()+".db"))
|
2024-09-28 19:41:34 +01:00
|
|
|
if err != nil {
|
|
|
|
// Report an error
|
2024-10-04 19:37:05 +01:00
|
|
|
services[message.ServiceID].Inbox <- library.InterServiceMessage{
|
2024-09-28 19:41:34 +01:00
|
|
|
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
|
|
|
|
ForServiceID: message.ServiceID,
|
|
|
|
MessageType: 1,
|
|
|
|
SentAt: time.Now(),
|
|
|
|
Message: err,
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Report a successful activation
|
2024-10-04 19:37:05 +01:00
|
|
|
services[message.ServiceID].Inbox <- library.InterServiceMessage{
|
2024-09-28 19:41:34 +01:00
|
|
|
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
|
|
|
|
ForServiceID: message.ServiceID,
|
|
|
|
MessageType: 2,
|
|
|
|
SentAt: time.Now(),
|
2024-10-13 19:20:19 +01:00
|
|
|
Message: library.Database{
|
|
|
|
DB: pluginConn,
|
|
|
|
DBType: library.Sqlite,
|
|
|
|
},
|
2024-09-28 19:41:34 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
} else if config.Database.DatabaseType == "postgres" {
|
|
|
|
// Connect to the database
|
|
|
|
conn, err := sql.Open("postgres", config.Database.ConnectionString)
|
|
|
|
if err != nil {
|
|
|
|
// Report an error
|
2024-10-04 19:37:05 +01:00
|
|
|
services[message.ServiceID].Inbox <- library.InterServiceMessage{
|
2024-09-28 19:41:34 +01:00
|
|
|
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
|
|
|
|
ForServiceID: message.ServiceID,
|
|
|
|
MessageType: 1,
|
|
|
|
SentAt: time.Now(),
|
|
|
|
Message: err,
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Try to create the schema
|
2024-10-13 19:20:19 +01:00
|
|
|
_, err = conn.Exec("CREATE SCHEMA IF NOT EXISTS \"" + message.ServiceID.String() + "\"")
|
2024-09-28 19:41:34 +01:00
|
|
|
if err != nil {
|
|
|
|
// Report an error
|
2024-10-04 19:37:05 +01:00
|
|
|
services[message.ServiceID].Inbox <- library.InterServiceMessage{
|
2024-09-28 19:41:34 +01:00
|
|
|
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
|
|
|
|
ForServiceID: message.ServiceID,
|
|
|
|
MessageType: 1,
|
|
|
|
SentAt: time.Now(),
|
|
|
|
Message: err,
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Create a new connection to the database
|
2024-10-13 19:20:19 +01:00
|
|
|
var connectionString string
|
|
|
|
if strings.Contains(config.Database.ConnectionString, "?") {
|
|
|
|
connectionString = config.Database.ConnectionString + "&search_path=\"" + message.ServiceID.String() + "\""
|
|
|
|
} else {
|
|
|
|
connectionString = config.Database.ConnectionString + "?search_path=\"" + message.ServiceID.String() + "\""
|
|
|
|
}
|
|
|
|
pluginConn, err := sql.Open("postgres", connectionString)
|
2024-09-28 19:41:34 +01:00
|
|
|
if err != nil {
|
|
|
|
// Report an error
|
2024-10-04 19:37:05 +01:00
|
|
|
services[message.ServiceID].Inbox <- library.InterServiceMessage{
|
2024-09-28 19:41:34 +01:00
|
|
|
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
|
|
|
|
ForServiceID: message.ServiceID,
|
|
|
|
MessageType: 1,
|
|
|
|
SentAt: time.Now(),
|
|
|
|
Message: err,
|
|
|
|
}
|
|
|
|
} else {
|
2024-10-14 12:23:33 +01:00
|
|
|
// Test the connection
|
|
|
|
err = pluginConn.Ping()
|
|
|
|
if err != nil {
|
|
|
|
// Report an error
|
|
|
|
services[message.ServiceID].Inbox <- library.InterServiceMessage{
|
|
|
|
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
|
|
|
|
ForServiceID: message.ServiceID,
|
|
|
|
MessageType: 1,
|
|
|
|
SentAt: time.Now(),
|
|
|
|
Message: err,
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Report a successful activation
|
|
|
|
services[message.ServiceID].Inbox <- library.InterServiceMessage{
|
|
|
|
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
|
|
|
|
ForServiceID: message.ServiceID,
|
|
|
|
MessageType: 2,
|
|
|
|
SentAt: time.Now(),
|
|
|
|
Message: library.Database{
|
|
|
|
DB: pluginConn,
|
|
|
|
DBType: library.Postgres,
|
|
|
|
},
|
|
|
|
}
|
2024-09-28 19:41:34 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Report an error
|
2024-10-04 19:37:05 +01:00
|
|
|
services[message.ServiceID].Inbox <- library.InterServiceMessage{
|
2024-09-28 19:41:34 +01:00
|
|
|
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
|
|
|
|
ForServiceID: message.ServiceID,
|
|
|
|
MessageType: 1,
|
|
|
|
SentAt: time.Now(),
|
|
|
|
Message: errors.New("database access not permitted"),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else if message.ForServiceID == uuid.MustParse("00000000-0000-0000-0000-000000000002") {
|
|
|
|
// Logger service
|
|
|
|
service, ok := services[message.ServiceID]
|
|
|
|
if ok {
|
2024-10-04 18:30:17 +01:00
|
|
|
switch message.MessageType {
|
|
|
|
case 0:
|
2024-09-28 19:41:34 +01:00
|
|
|
// Log message
|
2024-10-04 19:37:05 +01:00
|
|
|
slog.Info(service.ServiceMetadata.Name + " says: " + message.Message.(string))
|
2024-10-04 18:30:17 +01:00
|
|
|
case 1:
|
2024-09-28 19:41:34 +01:00
|
|
|
// Warn message
|
2024-10-04 19:37:05 +01:00
|
|
|
slog.Warn(service.ServiceMetadata.Name + " warns: " + message.Message.(string))
|
2024-10-04 18:30:17 +01:00
|
|
|
case 2:
|
2024-09-28 19:41:34 +01:00
|
|
|
// Error message
|
2024-10-04 19:37:05 +01:00
|
|
|
slog.Error(service.ServiceMetadata.Name + " complains: " + message.Message.(string))
|
2024-10-04 18:30:17 +01:00
|
|
|
case 3:
|
2024-09-28 19:41:34 +01:00
|
|
|
// Fatal message
|
2024-10-04 19:37:05 +01:00
|
|
|
slog.Error(service.ServiceMetadata.Name + "'s dying wish: " + message.Message.(string))
|
2024-09-28 19:41:34 +01:00
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else if message.ForServiceID == uuid.MustParse("00000000-0000-0000-0000-000000000003") {
|
|
|
|
// We need to check if the service is allowed to access the Blob Storage service
|
|
|
|
serviceMetadata, ok := services[message.ServiceID]
|
2024-10-04 19:37:05 +01:00
|
|
|
if ok && serviceMetadata.ServiceMetadata.Permissions.BlobStorage {
|
2024-09-28 19:41:34 +01:00
|
|
|
// Send message to Blob Storage service
|
2024-10-04 19:37:05 +01:00
|
|
|
service, ok := services[uuid.MustParse("00000000-0000-0000-0000-000000000003")]
|
2024-10-15 19:17:29 +01:00
|
|
|
if ok {
|
2024-09-28 19:41:34 +01:00
|
|
|
service.Inbox <- message
|
|
|
|
} else if !ok {
|
|
|
|
// Send error message
|
2024-10-04 19:37:05 +01:00
|
|
|
service, ok := services[message.ServiceID]
|
2024-09-28 19:41:34 +01:00
|
|
|
if ok {
|
|
|
|
service.Inbox <- library.InterServiceMessage{
|
|
|
|
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
|
|
|
|
ForServiceID: message.ServiceID,
|
|
|
|
MessageType: 1,
|
|
|
|
SentAt: time.Now(),
|
|
|
|
Message: errors.New("blob storage service not found"),
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// This should never happen
|
|
|
|
slog.Error("Bit flip error: Impossible service ID. Move away from radiation or use ECC memory.")
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Send error message
|
2024-10-04 19:37:05 +01:00
|
|
|
service, ok := services[message.ServiceID]
|
2024-09-28 19:41:34 +01:00
|
|
|
if ok {
|
|
|
|
service.Inbox <- library.InterServiceMessage{
|
|
|
|
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
|
|
|
|
ForServiceID: message.ServiceID,
|
|
|
|
MessageType: 1,
|
|
|
|
SentAt: time.Now(),
|
|
|
|
Message: errors.New("blob storage is not yet available"),
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// This should never happen
|
|
|
|
slog.Error("Bit flip error: Impossible service ID. Move away from radiation or use ECC memory.")
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Send error message
|
2024-10-04 19:37:05 +01:00
|
|
|
service, ok := services[message.ServiceID]
|
2024-09-28 19:41:34 +01:00
|
|
|
if ok {
|
|
|
|
service.Inbox <- library.InterServiceMessage{
|
|
|
|
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
|
|
|
|
ForServiceID: message.ServiceID,
|
|
|
|
MessageType: 1,
|
|
|
|
SentAt: time.Now(),
|
|
|
|
Message: errors.New("blob storage is not permitted"),
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// This should never happen
|
|
|
|
slog.Error("Bit flip error: Impossible service ID. Move away from radiation or use ECC memory.")
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else if message.ForServiceID == uuid.MustParse("00000000-0000-0000-0000-000000000004") {
|
|
|
|
// We need to check if the service is allowed to access the Authentication service
|
|
|
|
serviceMetadata, ok := services[message.ServiceID]
|
2024-10-04 19:37:05 +01:00
|
|
|
if ok && serviceMetadata.ServiceMetadata.Permissions.Authenticate {
|
2024-09-28 19:41:34 +01:00
|
|
|
// Send message to Authentication service
|
2024-10-04 19:37:05 +01:00
|
|
|
service, ok := services[uuid.MustParse("00000000-0000-0000-0000-000000000004")]
|
2024-10-15 19:17:29 +01:00
|
|
|
if ok {
|
2024-09-28 19:41:34 +01:00
|
|
|
service.Inbox <- message
|
|
|
|
} else if !ok {
|
|
|
|
// Send error message
|
2024-10-04 19:37:05 +01:00
|
|
|
service, ok := services[message.ServiceID]
|
2024-09-28 19:41:34 +01:00
|
|
|
if ok {
|
|
|
|
service.Inbox <- library.InterServiceMessage{
|
|
|
|
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
|
|
|
|
ForServiceID: message.ServiceID,
|
|
|
|
MessageType: 1,
|
|
|
|
SentAt: time.Now(),
|
|
|
|
Message: errors.New("authentication service not found"),
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// This should never happen
|
|
|
|
slog.Error("Bit flip error: Impossible service ID. Move away from radiation or use ECC memory.")
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Send error message
|
2024-10-04 19:37:05 +01:00
|
|
|
service, ok := services[message.ServiceID]
|
2024-09-28 19:41:34 +01:00
|
|
|
if ok {
|
|
|
|
service.Inbox <- library.InterServiceMessage{
|
|
|
|
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
|
|
|
|
ForServiceID: message.ServiceID,
|
|
|
|
MessageType: 1,
|
|
|
|
SentAt: time.Now(),
|
|
|
|
Message: errors.New("authentication service not yet available"),
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// This should never happen
|
|
|
|
slog.Error("Bit flip error: Impossible service ID. Move away from radiation or use ECC memory.")
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Send error message
|
2024-10-04 19:37:05 +01:00
|
|
|
service, ok := services[message.ServiceID]
|
2024-09-28 19:41:34 +01:00
|
|
|
if ok {
|
|
|
|
service.Inbox <- library.InterServiceMessage{
|
|
|
|
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
|
|
|
|
ForServiceID: message.ServiceID,
|
|
|
|
MessageType: 1,
|
|
|
|
SentAt: time.Now(),
|
|
|
|
Message: errors.New("authentication not permitted"),
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// This should never happen
|
|
|
|
slog.Error("Bit flip error: Impossible service ID. Move away from radiation or use ECC memory.")
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
serviceMetadata, ok := services[message.ServiceID]
|
2024-10-04 19:37:05 +01:00
|
|
|
if ok && serviceMetadata.ServiceMetadata.Permissions.InterServiceCommunication {
|
2024-09-28 19:41:34 +01:00
|
|
|
// Send message to specific service
|
2024-10-04 19:37:05 +01:00
|
|
|
service, ok := services[message.ForServiceID]
|
|
|
|
if !ok {
|
2024-09-28 19:41:34 +01:00
|
|
|
// Send error message
|
2024-10-04 19:37:05 +01:00
|
|
|
service, ok := services[message.ServiceID]
|
2024-09-28 19:41:34 +01:00
|
|
|
if ok {
|
|
|
|
service.Inbox <- library.InterServiceMessage{
|
|
|
|
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
|
|
|
|
ForServiceID: message.ServiceID,
|
|
|
|
MessageType: 1,
|
|
|
|
SentAt: time.Now(),
|
|
|
|
Message: errors.New("requested service not found"),
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// This should never happen
|
|
|
|
slog.Error("Bit flip error: Impossible service ID. Move away from radiation or use ECC memory.")
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
}
|
2024-10-04 19:37:05 +01:00
|
|
|
service.Inbox <- message
|
2024-09-28 19:41:34 +01:00
|
|
|
} else {
|
|
|
|
// Send error message
|
2024-10-04 19:37:05 +01:00
|
|
|
service, ok := services[message.ServiceID]
|
2024-09-28 19:41:34 +01:00
|
|
|
if ok {
|
|
|
|
service.Inbox <- library.InterServiceMessage{
|
|
|
|
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
|
|
|
|
ForServiceID: message.ServiceID,
|
|
|
|
MessageType: 1,
|
|
|
|
SentAt: time.Now(),
|
|
|
|
Message: errors.New("inter-service communication not permitted"),
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// This should never happen
|
|
|
|
slog.Error("Bit flip error: Impossible service ID. Move away from radiation or use ECC memory.")
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func parseConfig(path string) Config {
|
|
|
|
// Register the custom validators
|
|
|
|
validate = validator.New()
|
|
|
|
|
|
|
|
// Register the custom isDirectory validator
|
|
|
|
err := validate.RegisterValidation("isDirectory", func(fl validator.FieldLevel) bool {
|
|
|
|
// Check if it exists
|
|
|
|
fileInfo, err := os.Stat(fl.Field().String())
|
|
|
|
if err != nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check if it is a directory
|
|
|
|
return fileInfo.IsDir()
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
slog.Error("Error registering custom validator: ", err)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Parse the configuration file
|
2024-10-29 11:20:01 +00:00
|
|
|
configFile, err := os.Open(path)
|
2024-09-28 19:41:34 +01:00
|
|
|
if err != nil {
|
|
|
|
slog.Error("Error reading configuration file: ", err)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Parse the configuration file
|
|
|
|
var config Config
|
2024-10-29 11:20:01 +00:00
|
|
|
decoder := json.NewDecoder(configFile)
|
|
|
|
decoder.UseNumber()
|
|
|
|
err = decoder.Decode(&config)
|
2024-09-28 19:41:34 +01:00
|
|
|
if err != nil {
|
|
|
|
slog.Error("Error parsing configuration file: ", err)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
2024-10-29 11:20:01 +00:00
|
|
|
// Set the compression level
|
2024-10-29 12:51:04 +00:00
|
|
|
if config.Global.Compression != "" {
|
|
|
|
compressionLevelI64, err := config.Global.CompressionLevelJN.Int64()
|
|
|
|
if err != nil {
|
|
|
|
slog.Error("Error parsing compression level: ", err)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
config.Global.CompressionLevel = int(compressionLevelI64)
|
2024-10-29 11:20:01 +00:00
|
|
|
}
|
|
|
|
|
2024-09-28 19:41:34 +01:00
|
|
|
// Validate the configuration
|
|
|
|
err = validate.Struct(config)
|
|
|
|
if err != nil {
|
2024-10-04 19:37:05 +01:00
|
|
|
slog.Error("Invalid configuration: ", err)
|
2024-09-28 19:41:34 +01:00
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check if we are logging to a file
|
|
|
|
if config.Logging != (Config{}.Logging) && config.Logging.Enabled {
|
|
|
|
// Check if the log file is set
|
|
|
|
logFilePath := config.Logging.File
|
|
|
|
|
|
|
|
// Set the log file
|
|
|
|
logFile, err := os.OpenFile(logFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
|
|
|
if err != nil {
|
|
|
|
slog.Error("Error opening log file: ", err)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
|
|
|
log.SetOutput(io.MultiWriter(os.Stdout, logFile))
|
|
|
|
}
|
|
|
|
|
|
|
|
return config
|
|
|
|
}
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
// Parse the configuration file
|
|
|
|
if len(os.Args) < 2 {
|
|
|
|
info, err := os.Stat("config.json")
|
|
|
|
if err != nil {
|
|
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
|
|
slog.Error("No configuration file provided")
|
|
|
|
os.Exit(1)
|
|
|
|
} else {
|
|
|
|
slog.Error("Error reading configuration file: ", err)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if info.IsDir() {
|
|
|
|
slog.Error("No configuration file provided")
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
|
|
|
config = parseConfig("config.json")
|
|
|
|
} else {
|
|
|
|
config = parseConfig(os.Args[1])
|
|
|
|
}
|
|
|
|
|
2024-10-20 19:51:16 +01:00
|
|
|
// If we are using sqlite, create the database directory if it does not exist
|
|
|
|
if config.Database.DatabaseType == "sqlite" {
|
|
|
|
err := os.MkdirAll(config.Database.DatabasePath, 0755)
|
|
|
|
if err != nil {
|
|
|
|
slog.Error("Error creating database directory: ", err)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-09-28 19:41:34 +01:00
|
|
|
// Create the router
|
|
|
|
router := chi.NewRouter()
|
|
|
|
router.Use(logger)
|
2024-10-24 19:39:45 +01:00
|
|
|
router.Use(serverChanger)
|
2024-09-28 19:41:34 +01:00
|
|
|
|
2024-10-20 19:51:16 +01:00
|
|
|
// Iterate through the service configurations and create routers for each unique subdomain
|
|
|
|
subdomains := make(map[string]*chi.Mux)
|
|
|
|
for _, service := range config.Services {
|
|
|
|
if service.(map[string]interface{})["subdomain"] != nil {
|
|
|
|
subdomain := service.(map[string]interface{})["subdomain"].(string)
|
|
|
|
if subdomains[subdomain] == nil {
|
|
|
|
subdomains[subdomain] = chi.NewRouter()
|
2024-10-24 19:27:25 +01:00
|
|
|
slog.Info("Mapping subdomain " + subdomain)
|
2024-10-20 19:51:16 +01:00
|
|
|
hostRouter.Map(subdomain, subdomains[subdomain])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-24 19:27:25 +01:00
|
|
|
// Iterate through the static configurations and create routers for each unique subdomain
|
|
|
|
for _, static := range config.Static {
|
|
|
|
// Check if it wants a subdomain
|
|
|
|
if static.Subdomain != "" {
|
|
|
|
// Check if the subdomain exists
|
|
|
|
if subdomains[static.Subdomain] == nil {
|
|
|
|
subdomains[static.Subdomain] = chi.NewRouter()
|
|
|
|
slog.Info("Mapping subdomain " + static.Subdomain)
|
|
|
|
hostRouter.Map(static.Subdomain, subdomains[static.Subdomain])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-09-28 19:41:34 +01:00
|
|
|
var globalOutbox = make(chan library.InterServiceMessage)
|
|
|
|
|
|
|
|
// Initialize the service discovery, health-check, and logging services
|
|
|
|
// Since these are core services, always allocate them the service IDs 0, 1, and 2
|
|
|
|
// These are not dynamically loaded, as they are integral to the system functioning
|
2024-10-29 11:20:01 +00:00
|
|
|
go processInterServiceMessage(globalOutbox)
|
2024-09-28 19:41:34 +01:00
|
|
|
|
2024-10-04 18:30:17 +01:00
|
|
|
// Initialize all the services
|
2024-09-28 19:41:34 +01:00
|
|
|
plugins := make(map[time.Time]string)
|
2024-10-04 18:30:17 +01:00
|
|
|
err := filepath.Walk(config.Global.ServiceDirectory, func(path string, info os.FileInfo, err error) error {
|
2024-09-28 19:41:34 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2024-10-04 18:30:17 +01:00
|
|
|
if info.IsDir() || filepath.Ext(path) != ".fgs" {
|
2024-09-28 19:41:34 +01:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add the plugin to the list of plugins
|
2024-10-04 18:30:17 +01:00
|
|
|
if info.Name() == "storage.fgs" {
|
|
|
|
plugins[time.Unix(0, 0)] = path
|
|
|
|
return nil
|
|
|
|
} else if info.Name() == "auth.fgs" {
|
|
|
|
plugins[time.Unix(0, 1)] = path
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-09-28 19:41:34 +01:00
|
|
|
plugins[info.ModTime()] = path
|
|
|
|
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
slog.Error("Error walking the services directory: ", err)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Sort the plugins by modification time, newest last
|
|
|
|
var keys []time.Time
|
|
|
|
for k := range plugins {
|
|
|
|
keys = append(keys, k)
|
|
|
|
}
|
|
|
|
|
|
|
|
sort.Slice(keys, func(i, j int) bool {
|
|
|
|
return keys[i].Before(keys[j])
|
|
|
|
})
|
|
|
|
|
|
|
|
for _, k := range keys {
|
|
|
|
// Get the plugin path
|
|
|
|
pluginPath := plugins[k]
|
|
|
|
|
|
|
|
// Load the plugin
|
|
|
|
servicePlugin, err := plugin.Open(pluginPath)
|
|
|
|
if err != nil {
|
|
|
|
slog.Error("Could not load service: ", err)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Load the service information
|
2024-10-04 19:37:05 +01:00
|
|
|
serviceInformationSymbol, err := servicePlugin.Lookup("ServiceInformation")
|
2024-09-28 19:41:34 +01:00
|
|
|
if err != nil {
|
|
|
|
slog.Error("Service lacks necessary information: ", err)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
2024-10-04 19:37:05 +01:00
|
|
|
serviceInformation := *serviceInformationSymbol.(*library.Service)
|
|
|
|
|
2024-09-28 19:41:34 +01:00
|
|
|
// Load the main function
|
|
|
|
main, err := servicePlugin.Lookup("Main")
|
|
|
|
if err != nil {
|
|
|
|
slog.Error("Service lacks necessary main function: ", err)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Initialize the service
|
|
|
|
var inbox = make(chan library.InterServiceMessage)
|
2024-10-03 18:33:41 +01:00
|
|
|
lock.Lock()
|
2024-10-04 19:37:05 +01:00
|
|
|
services[serviceInformation.ServiceID] = Service{
|
2024-10-15 19:17:29 +01:00
|
|
|
ServiceID: serviceInformation.ServiceID,
|
|
|
|
Inbox: inbox,
|
|
|
|
ServiceMetadata: serviceInformation,
|
2024-09-28 19:41:34 +01:00
|
|
|
}
|
2024-10-03 18:33:41 +01:00
|
|
|
lock.Unlock()
|
2024-09-28 19:41:34 +01:00
|
|
|
|
2024-10-04 19:37:05 +01:00
|
|
|
slog.Info("Activating service " + serviceInformation.Name + " with ID " + serviceInformation.ServiceID.String())
|
2024-10-04 18:30:17 +01:00
|
|
|
|
2024-10-20 19:51:16 +01:00
|
|
|
// Make finalRouter a subdomain router if necessary
|
|
|
|
var finalRouter *chi.Mux
|
|
|
|
if config.Services[strings.ToLower(serviceInformation.Name)].(map[string]interface{})["subdomain"] != nil {
|
|
|
|
finalRouter = subdomains[config.Services[strings.ToLower(serviceInformation.Name)].(map[string]interface{})["subdomain"].(string)]
|
|
|
|
} else {
|
|
|
|
finalRouter = router
|
|
|
|
}
|
|
|
|
|
2024-09-28 19:41:34 +01:00
|
|
|
// Check if they want a resource directory
|
2024-10-20 19:51:16 +01:00
|
|
|
var resourceDir fs.FS = nil
|
2024-10-04 19:37:05 +01:00
|
|
|
if serviceInformation.Permissions.Resources {
|
2024-10-20 19:51:16 +01:00
|
|
|
resourceDir = os.DirFS(filepath.Join(config.Global.ResourceDirectory, serviceInformation.ServiceID.String()))
|
2024-09-28 19:41:34 +01:00
|
|
|
}
|
2024-10-04 18:30:17 +01:00
|
|
|
|
2024-10-20 19:51:16 +01:00
|
|
|
main.(func(library.ServiceInitializationInformation))(library.ServiceInitializationInformation{
|
|
|
|
Domain: serviceInformation.Name,
|
|
|
|
Configuration: config.Services[strings.ToLower(serviceInformation.Name)].(map[string]interface{}),
|
|
|
|
Outbox: globalOutbox,
|
|
|
|
Inbox: inbox,
|
|
|
|
ResourceDir: resourceDir,
|
|
|
|
Router: finalRouter,
|
|
|
|
})
|
|
|
|
|
2024-10-04 18:30:17 +01:00
|
|
|
// Log the service activation
|
2024-10-04 19:37:05 +01:00
|
|
|
slog.Info("Service " + serviceInformation.Name + " activated with ID " + serviceInformation.ServiceID.String())
|
2024-09-28 19:41:34 +01:00
|
|
|
}
|
|
|
|
|
2024-10-24 19:27:25 +01:00
|
|
|
// Mount the host router
|
|
|
|
router.Mount("/", hostRouter)
|
|
|
|
slog.Info("All subdomains mapped")
|
|
|
|
|
|
|
|
// Initialize the static file servers
|
|
|
|
for _, static := range config.Static {
|
|
|
|
if static.Subdomain != "" {
|
|
|
|
// Serve the static directory
|
|
|
|
if static.Pattern != "" {
|
|
|
|
subdomains[static.Subdomain].Handle(static.Pattern, http.FileServerFS(os.DirFS(static.Directory)))
|
|
|
|
slog.Info("Serving static directory " + static.Directory + " on subdomain " + static.Subdomain + " with pattern " + static.Pattern)
|
|
|
|
} else {
|
|
|
|
subdomains[static.Subdomain].Handle("/*", http.FileServerFS(os.DirFS(static.Directory)))
|
|
|
|
slog.Info("Serving static directory " + static.Directory + " on subdomain " + static.Subdomain)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Serve the static directory
|
|
|
|
if static.Pattern != "" {
|
|
|
|
router.Handle(static.Pattern, http.FileServerFS(os.DirFS(static.Directory)))
|
|
|
|
slog.Info("Serving static directory " + static.Directory + " with pattern " + static.Pattern)
|
|
|
|
} else {
|
|
|
|
router.Handle("/*", http.FileServerFS(os.DirFS(static.Directory)))
|
|
|
|
slog.Info("Serving static directory " + static.Directory)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-09-28 19:41:34 +01:00
|
|
|
// Start the server
|
2024-10-04 19:37:05 +01:00
|
|
|
slog.Info("Starting server on " + config.Global.IP + ":" + config.Global.Port)
|
2024-10-29 11:20:01 +00:00
|
|
|
switch config.Global.Compression {
|
|
|
|
case "":
|
|
|
|
err = http.ListenAndServe(config.Global.IP+":"+config.Global.Port, router)
|
|
|
|
case "gzip":
|
|
|
|
slog.Info("GZip compression enabled")
|
|
|
|
err = http.ListenAndServe(config.Global.IP+":"+config.Global.Port, gzipHandler(router))
|
|
|
|
case "brotli":
|
|
|
|
slog.Info("Brotli compression enabled")
|
|
|
|
err = http.ListenAndServe(config.Global.IP+":"+config.Global.Port, brotliHandler(router))
|
|
|
|
case "zstd":
|
|
|
|
slog.Info("ZStandard compression enabled")
|
|
|
|
err = http.ListenAndServe(config.Global.IP+":"+config.Global.Port, zStandardHandler(router))
|
|
|
|
}
|
2024-09-28 19:41:34 +01:00
|
|
|
if err != nil {
|
|
|
|
slog.Error("Error starting server: ", err)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
}
|