package main import ( library "git.ailur.dev/ailur/fg-library/v2" "os/signal" "syscall" "errors" "io" "log" "mime" "os" "plugin" "strconv" "strings" "sync" "time" "crypto/tls" "database/sql" "log/slog" "net/http" "net/http/httputil" "net/url" "path/filepath" "github.com/go-chi/chi/v5" "github.com/go-playground/validator/v10" "github.com/google/uuid" "gopkg.in/yaml.v3" "github.com/CAFxX/httpcompression" "github.com/CAFxX/httpcompression/contrib/andybalholm/brotli" "github.com/CAFxX/httpcompression/contrib/klauspost/gzip" "github.com/CAFxX/httpcompression/contrib/klauspost/zstd" kpzstd "github.com/klauspost/compress/zstd" _ "github.com/lib/pq" _ "github.com/mattn/go-sqlite3" ) type Config struct { Global struct { IP string `yaml:"ip" validate:"required,ip_addr"` ServiceDirectory string `yaml:"serviceDirectory" validate:"required"` ResourceDirectory string `yaml:"resourceDirectory" validate:"required"` Compression CompressionSettings `yaml:"compression"` Logging struct { Enabled bool `yaml:"enabled"` File string `yaml:"file" validate:"required_if=Enabled true"` } `yaml:"logging"` Database struct { 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"` Stealth struct { Enabled bool `yaml:"enabled"` Server string `yaml:"server" validate:"required_if=Enabled true"` PHP struct { Enabled bool `yaml:"enabled"` Version string `yaml:"version" validate:"required_if=Enabled true"` } `yaml:"php"` ASPNet bool `yaml:"aspNet"` } } `yaml:"global" validate:"required"` Routes []struct { Port string `yaml:"port" validate:"required"` Subdomain string `yaml:"subdomain" validate:"required"` Services []string `yaml:"services"` Paths []struct { Paths []string `yaml:"paths" validate:"required"` Proxy struct { URL string `yaml:"url" validate:"required"` StripPrefix bool `yaml:"stripPrefix"` Headers HeaderSettings `yaml:"headers"` } `yaml:"proxy" validate:"required_without=Static Redirect"` Static struct { Root string `yaml:"root" validate:"required,isDirectory"` DirectoryListing bool `yaml:"directoryListing"` } `yaml:"static" validate:"required_without_all=Proxy Redirect"` Redirect struct { URL string `yaml:"url" validate:"required"` Permanent bool `yaml:"permanent"` } `yaml:"redirect" validate:"required_without_all=Proxy Static"` } `yaml:"paths"` HTTPS struct { CertificatePath string `yaml:"certificate" validate:"required"` KeyPath string `yaml:"key" validate:"required"` } `yaml:"https"` Compression CompressionSettings `yaml:"compression"` } `yaml:"routes"` Services map[string]interface{} `yaml:"services"` } type HeaderSettings struct { Forbid []string `yaml:"forbid"` PreserveServer bool `yaml:"preserveServer"` PreserveXPoweredBy bool `yaml:"preserveXPoweredBy"` PreserveAltSvc bool `yaml:"preserveAltSvc"` PassHost bool `yaml:"passHost"` XForward bool `yaml:"xForward"` } type Service struct { ServiceID uuid.UUID ServiceMetadata library.Service ServiceMainFunc func(library.ServiceInitializationInformation) Inbox chan library.InterServiceMessage } type CompressionSettings struct { Algorithm string `yaml:"algorithm" validate:"omitempty,oneof=gzip brotli zstd"` Level int `yaml:"level" validate:"omitempty,min=1,max=22"` } type RouterAndCompression struct { Router *chi.Mux Compression CompressionSettings } type PortRouter struct { https struct { enabled bool httpSettings map[string]*tls.Certificate } routers map[string]RouterAndCompression } func NewPortRouter() *PortRouter { return &PortRouter{ routers: make(map[string]RouterAndCompression), https: struct { enabled bool httpSettings map[string]*tls.Certificate }{enabled: false, httpSettings: make(map[string]*tls.Certificate)}, } } func (pr *PortRouter) Register(router *chi.Mux, compression CompressionSettings, subdomain string, certificate ...*tls.Certificate) { pr.routers[subdomain] = RouterAndCompression{Router: router, Compression: compression} if len(certificate) > 0 { pr.https.enabled = true pr.https.httpSettings[subdomain] = certificate[0] } } func (pr *PortRouter) Router(w http.ResponseWriter, r *http.Request) { host := strings.Split(r.Host, ":")[0] router, ok := pr.routers[host] if !ok { router, ok = pr.routers["none"] } if router.Compression.Algorithm != "none" { compressRouter(router.Compression, router.Router).ServeHTTP(w, r) } else { router.Router.ServeHTTP(w, r) } } func (pr *PortRouter) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { cert, ok := pr.https.httpSettings[hello.ServerName] if !ok { return nil, errors.New("certificate not found") } else { return cert, nil } } func (pr *PortRouter) HTTPSEnabled() bool { return pr.https.enabled } func compressRouter(settings CompressionSettings, handler http.Handler) http.Handler { switch settings.Algorithm { case "gzip": encoder, err := gzip.New(gzip.Options{Level: int(settings.Level)}) if err != nil { slog.Error("Error creating gzip encoder: " + err.Error()) return handler } gzipHandler, err := httpcompression.Adapter(httpcompression.Compressor(gzip.Encoding, 0, encoder)) if err != nil { slog.Error("Error creating gzip handler: " + err.Error()) return handler } return gzipHandler(handler) case "brotli": encoder, err := brotli.New(brotli.Options{Quality: int(settings.Level)}) if err != nil { slog.Error("Error creating brotli encoder: " + err.Error()) return handler } brotliHandler, err := httpcompression.Adapter(httpcompression.Compressor(brotli.Encoding, 0, encoder)) if err != nil { slog.Error("Error creating brotli handler: " + err.Error()) return handler } return brotliHandler(handler) case "zstd": encoder, err := zstd.New(kpzstd.WithEncoderLevel(kpzstd.EncoderLevelFromZstd(int(settings.Level)))) if err != nil { slog.Error("Error creating zstd encoder: " + err.Error()) return handler } zstdHandler, err := httpcompression.Adapter(httpcompression.Compressor(zstd.Encoding, 0, encoder)) if err != nil { slog.Error("Error creating zstd handler: " + err.Error()) return handler } return zstdHandler(handler) default: return handler } } func logger(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) }) } func serverChanger(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !config.Global.Stealth.Enabled { if !strings.Contains("Server", w.Header().Get(":X-Preserve-Headers")) { w.Header().Set("Server", "Fulgens HTTP Server") } if !strings.Contains("X-Powered-By", w.Header().Get(":X-Preserve-Headers")) { w.Header().Set("X-Powered-By", "Go net/http") } if !strings.Contains("Alt-Svc", w.Header().Get(":X-Preserve-Headers")) { w.Header().Set("Alt-Svc", "h2=\":443\"; ma=3600") } } else { switch config.Global.Stealth.Server { case "nginx": w.Header().Set("Server", "nginx") } var poweredBy strings.Builder if config.Global.Stealth.PHP.Enabled { poweredBy.WriteString("PHP/" + config.Global.Stealth.PHP.Version) } if config.Global.Stealth.ASPNet { if poweredBy.Len() > 0 { poweredBy.WriteString(", ") } poweredBy.WriteString("ASP.NET") } if poweredBy.Len() > 0 { w.Header().Set("X-Powered-By", poweredBy.String()) } } next.ServeHTTP(w, r) }) } func listDirectory(w http.ResponseWriter, r *http.Request, root string, path string) { // Provide a directory listing w.WriteHeader(200) w.Header().Set("Content-Type", "text/html") _, err := w.Write([]byte("

Directory listing

")) if err != nil { serverError(w, 500) slog.Error("Error writing directory listing: " + err.Error()) return } } func parseEndRange(w http.ResponseWriter, file *os.File, end string) { endI64, err := strconv.ParseInt(end, 10, 64) if err != nil { serverError(w, 500) slog.Error("Error parsing range: " + err.Error()) return } _, err = file.Seek(-endI64, io.SeekEnd) if err != nil { serverError(w, 500) slog.Error("Error seeking file: " + err.Error()) return } _, err = io.Copy(w, file) if err != nil { serverError(w, 500) slog.Error("Error writing file: " + err.Error()) return } } func parseBeginningRange(w http.ResponseWriter, file *os.File, beginning string) { beginningI64, err := strconv.ParseInt(beginning, 10, 64) if err != nil { serverError(w, 500) slog.Error("Error parsing range: " + err.Error()) return } _, err = file.Seek(beginningI64, io.SeekStart) if err != nil { serverError(w, 500) slog.Error("Error seeking file: " + err.Error()) return } _, err = io.Copy(w, file) if err != nil { serverError(w, 500) slog.Error("Error writing file: " + err.Error()) return } } func parsePartRange(w http.ResponseWriter, file *os.File, beginning, end string) { beginningI64, err := strconv.ParseInt(beginning, 10, 64) if err != nil { serverError(w, 500) slog.Error("Error parsing range: " + err.Error()) return } endI64, err := strconv.ParseInt(end, 10, 64) if err != nil { serverError(w, 500) slog.Error("Error parsing range: " + err.Error()) return } _, err = file.Seek(beginningI64, io.SeekStart) if err != nil { serverError(w, 500) slog.Error("Error seeking file: " + err.Error()) return } _, err = io.CopyN(w, file, endI64-beginningI64) if err != nil { serverError(w, 500) slog.Error("Error writing file: " + err.Error()) return } } func newFileServer(root string, directoryListing bool, path string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { stat, err := os.Stat(filepath.Join(root, filepath.FromSlash(r.URL.Path))) if err != nil { serverError(w, 404) return } if stat.IsDir() { // See if index.html exists _, err := os.Stat(filepath.Join(root, filepath.FromSlash(r.URL.Path), "index.html")) if err != nil { if directoryListing { listDirectory(w, r, root, path) } else { serverError(w, 403) } return } else { // Serve the index.html file r.URL.Path = filepath.Join(r.URL.Path, "index.html") } } file, err := os.Open(filepath.Join(root, filepath.FromSlash(r.URL.Path))) if err != nil { serverError(w, 500) return } w.Header().Set("Content-Type", mime.TypeByExtension(filepath.Ext(r.URL.Path))) if strings.HasPrefix(r.Header.Get("Range"), "bytes=") { // Parse the range header. If there is an int-int, seek to the first int then return a limitedReader. // If there is an int-, seek to the first int and return the rest of the file. // If there is an -int, seek to the end of the file minus int and return the last int bytes. for _, item := range strings.Split(strings.TrimPrefix(r.Header.Get("Range"), "bytes="), ", ") { if strings.Contains(item, "-") { beginning := strings.Split(item, "-")[0] end := strings.Split(item, "-")[1] if beginning == "" { parseEndRange(w, file, end) } else if end == "" { parseBeginningRange(w, file, beginning) } else { parsePartRange(w, file, beginning, end) } } else { serverError(w, 416) return } } } else { _, err = io.Copy(w, file) if err != nil { serverError(w, 500) slog.Error("Error writing file: " + err.Error()) return } err = file.Close() if err != nil { slog.Error("Error closing file: " + err.Error()) } } }) } func newReverseProxy(uri *url.URL, headerSettings HeaderSettings) http.Handler { proxy := httputil.NewSingleHostReverseProxy(uri) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Strip the headers for _, header := range headerSettings.Forbid { r.Header.Del(header) } if !headerSettings.PassHost { r.Host = uri.Host } if !headerSettings.XForward { r.Header["X-Forwarded-For"] = nil } else { r.Header.Set("X-Forwarded-Host", r.Host) if r.URL.Scheme != "" { r.Header.Set("X-Forwarded-Proto", r.URL.Scheme) } else { r.Header.Set("X-Forwarded-Proto", "http") } } // Set the preserve headers which will be stripped by the server changer var xPreserveHeaders strings.Builder if headerSettings.PreserveServer { xPreserveHeaders.WriteString("Server") } if headerSettings.PreserveXPoweredBy { if xPreserveHeaders.Len() > 0 { xPreserveHeaders.WriteString(", ") } xPreserveHeaders.WriteString("X-Powered-By") } if headerSettings.PreserveAltSvc { if xPreserveHeaders.Len() > 0 { xPreserveHeaders.WriteString(", ") } xPreserveHeaders.WriteString("Alt-Svc") } w.Header().Set(":X-Preserve-Headers", xPreserveHeaders.String()) proxy.ServeHTTP(w, r) }) } func serverError(w http.ResponseWriter, status int) { w.Header().Set("Content-Type", "text/html") w.WriteHeader(status) if !config.Global.Stealth.Enabled { _, err := w.Write([]byte("

" + strconv.Itoa(status) + " " + http.StatusText(status) + "

Fulgens HTTP Server")) if err != nil { slog.Error("Error writing " + strconv.Itoa(status) + ": " + err.Error()) return } } else { switch config.Global.Stealth.Server { case "nginx": _, err := w.Write([]byte("" + strconv.Itoa(status) + " " + http.StatusText(status) + "\n\n

" + strconv.Itoa(status) + " " + http.StatusText(status) + "

\n
nginx/1.27.2
\n\n\n")) if err != nil { slog.Error("Error writing " + strconv.Itoa(status) + ": " + err.Error()) return } case "net/http": _, err := w.Write([]byte(strconv.Itoa(status) + " " + http.StatusText(status))) if err != nil { slog.Error("Error writing " + strconv.Itoa(status) + ": " + err.Error()) return } } } } var ( validate *validator.Validate lock sync.RWMutex config Config registeredServices = make(map[string]Service) activeServices = make(map[uuid.UUID]Service) portRouters = make(map[string]*PortRouter) ) func loadTLSCertificate(certificatePath, keyPath string) (*tls.Certificate, error) { certificate, err := tls.LoadX509KeyPair(certificatePath, keyPath) if err != nil { return nil, err } else { return &certificate, nil } } func svInit(message library.InterServiceMessage) { // Service database initialization message // Check if the service has the necessary permissions 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 activeServices[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 activeServices[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.Sqlite, }, } } } else if config.Global.Database.Type == "postgres" { // Connect to the database conn, err := sql.Open("postgres", config.Global.Database.ConnectionString) if err != nil { // Report an error activeServices[message.ServiceID].Inbox <- library.InterServiceMessage{ ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"), ForServiceID: message.ServiceID, MessageType: 1, SentAt: time.Now(), Message: err, } } else { // Try to create the schema _, err = conn.Exec("CREATE SCHEMA IF NOT EXISTS \"" + message.ServiceID.String() + "\"") if err != nil { // Report an error activeServices[message.ServiceID].Inbox <- library.InterServiceMessage{ 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 var connectionString string if strings.Contains(config.Global.Database.ConnectionString, "?") { connectionString = config.Global.Database.ConnectionString + "&search_path=\"" + message.ServiceID.String() + "\"" } else { connectionString = config.Global.Database.ConnectionString + "?search_path=\"" + message.ServiceID.String() + "\"" } pluginConn, err := sql.Open("postgres", connectionString) if err != nil { // Report an error activeServices[message.ServiceID].Inbox <- library.InterServiceMessage{ ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"), ForServiceID: message.ServiceID, MessageType: 1, SentAt: time.Now(), Message: err, } } else { // Test the connection err = pluginConn.Ping() if err != nil { // Report an error activeServices[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 activeServices[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, }, } } } } } } } else { // Report an error activeServices[message.ServiceID].Inbox <- library.InterServiceMessage{ ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"), ForServiceID: message.ServiceID, MessageType: 1, SentAt: time.Now(), Message: errors.New("database access not permitted"), } } } func tryAuthAccess(message library.InterServiceMessage) { // We need to check if the service is allowed to access the Authentication service serviceMetadata, ok := activeServices[message.ServiceID] if ok && serviceMetadata.ServiceMetadata.Permissions.Authenticate { // Send message to Authentication service service, ok := activeServices[uuid.MustParse("00000000-0000-0000-0000-000000000004")] if ok { service.Inbox <- message } else { // Send error message service, ok := activeServices[message.ServiceID] 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 service, ok := activeServices[message.ServiceID] 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) } } } func tryStorageAccess(message library.InterServiceMessage) { // We need to check if the service is allowed to access the Blob Storage service serviceMetadata, ok := activeServices[message.ServiceID] if ok && serviceMetadata.ServiceMetadata.Permissions.BlobStorage { // Send message to Blob Storage service service, ok := activeServices[uuid.MustParse("00000000-0000-0000-0000-000000000003")] if ok { service.Inbox <- message } else { // Send error message service, ok := activeServices[message.ServiceID] 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 service, ok := activeServices[message.ServiceID] 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) } } } func tryLogger(message library.InterServiceMessage) { // Logger service service, ok := activeServices[message.ServiceID] if ok { switch message.MessageType { case 0: // Log message slog.Info(strings.ToLower(service.ServiceMetadata.Name) + " says: " + message.Message.(string)) case 1: // Warn message slog.Warn(strings.ToLower(service.ServiceMetadata.Name) + " warns: " + message.Message.(string)) case 2: // Error message slog.Error(strings.ToLower(service.ServiceMetadata.Name) + " complains: " + message.Message.(string)) case 3: // Fatal message slog.Error(strings.ToLower(service.ServiceMetadata.Name) + "'s dying wish: " + message.Message.(string)) os.Exit(1) } } } func processInterServiceMessage(channel chan library.InterServiceMessage) { for { message := <-channel if message.ForServiceID == uuid.MustParse("00000000-0000-0000-0000-000000000000") { // Broadcast message for _, service := range activeServices { service.Inbox <- message } } else if message.ForServiceID == uuid.MustParse("00000000-0000-0000-0000-000000000001") { // Service initialization service switch message.MessageType { case 0: // This has been deprecated, ignore it // Send "true" back activeServices[message.ServiceID].Inbox <- library.InterServiceMessage{ ServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"), ForServiceID: message.ServiceID, MessageType: 0, SentAt: time.Now(), Message: true, } case 1: svInit(message) } } else if message.ForServiceID == uuid.MustParse("00000000-0000-0000-0000-000000000002") { tryLogger(message) } else if message.ForServiceID == uuid.MustParse("00000000-0000-0000-0000-000000000003") { tryStorageAccess(message) } else if message.ForServiceID == uuid.MustParse("00000000-0000-0000-0000-000000000004") { tryAuthAccess(message) } else { serviceMetadata, ok := activeServices[message.ServiceID] if ok && serviceMetadata.ServiceMetadata.Permissions.InterServiceCommunication { // Send message to specific service service, ok := activeServices[message.ForServiceID] if !ok { // Send error message service, ok := activeServices[message.ServiceID] 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) } } service.Inbox <- message } else { // Send error message service, ok := activeServices[message.ServiceID] 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.Error()) os.Exit(1) } // Parse the configuration file configFile, err := os.Open(path) if err != nil { slog.Error("Error reading configuration file: " + err.Error()) os.Exit(1) } // Parse the configuration file decoder := yaml.NewDecoder(configFile) err = decoder.Decode(&config) if err != nil { slog.Error("Error parsing configuration file: " + err.Error()) os.Exit(1) } // Validate the configuration err = validate.Struct(config) if err != nil { slog.Error("Invalid configuration: " + err.Error()) os.Exit(1) } // Check if we are logging to a file if config.Global.Logging != (Config{}.Global.Logging) && config.Global.Logging.Enabled { // Check if the log file is set logFilePath := config.Global.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.Error()) os.Exit(1) } log.SetOutput(io.MultiWriter(os.Stdout, logFile)) } return config } func iterateThroughSubdomains(globalOutbox chan library.InterServiceMessage) { for _, route := range config.Routes { var compressionSettings CompressionSettings if route.Compression != (CompressionSettings{}) { compressionSettings = route.Compression } else { compressionSettings = config.Global.Compression } // Create the subdomain router subdomainRouter := chi.NewRouter() subdomainRouter.NotFound(func(w http.ResponseWriter, r *http.Request) { serverError(w, 404) }) _, ok := portRouters[route.Port] if !ok { portRouters[route.Port] = NewPortRouter() } // Check if HTTPS is enabled if route.HTTPS.KeyPath != "" && route.HTTPS.CertificatePath != "" { certificate, err := loadTLSCertificate(route.HTTPS.CertificatePath, route.HTTPS.KeyPath) if err != nil { slog.Error("Error loading TLS certificate: " + err.Error()) os.Exit(1) } portRouters[route.Port].Register(subdomainRouter, compressionSettings, route.Subdomain, certificate) } else { portRouters[route.Port].Register(subdomainRouter, compressionSettings, route.Subdomain) } // Check the services if route.Services != nil { // Iterate through the services for _, service := range route.Services { // 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.Warn("Service with ID " + service + " is not registered") } } } // Iterate through the paths for _, pathBlock := range route.Paths { for _, path := range pathBlock.Paths { if pathBlock.Static.Root != "" { // Serve the static directory rawPath := strings.TrimSuffix(path, "*") subdomainRouter.Handle(path, http.StripPrefix(rawPath, newFileServer(pathBlock.Static.Root, pathBlock.Static.DirectoryListing, rawPath))) slog.Info("Serving static directory " + pathBlock.Static.Root + " on subdomain " + route.Subdomain + " with pattern " + path) } else if pathBlock.Proxy.URL != "" { // Create the proxy parsedURL, err := url.Parse(pathBlock.Proxy.URL) if err != nil { slog.Error("Error parsing URL: " + err.Error()) os.Exit(1) } if pathBlock.Proxy.StripPrefix { subdomainRouter.Handle(path, http.StripPrefix(strings.TrimSuffix(path, "*"), newReverseProxy(parsedURL, pathBlock.Proxy.Headers))) } else { subdomainRouter.Handle(path, newReverseProxy(parsedURL, pathBlock.Proxy.Headers)) } } else if pathBlock.Redirect.URL != "" { // Set the code code := http.StatusFound if pathBlock.Redirect.Permanent { code = http.StatusMovedPermanently } // Create the redirect subdomainRouter.Handle(path, http.RedirectHandler(pathBlock.Redirect.URL, code)) } } } } } func registerServices() (err error) { err = filepath.Walk(config.Global.ServiceDirectory, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if info.IsDir() || filepath.Ext(path) != ".fgs" { return nil } // Open the service service, err := plugin.Open(path) if err != nil { return err } // 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 }) return err } 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()) 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.yaml") 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.Error()) os.Exit(1) } } if info.IsDir() { slog.Error("No configuration file provided") os.Exit(1) } config = parseConfig("config.yaml") } else { config = parseConfig(os.Args[1]) } // If we are using sqlite, create the database directory if it does not exist if config.Global.Database.Type == "sqlite" { err := os.MkdirAll(config.Global.Database.Path, 0755) if err != nil { slog.Error("Error creating database directory: " + err.Error()) os.Exit(1) } } // Walk through the service directory and load the plugins err := registerServices() if err != nil { slog.Error("Error registering services: " + err.Error()) os.Exit(1) } var globalOutbox = make(chan library.InterServiceMessage, 1) // 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) // Start the storage service initializeService(registeredServices["storage"], globalOutbox, nil) // Iterate through the subdomains and create the routers as well as the compression levels and service maps iterateThroughSubdomains(globalOutbox) // Start the servers slog.Info("Starting servers") for port, router := range portRouters { if !router.HTTPSEnabled() { go func() { // Start the HTTP server err = http.ListenAndServe(config.Global.IP+":"+port, logger(serverChanger(http.HandlerFunc(router.Router)))) slog.Error("Error starting server: " + err.Error()) os.Exit(1) }() } else { // Create the TLS server server := &http.Server{ Addr: config.Global.IP + ":" + port, Handler: logger(serverChanger(http.HandlerFunc(router.Router))), TLSConfig: &tls.Config{ GetCertificate: router.GetCertificate, }, } go func() { // Start the TLS server err = server.ListenAndServeTLS("", "") slog.Error("Error starting HTTPS server: " + err.Error()) os.Exit(1) }() } } slog.Info("Servers started. Fulgens is now running. Press Ctrl+C to stop the server.") // Wait for a signal to stop the server signalChannel := make(chan os.Signal, 1) signal.Notify(signalChannel, os.Interrupt, syscall.SIGTERM) <-signalChannel }