From d25e0a487774abdc5702db16ad87ef02ca592e33 Mon Sep 17 00:00:00 2001 From: arzumify Date: Sun, 3 Nov 2024 11:58:33 +0000 Subject: [PATCH] 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 --- .gitignore | 2 +- config.conf.example | 120 ---------------- config.yaml.example | 118 +++++++++++++++ go.mod | 4 +- go.sum | 10 +- main.go | 342 ++++++++++++++++++++------------------------ 6 files changed, 285 insertions(+), 311 deletions(-) delete mode 100644 config.conf.example create mode 100644 config.yaml.example diff --git a/.gitignore b/.gitignore index 3d82ed3..d3eeb85 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,5 @@ /resources /services /services-src/eternity-web -/config.conf +/config.yaml fulgens.log \ No newline at end of file diff --git a/config.conf.example b/config.conf.example deleted file mode 100644 index 5019a73..0000000 --- a/config.conf.example +++ /dev/null @@ -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 - }, - } -} \ No newline at end of file diff --git a/config.yaml.example b/config.yaml.example new file mode 100644 index 0000000..b8cebf2 --- /dev/null +++ b/config.yaml.example @@ -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 + } +} diff --git a/go.mod b/go.mod index 56272a4..55408c6 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index 36b34e0..057409b 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/main.go b/main.go index 24288da..a0a1cd5 100644 --- a/main.go +++ b/main.go @@ -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)