Fixed subdomain routers not working if the service is activated after the file servers, which always happened. Also, do not load services not specified. Also, switch to a yaml config.

Signed-off-by: arzumify <jliwin98@danwin1210.de>
This commit is contained in:
Tracker-Friendly 2024-11-03 11:58:33 +00:00
parent 7bc3ca8a37
commit d25e0a4877
6 changed files with 285 additions and 311 deletions

2
.gitignore vendored
View File

@ -5,5 +5,5 @@
/resources
/services
/services-src/eternity-web
/config.conf
/config.yaml
fulgens.log

View File

@ -1,120 +0,0 @@
// NOTE: This is NOT a valid JSON file.
// Comments are added here, which are stripped out by fulgens. This is not standard behavior for JSON and will only work with fulgens.
{
// Global configuration
"global": {
// IP defines the IP address to bind to.
"ip": "0.0.0.0",
// httpPort defines the port to bind to for HTTP.
"httpPort": "8080",
// httpsPort defines the port to bind to for HTTPS (TLS).
"httpsPort": "8443",
// serviceDirectory defines the directory to look for services in.
"serviceDirectory": "./services",
// resourceDirectory defines the directory to look for resources in.
"resourceDirectory": "./resources",
// compression defines the compression settings on a global level - per-route settings override these.
"compression": {
// algorithm defines the compression algorithm to use, possible values are "gzip", "brotli" and "zstd".
"algorithm": "gzip",
// level defines the compression level to use, possible values are 1-9 for gzip, 0-11 for brotli and 1-22 for zstd.
"level": 5
},
// logging defines the logging settings.
"logging": {
// enabled defines whether logging is enabled.
"enabled": true,
// file defines the file to log to, relative to the working directory.
"file": "fulgens.log"
},
// database defines the database settings.
"database": {
// type defines the type of database to use, possible values are "sqlite" and "postgres".
"type": "sqlite",
// path defines the path to the directory to store database files in (sqlite only).
"path": "./databases",
// connectionString defines the connection string to use for the database (postgres only).
"connectionString": "postgres://user:password@localhost:5432/database"
}
},
// Routes define per-subdomain routing settings.
"routes": [
{
// none is a special subdomain that matches all requests without a subdomain (Host header).
"subdomain": "none",
// services defines the services to use for this route. Services must be defined on a per-subdomain basis.
// Each service may not be used more than once globally. The server will fail to start if this is violated.
"services": ["authentication"]
},
{
// any subdomain value that isn't "none" will match that specific subdomain.
"subdomain": "www.localhost",
// https defines the HTTPS settings for this route.
"https": {
// certificate defines the path to the certificate file.
"certificate": "./certs/localhost.crt",
// key defines the path to the key file.
"key": "./certs/localhost.key"
},
// paths defines per-path settings (NOT for services, which MUST be defined on a per-subdomain basis).
"paths": [
{
// path defines the path to match. They can contain wildcards.
"path": "/static/*",
// static defines the static file serving settings for this path. This conflicts with proxy.
// If both proxy and static are defined, static will take precedence.
"static": {
// root defines the root directory to serve static files from.
"root": "./static",
// directoryListing defines whether to show a directory listing when a directory is requested.
// if it is false or unset, a 403 Forbidden will be returned instead.
"directoryListing": true
}
},
{
// path defines the path to match. They can contain wildcards.
"path": "/proxy/*",
// proxy defines the proxy settings for this path. This conflicts with static.
// If both proxy and static are defined, static will take precedence.
"proxy": {
// url defines the URL to proxy requests to.
"url": "http://localhost:8000",
// stripPrefix defines whether to strip the prefix from the path before proxying.
"stripPrefix": true
// TODO: In a future update, passing X-Forwarded-For and X-Forwarded-Proto headers will be supported.
// TODO: In a future update, forbidding certain headers from being passed will be supported.
// TODO: In a future update, passing X-Powered-By and Server headers will be supported.
// TODO: In a future update, passing Host header will be supported.
}
}
]
}
],
// Services define the settings for services.
"services": {
// authentication defines the settings for the authentication service, which is built-in.
"authentication": {
// privacyPolicy defines the URL to the privacy policy.
"privacyPolicy": "https://git.ailur.dev/Paperwork/nucleus/src/commit/5d191eea87cffae8bdca42017ac26dc19e6cb3de/Privacy.md",
// url defines the publicly-facing URL of the service, in case of it being behind a reverse proxy.
"url": "http://localhost:8000",
// identifier defines the identifier for the service, in the form of [Identifier] Accounts.
"identifier": "Authenticator",
// adminKey defines the key to use for administrative operations, such as listing all users.
"adminKey": "supersecretkey",
// testAppIsInternalApp defines whether the test app is an internal app, which allows it to bypass the user consent screen.
"testAppIsInternalApp": true,
// testAppEnabled defines whether the test app is enabled, which is recommended for testing purposes.
"testAppEnabled": true
},
// storage defines the settings for the storage service, which is built-in.
"storage": {
// path defines the path to store blobs in.
"path": "./blob",
// defaultQuota defines the default quota for users in bytes.
"defaultQuota": 50000000
},
}
}

118
config.yaml.example Normal file
View File

@ -0,0 +1,118 @@
# This is just YAML, but I decided to use JSON-like formatting because I like it better.
# Global configuration
global: {
# IP defines the IP address to bind to.
ip: "0.0.0.0",
# httpPort defines the port to bind to for HTTP.
httpPort: "8080",
# httpsPort defines the port to bind to for HTTPS (TLS).
httpsPort: "8443",
# serviceDirectory defines the directory to look for services in.
serviceDirectory: "./services",
# resourceDirectory defines the directory to look for resources in.
resourceDirectory: "./resources",
# compression defines the compression settings on a global level - per-route settings override these.
compression: {
# algorithm defines the compression algorithm to use, possible values are "gzip", "brotli" and "zstd".
algorithm: "gzip",
# level defines the compression level to use, possible values are 1-9 for gzip, 0-11 for brotli and 1-22 for zstd.
level: 5
},
# logging defines the logging settings.
logging: {
# enabled defines whether logging is enabled.
enabled: true,
# file defines the file to log to, relative to the working directory.
file: "fulgens.log"
},
# database defines the database settings.
database: {
# type defines the type of database to use, possible values are "sqlite" and "postgres".
type: "sqlite",
# path defines the path to the directory to store database files in (sqlite only).
path: "./databases",
# connectionString defines the connection string to use for the database (postgres only).
connectionString: "postgres://user:password@localhost:5432/database"
}
}
# Routes define per-subdomain routing settings.
routes: [
{
# none is a special subdomain that matches all requests without a subdomain (Host header).
subdomain: "none",
# services defines the services to use for this route. Services must be defined on a per-subdomain basis.
# Each service may not be used more than once globally. The server will fail to start if this is violated.
services: ["authentication"]
},
{
# any subdomain value that isn't "none" will match that specific subdomain.
subdomain: "www.localhost",
# https defines the HTTPS settings for this route.
https: {
# certificate defines the path to the certificate file.
certificate: "./certs/localhost.crt",
# key defines the path to the key file.
key: "./certs/localhost.key"
},
# paths defines per-path settings (NOT for services, which MUST be defined on a per-subdomain basis).
paths: [
{
# path defines the path to match. They can contain wildcards.
path: "/static/*",
# static defines the static file serving settings for this path. This conflicts with proxy.
# If both proxy and static are defined, static will take precedence.
static: {
# root defines the root directory to serve static files from.
root: "./static",
# directoryListing defines whether to show a directory listing when a directory is requested.
# if it is false or unset, a 403 Forbidden will be returned instead.
directoryListing: true
}
},
{
# path defines the path to match. They can contain wildcards.
path: "/proxy/*",
# proxy defines the proxy settings for this path. This conflicts with static.
# If both proxy and static are defined, static will take precedence.
proxy: {
# url defines the URL to proxy requests to.
url: "http://localhost:8000",
# stripPrefix defines whether to strip the prefix from the path before proxying.
stripPrefix: true
# TODO: In a future update, passing X-Forwarded-For and X-Forwarded-Proto headers will be supported.
# TODO: In a future update, forbidding certain headers from being passed will be supported.
# TODO: In a future update, passing X-Powered-By and Server headers will be supported.
# TODO: In a future update, passing Host header will be supported.
}
}
]
}
]
# Services define the settings for services.
services: {
# authentication defines the settings for the authentication service, which is built-in.
authentication: {
# privacyPolicy defines the URL to the privacy policy.
privacyPolicy: "https://git.ailur.dev/Paperwork/nucleus/src/commit/5d191eea87cffae8bdca42017ac26dc19e6cb3de/Privacy.md",
# url defines the publicly-facing URL of the service, in case of it being behind a reverse proxy.
url: "http://localhost:8000",
# identifier defines the identifier for the service, in the form of [Identifier] Accounts.
identifier: "Authenticator",
# adminKey defines the key to use for administrative operations, such as listing all users.
adminKey: "supersecretkey",
# testAppIsInternalApp defines whether the test app is an internal app, which allows it to bypass the user consent screen.
testAppIsInternalApp: true,
# testAppEnabled defines whether the test app is enabled, which is recommended for testing purposes.
testAppEnabled: true
},
# storage defines the settings for the storage service, which is built-in.
storage: {
# path defines the path to store blobs in.
path: "./blob",
# defaultQuota defines the default quota for users in bytes.
defaultQuota: 50000000
}
}

4
go.mod
View File

@ -6,7 +6,6 @@ require (
git.ailur.dev/ailur/fg-library/v2 v2.1.1
git.ailur.dev/ailur/fg-nucleus-library v1.0.3
git.ailur.dev/ailur/pow v1.0.2
github.com/BurntSushi/toml v1.4.0
github.com/andybalholm/brotli v1.1.1
github.com/cespare/xxhash/v2 v2.3.0
github.com/go-chi/chi/v5 v5.1.0
@ -17,15 +16,18 @@ require (
github.com/lib/pq v1.10.9
github.com/mattn/go-sqlite3 v1.14.24
golang.org/x/crypto v0.28.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/gabriel-vasile/mimetype v1.4.6 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/stretchr/testify v1.9.0 // indirect
golang.org/x/net v0.30.0 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/text v0.19.0 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
)

10
go.sum
View File

@ -4,8 +4,6 @@ git.ailur.dev/ailur/fg-nucleus-library v1.0.3 h1:C0xgfZg7bkULhh9Ci7ZoAcx4QIqxLh+
git.ailur.dev/ailur/fg-nucleus-library v1.0.3/go.mod h1:RbBVFRwtQgYvCWoru1mC3vUJ1dMftkNbvd7hVFtREFw=
git.ailur.dev/ailur/pow v1.0.2 h1:8tb6mXZdyQYjrKRW+AUmWMi5wJoHh9Ch3oRqiJr/ivs=
git.ailur.dev/ailur/pow v1.0.2/go.mod h1:fjFb1z5KtF6V14HRhGWiDmmJKggO8KyAP20Lr5OJI/g=
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
@ -30,6 +28,11 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
@ -50,5 +53,8 @@ golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

342
main.go
View File

@ -9,8 +9,6 @@ import (
"mime"
"os"
"plugin"
"regexp"
"sort"
"strconv"
"strings"
"sync"
@ -19,7 +17,6 @@ import (
"compress/gzip"
"crypto/tls"
"database/sql"
"encoding/json"
"log/slog"
"net/http"
"net/http/httputil"
@ -31,6 +28,7 @@ import (
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
"github.com/klauspost/compress/zstd"
"gopkg.in/yaml.v3"
_ "github.com/lib/pq"
_ "github.com/mattn/go-sqlite3"
@ -38,54 +36,55 @@ import (
type Config struct {
Global struct {
IP string `json:"ip" validate:"required,ip_addr"`
HTTPPort string `json:"httpPort" validate:"required"`
HTTPSPort string `json:"httpsPort" validate:"required"`
ServiceDirectory string `json:"serviceDirectory" validate:"required"`
ResourceDirectory string `json:"resourceDirectory" validate:"required"`
IP string `yaml:"ip" validate:"required,ip_addr"`
HTTPPort string `yaml:"httpPort" validate:"required"`
HTTPSPort string `yaml:"httpsPort" validate:"required"`
ServiceDirectory string `yaml:"serviceDirectory" validate:"required"`
ResourceDirectory string `yaml:"resourceDirectory" validate:"required"`
Compression struct {
Algorithm string `json:"algorithm" validate:"omitempty,oneof=gzip brotli zstd"`
Level float64 `json:"level" validate:"omitempty,min=1,max=22"`
} `json:"compression"`
Algorithm string `yaml:"algorithm" validate:"omitempty,oneof=gzip brotli zstd"`
Level float64 `yaml:"level" validate:"omitempty,min=1,max=22"`
} `yaml:"compression"`
Logging struct {
Enabled bool `json:"enabled"`
File string `json:"file" validate:"required_if=Enabled true"`
} `json:"logging"`
Enabled bool `yaml:"enabled"`
File string `yaml:"file" validate:"required_if=Enabled true"`
} `yaml:"logging"`
Database struct {
Type string `json:"type" validate:"required,oneof=sqlite postgres"`
ConnectionString string `json:"connectionString" validate:"required_if=Type postgres"`
Path string `json:"path" validate:"required_if=Type sqlite"`
} `json:"database" validate:"required"`
} `json:"global" validate:"required"`
Type string `yaml:"type" validate:"required,oneof=sqlite postgres"`
ConnectionString string `yaml:"connectionString" validate:"required_if=Type postgres"`
Path string `yaml:"path" validate:"required_if=Type sqlite"`
} `yaml:"database" validate:"required"`
} `yaml:"global" validate:"required"`
Routes []struct {
Subdomain string `json:"subdomain" validate:"required"`
Services []string `json:"services"`
Subdomain string `yaml:"subdomain" validate:"required"`
Services []string `yaml:"services"`
Paths []struct {
Path string `json:"path" validate:"required"`
Path string `yaml:"path" validate:"required"`
Proxy struct {
URL string `json:"url" validate:"required"`
StripPrefix bool `json:"stripPrefix"`
} `json:"proxy" validate:"required_without=Static"`
URL string `yaml:"url" validate:"required"`
StripPrefix bool `yaml:"stripPrefix"`
} `yaml:"proxy" validate:"required_without=Static"`
Static struct {
Root string `json:"root" validate:"required,isDirectory"`
DirectoryListing bool `json:"directoryListing"`
} `json:"static" validate:"required_without=Proxy"`
} `json:"paths"`
Root string `yaml:"root" validate:"required,isDirectory"`
DirectoryListing bool `yaml:"directoryListing"`
} `yaml:"static" validate:"required_without=Proxy"`
} `yaml:"paths"`
HTTPS struct {
CertificatePath string `json:"certificatePath" validate:"required"`
KeyPath string `json:"keyPath" validate:"required"`
} `json:"https"`
CertificatePath string `yaml:"certificatePath" validate:"required"`
KeyPath string `yaml:"keyPath" validate:"required"`
} `yaml:"https"`
Compression struct {
Algorithm string `json:"algorithm" validate:"omitempty,oneof=gzip brotli zstd"`
Level float64 `json:"level" validate:"omitempty,min=1,max=22"`
} `json:"compression"`
} `json:"routes"`
Services map[string]interface{} `json:"services"`
Algorithm string `yaml:"algorithm" validate:"omitempty,oneof=gzip brotli zstd"`
Level float64 `yaml:"level" validate:"omitempty,min=1,max=22"`
} `yaml:"compression"`
} `yaml:"routes"`
Services map[string]interface{} `yaml:"services"`
}
type Service struct {
ServiceID uuid.UUID
ServiceMetadata library.Service
ServiceMainFunc func(library.ServiceInitializationInformation)
Inbox chan library.InterServiceMessage
}
@ -475,14 +474,14 @@ func hostRouter(w http.ResponseWriter, r *http.Request) {
}
var (
validate *validator.Validate
services = make(map[uuid.UUID]Service)
lock sync.RWMutex
config Config
certificates = make(map[string]*tls.Certificate)
compression = make(map[string]CompressionSettings)
subdomains = make(map[string]*chi.Mux)
serviceSubdomains = make(map[string]string)
validate *validator.Validate
lock sync.RWMutex
config Config
registeredServices = make(map[string]Service)
activeServices = make(map[uuid.UUID]Service)
certificates = make(map[string]*tls.Certificate)
compression = make(map[string]CompressionSettings)
subdomains = make(map[string]*chi.Mux)
)
func loadTLSCertificate(certificatePath, keyPath string) (*tls.Certificate, error) {
@ -506,14 +505,14 @@ func getTLSCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
func svInit(message library.InterServiceMessage) {
// Service database initialization message
// Check if the service has the necessary permissions
if services[message.ServiceID].ServiceMetadata.Permissions.Database {
if activeServices[message.ServiceID].ServiceMetadata.Permissions.Database {
// Check if we are using sqlite or postgres
if config.Global.Database.Type == "sqlite" {
// Open the database and return the connection
pluginConn, err := sql.Open("sqlite3", filepath.Join(config.Global.Database.Path, message.ServiceID.String()+".db"))
if err != nil {
// Report an error
services[message.ServiceID].Inbox <- library.InterServiceMessage{
activeServices[message.ServiceID].Inbox <- library.InterServiceMessage{
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
ForServiceID: message.ServiceID,
MessageType: 1,
@ -522,7 +521,7 @@ func svInit(message library.InterServiceMessage) {
}
} else {
// Report a successful activation
services[message.ServiceID].Inbox <- library.InterServiceMessage{
activeServices[message.ServiceID].Inbox <- library.InterServiceMessage{
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
ForServiceID: message.ServiceID,
MessageType: 2,
@ -538,7 +537,7 @@ func svInit(message library.InterServiceMessage) {
conn, err := sql.Open("postgres", config.Global.Database.ConnectionString)
if err != nil {
// Report an error
services[message.ServiceID].Inbox <- library.InterServiceMessage{
activeServices[message.ServiceID].Inbox <- library.InterServiceMessage{
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
ForServiceID: message.ServiceID,
MessageType: 1,
@ -550,7 +549,7 @@ func svInit(message library.InterServiceMessage) {
_, err = conn.Exec("CREATE SCHEMA IF NOT EXISTS \"" + message.ServiceID.String() + "\"")
if err != nil {
// Report an error
services[message.ServiceID].Inbox <- library.InterServiceMessage{
activeServices[message.ServiceID].Inbox <- library.InterServiceMessage{
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
ForServiceID: message.ServiceID,
MessageType: 1,
@ -568,7 +567,7 @@ func svInit(message library.InterServiceMessage) {
pluginConn, err := sql.Open("postgres", connectionString)
if err != nil {
// Report an error
services[message.ServiceID].Inbox <- library.InterServiceMessage{
activeServices[message.ServiceID].Inbox <- library.InterServiceMessage{
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
ForServiceID: message.ServiceID,
MessageType: 1,
@ -580,7 +579,7 @@ func svInit(message library.InterServiceMessage) {
err = pluginConn.Ping()
if err != nil {
// Report an error
services[message.ServiceID].Inbox <- library.InterServiceMessage{
activeServices[message.ServiceID].Inbox <- library.InterServiceMessage{
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
ForServiceID: message.ServiceID,
MessageType: 1,
@ -589,7 +588,7 @@ func svInit(message library.InterServiceMessage) {
}
} else {
// Report a successful activation
services[message.ServiceID].Inbox <- library.InterServiceMessage{
activeServices[message.ServiceID].Inbox <- library.InterServiceMessage{
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
ForServiceID: message.ServiceID,
MessageType: 2,
@ -606,7 +605,7 @@ func svInit(message library.InterServiceMessage) {
}
} else {
// Report an error
services[message.ServiceID].Inbox <- library.InterServiceMessage{
activeServices[message.ServiceID].Inbox <- library.InterServiceMessage{
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
ForServiceID: message.ServiceID,
MessageType: 1,
@ -618,15 +617,15 @@ func svInit(message library.InterServiceMessage) {
func tryAuthAccess(message library.InterServiceMessage) {
// We need to check if the service is allowed to access the Authentication service
serviceMetadata, ok := services[message.ServiceID]
serviceMetadata, ok := activeServices[message.ServiceID]
if ok && serviceMetadata.ServiceMetadata.Permissions.Authenticate {
// Send message to Authentication service
service, ok := services[uuid.MustParse("00000000-0000-0000-0000-000000000004")]
service, ok := activeServices[uuid.MustParse("00000000-0000-0000-0000-000000000004")]
if ok {
service.Inbox <- message
} else if !ok {
// Send error message
service, ok := services[message.ServiceID]
service, ok := activeServices[message.ServiceID]
if ok {
service.Inbox <- library.InterServiceMessage{
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
@ -642,7 +641,7 @@ func tryAuthAccess(message library.InterServiceMessage) {
}
} else {
// Send error message
service, ok := services[message.ServiceID]
service, ok := activeServices[message.ServiceID]
if ok {
service.Inbox <- library.InterServiceMessage{
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
@ -659,7 +658,7 @@ func tryAuthAccess(message library.InterServiceMessage) {
}
} else {
// Send error message
service, ok := services[message.ServiceID]
service, ok := activeServices[message.ServiceID]
if ok {
service.Inbox <- library.InterServiceMessage{
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
@ -678,15 +677,15 @@ func tryAuthAccess(message library.InterServiceMessage) {
func tryStorageAccess(message library.InterServiceMessage) {
// We need to check if the service is allowed to access the Blob Storage service
serviceMetadata, ok := services[message.ServiceID]
serviceMetadata, ok := activeServices[message.ServiceID]
if ok && serviceMetadata.ServiceMetadata.Permissions.BlobStorage {
// Send message to Blob Storage service
service, ok := services[uuid.MustParse("00000000-0000-0000-0000-000000000003")]
service, ok := activeServices[uuid.MustParse("00000000-0000-0000-0000-000000000003")]
if ok {
service.Inbox <- message
} else if !ok {
// Send error message
service, ok := services[message.ServiceID]
service, ok := activeServices[message.ServiceID]
if ok {
service.Inbox <- library.InterServiceMessage{
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
@ -702,7 +701,7 @@ func tryStorageAccess(message library.InterServiceMessage) {
}
} else {
// Send error message
service, ok := services[message.ServiceID]
service, ok := activeServices[message.ServiceID]
if ok {
service.Inbox <- library.InterServiceMessage{
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
@ -719,7 +718,7 @@ func tryStorageAccess(message library.InterServiceMessage) {
}
} else {
// Send error message
service, ok := services[message.ServiceID]
service, ok := activeServices[message.ServiceID]
if ok {
service.Inbox <- library.InterServiceMessage{
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
@ -738,21 +737,21 @@ func tryStorageAccess(message library.InterServiceMessage) {
func tryLogger(message library.InterServiceMessage) {
// Logger service
service, ok := services[message.ServiceID]
service, ok := activeServices[message.ServiceID]
if ok {
switch message.MessageType {
case 0:
// Log message
slog.Info(service.ServiceMetadata.Name + " says: " + message.Message.(string))
slog.Info(strings.ToLower(service.ServiceMetadata.Name) + " says: " + message.Message.(string))
case 1:
// Warn message
slog.Warn(service.ServiceMetadata.Name + " warns: " + message.Message.(string))
slog.Warn(strings.ToLower(service.ServiceMetadata.Name) + " warns: " + message.Message.(string))
case 2:
// Error message
slog.Error(service.ServiceMetadata.Name + " complains: " + message.Message.(string))
slog.Error(strings.ToLower(service.ServiceMetadata.Name) + " complains: " + message.Message.(string))
case 3:
// Fatal message
slog.Error(service.ServiceMetadata.Name + "'s dying wish: " + message.Message.(string))
slog.Error(strings.ToLower(service.ServiceMetadata.Name) + "'s dying wish: " + message.Message.(string))
os.Exit(1)
}
}
@ -763,7 +762,7 @@ func processInterServiceMessage(channel chan library.InterServiceMessage) {
message := <-channel
if message.ForServiceID == uuid.MustParse("00000000-0000-0000-0000-000000000000") {
// Broadcast message
for _, service := range services {
for _, service := range activeServices {
service.Inbox <- message
}
} else if message.ForServiceID == uuid.MustParse("00000000-0000-0000-0000-000000000001") {
@ -772,7 +771,7 @@ func processInterServiceMessage(channel chan library.InterServiceMessage) {
case 0:
// This has been deprecated, ignore it
// Send "true" back
services[message.ServiceID].Inbox <- library.InterServiceMessage{
activeServices[message.ServiceID].Inbox <- library.InterServiceMessage{
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
ForServiceID: message.ServiceID,
MessageType: 0,
@ -789,13 +788,13 @@ func processInterServiceMessage(channel chan library.InterServiceMessage) {
} else if message.ForServiceID == uuid.MustParse("00000000-0000-0000-0000-000000000004") {
tryAuthAccess(message)
} else {
serviceMetadata, ok := services[message.ServiceID]
serviceMetadata, ok := activeServices[message.ServiceID]
if ok && serviceMetadata.ServiceMetadata.Permissions.InterServiceCommunication {
// Send message to specific service
service, ok := services[message.ForServiceID]
service, ok := activeServices[message.ForServiceID]
if !ok {
// Send error message
service, ok := services[message.ServiceID]
service, ok := activeServices[message.ServiceID]
if ok {
service.Inbox <- library.InterServiceMessage{
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
@ -813,7 +812,7 @@ func processInterServiceMessage(channel chan library.InterServiceMessage) {
service.Inbox <- message
} else {
// Send error message
service, ok := services[message.ServiceID]
service, ok := activeServices[message.ServiceID]
if ok {
service.Inbox <- library.InterServiceMessage{
ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"),
@ -854,16 +853,14 @@ func parseConfig(path string) Config {
}
// Parse the configuration file
configFile, err := os.ReadFile(path)
configFile, err := os.Open(path)
if err != nil {
slog.Error("Error reading configuration file: " + err.Error())
os.Exit(1)
}
// Parse the configuration file
var config Config
decoder := json.NewDecoder(strings.NewReader(string(regexp.MustCompile(`(?m)^\s*//.*`).ReplaceAll(configFile, []byte("")))))
decoder.UseNumber()
decoder := yaml.NewDecoder(configFile)
err = decoder.Decode(&config)
if err != nil {
slog.Error("Error parsing configuration file: " + err.Error())
@ -895,7 +892,7 @@ func parseConfig(path string) Config {
return config
}
func iterateThroughSubdomains() {
func iterateThroughSubdomains(globalOutbox chan library.InterServiceMessage) {
for _, route := range config.Routes {
var subdomainRouter *chi.Mux
// Create the subdomain router
@ -919,12 +916,19 @@ func iterateThroughSubdomains() {
if route.Services != nil {
// Iterate through the services
for _, service := range route.Services {
_, ok := serviceSubdomains[strings.ToLower(service)]
if !ok {
serviceSubdomains[strings.ToLower(service)] = route.Subdomain
// Check if the service is registered
registeredService, ok := registeredServices[service]
if ok {
// Check if the service is already active
_, ok := activeServices[registeredService.ServiceMetadata.ServiceID]
if ok {
slog.Error("Service with ID " + service + " is already active, will not activate again")
os.Exit(1)
}
// Initialize the service
initializeService(registeredService, globalOutbox, subdomainRouter)
} else {
slog.Error("Service " + service + " has multiple subdomains")
os.Exit(1)
slog.Warn("Service with ID " + service + " is not registered")
}
}
}
@ -963,81 +967,39 @@ func iterateThroughSubdomains() {
}
}
func initializeService(keys []time.Time, plugins map[time.Time]string, globalOutbox chan library.InterServiceMessage) {
for _, k := range keys {
// Get the plugin path
pluginPath := plugins[k]
func initializeService(service Service, globalOutbox chan library.InterServiceMessage, subdomainRouter *chi.Mux) {
// Get the plugin from the map
slog.Info("Activating service " + strings.ToLower(service.ServiceMetadata.Name) + " with ID " + service.ServiceMetadata.ServiceID.String())
// Load the plugin
servicePlugin, err := plugin.Open(pluginPath)
if err != nil {
slog.Error("Could not load service: " + err.Error())
os.Exit(1)
}
// Load the service information
serviceInformationSymbol, err := servicePlugin.Lookup("ServiceInformation")
if err != nil {
slog.Error("Service lacks necessary information: " + err.Error())
os.Exit(1)
}
serviceInformation := *serviceInformationSymbol.(*library.Service)
// Load the main function
main, err := servicePlugin.Lookup("Main")
if err != nil {
slog.Error("Service lacks necessary main function: " + err.Error())
os.Exit(1)
}
// Initialize the service
var inbox = make(chan library.InterServiceMessage)
lock.Lock()
services[serviceInformation.ServiceID] = Service{
ServiceID: serviceInformation.ServiceID,
Inbox: inbox,
ServiceMetadata: serviceInformation,
}
lock.Unlock()
slog.Info("Activating service " + serviceInformation.Name + " with ID " + serviceInformation.ServiceID.String())
serviceInitializationInformation := library.ServiceInitializationInformation{
Domain: serviceInformation.Name,
Configuration: config.Services[strings.ToLower(serviceInformation.Name)].(map[string]interface{}),
Outbox: globalOutbox,
Inbox: inbox,
}
// Make finalRouter a subdomain router if necessary
serviceSubdomain, ok := serviceSubdomains[strings.ToLower(serviceInformation.Name)]
if ok {
serviceInitializationInformation.Router = subdomains[serviceSubdomain]
} else {
if serviceInformation.ServiceID != uuid.MustParse("00000000-0000-0000-0000-000000000003") {
slog.Warn("Service " + serviceInformation.Name + " does not have a subdomain, it will not be served")
// Give it a blank router so it doesn't try to nil pointer dereference
serviceInitializationInformation.Router = chi.NewRouter()
}
}
// Check if they want a resource directory
if serviceInformation.Permissions.Resources {
serviceInitializationInformation.ResourceDir = os.DirFS(filepath.Join(config.Global.ResourceDirectory, serviceInformation.ServiceID.String()))
}
main.(func(library.ServiceInitializationInformation))(serviceInitializationInformation)
// Log the service activation
slog.Info("Service " + serviceInformation.Name + " activated with ID " + serviceInformation.ServiceID.String())
serviceInitializationInformation := library.ServiceInitializationInformation{
Domain: strings.ToLower(service.ServiceMetadata.Name),
Configuration: config.Services[strings.ToLower(service.ServiceMetadata.Name)].(map[string]interface{}),
Outbox: globalOutbox,
Inbox: service.Inbox,
Router: subdomainRouter,
}
// Check if they want a resource directory
if service.ServiceMetadata.Permissions.Resources {
serviceInitializationInformation.ResourceDir = os.DirFS(filepath.Join(config.Global.ResourceDirectory, service.ServiceMetadata.ServiceID.String()))
}
// Add the service to the active services
lock.Lock()
activeServices[service.ServiceMetadata.ServiceID] = service
lock.Unlock()
// Call the main function
service.ServiceMainFunc(serviceInitializationInformation)
// Log the service activation
slog.Info("Service " + strings.ToLower(service.ServiceMetadata.Name) + " activated with ID " + service.ServiceMetadata.ServiceID.String())
}
func main() {
// Parse the configuration file
if len(os.Args) < 2 {
info, err := os.Stat("config.conf")
info, err := os.Stat("config.yaml")
if err != nil {
if errors.Is(err, os.ErrNotExist) {
slog.Error("No configuration file provided")
@ -1053,7 +1015,7 @@ func main() {
os.Exit(1)
}
config = parseConfig("config.conf")
config = parseConfig("config.yaml")
} else {
config = parseConfig(os.Args[1])
}
@ -1067,18 +1029,7 @@ func main() {
}
}
// Iterate through the subdomains and create the routers as well as the compression levels and service maps
iterateThroughSubdomains()
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
go processInterServiceMessage(globalOutbox)
// Initialize all the services
plugins := make(map[time.Time]string)
// Walk through the service directory and load the plugins
err := filepath.Walk(config.Global.ServiceDirectory, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
@ -1088,36 +1039,53 @@ func main() {
return nil
}
// Add the plugin to the list of plugins
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
// Open the service
service, err := plugin.Open(path)
if err != nil {
return err
}
plugins[info.ModTime()] = path
// Load the service information
serviceInformation, err := service.Lookup("ServiceInformation")
if err != nil {
return errors.New("service lacks necessary information")
}
// Load the main function
mainFunc, err := service.Lookup("Main")
if err != nil {
return errors.New("service lacks necessary main function")
}
// Register the service
var inbox = make(chan library.InterServiceMessage, 1)
lock.Lock()
registeredServices[strings.ToLower(serviceInformation.(*library.Service).Name)] = Service{
ServiceID: serviceInformation.(*library.Service).ServiceID,
Inbox: inbox,
ServiceMetadata: *serviceInformation.(*library.Service),
ServiceMainFunc: mainFunc.(func(library.ServiceInitializationInformation)),
}
lock.Unlock()
// Log the service registration
slog.Info("Service " + strings.ToLower(serviceInformation.(*library.Service).Name) + " registered with ID " + serviceInformation.(*library.Service).ServiceID.String())
return nil
})
if err != nil {
slog.Error("Error walking the services directory: " + err.Error())
os.Exit(1)
}
var globalOutbox = make(chan library.InterServiceMessage, 1)
// Sort the plugins by modification time, newest last
var keys []time.Time
for k := range plugins {
keys = append(keys, k)
}
// 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
go processInterServiceMessage(globalOutbox)
sort.Slice(keys, func(i, j int) bool {
return keys[i].Before(keys[j])
})
// Start the storage service
initializeService(registeredServices["storage"], globalOutbox, nil)
initializeService(keys, plugins, globalOutbox)
// Iterate through the subdomains and create the routers as well as the compression levels and service maps
iterateThroughSubdomains(globalOutbox)
// Start the server
slog.Info("Starting server on " + config.Global.IP + " with ports " + config.Global.HTTPPort + " and " + config.Global.HTTPSPort)