diff --git a/build.sh b/build.sh index 67b9fac..dfa8a29 100755 --- a/build.sh +++ b/build.sh @@ -30,4 +30,4 @@ clear fancy "\033[1;105m" "Building Fulgens..." go build --ldflags "-s -w" -o "$path/fulgens" || exit 1 clear -fancy "\033[1;102m" "Fulgensfas has been built successfully!" \ No newline at end of file +fancy "\033[1;102m" "Fulgens has been built successfully!" \ No newline at end of file diff --git a/config.yaml.example b/config.yaml.example index 72c620d..d8ea71a 100644 --- a/config.yaml.example +++ b/config.yaml.example @@ -4,10 +4,6 @@ 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. @@ -19,13 +15,6 @@ global: { # 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 }, - # https defines the HTTPS settings on a global level - per-route settings override these. It is optional. - https: { - # certificate defines the path to the certificate file (must be a wildcard in order to support multiple subdomains). - certificate: "./certs/localhost.crt", - # key defines the path to the key file (must be a wildcard in order to support multiple subdomains). - key: "./certs/localhost.key" - }, # logging defines the logging settings. logging: { # enabled defines whether logging is enabled. @@ -41,6 +30,23 @@ global: { path: "./databases", # connectionString defines the connection string to use for the database (postgres only). connectionString: "postgres://user:password@localhost:5432/database" + }, + # stealth enables stealth mode, which makes the server look like some preset http servers. + # stealth mode overrides all proxy preservations and headers. + stealth: { + # enabled defines whether stealth mode is enabled. + enabled: true, + # server defines the server to pretend to be, possible values are "nginx" or "net/http". + server: "nginx", + # php defines if the server should pretend to be running PHP. This should only be used on nginx. + php: { + # enabled defines whether PHP spoofing is enabled. + enabled: true, + # version defines the version of PHP to pretend to be. + version: "8.2.25" + }, + # aspnet defines if the server should pretend to be running ASP.NET. This should only be used on nginx. + aspNet: true } } @@ -49,6 +55,8 @@ routes: [ { # none is a special subdomain that matches all requests without a subdomain (Host header). subdomain: "none", + # port defines the port to use for this route. They do not have to be unique. + port: "8080", # 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"] @@ -56,7 +64,11 @@ routes: [ { # any subdomain value that isn't "none" will match that specific subdomain. subdomain: "www.localhost", - # https defines the HTTPS settings for this route. + # port defines the port to use for this route. They do not have to be unique. + port: "8443", + # https defines the HTTPS settings for this route. If this block is missing, HTTPS will not be enabled for this port. + # If https is set once for any subdomain with this port, it will be enabled for all subdomains with this port. + # The connection will fail if the above condition is true, but there is not an HTTPS block for that subdomain. https: { # certificate defines the path to the certificate file. certificate: "./certs/localhost.crt", @@ -66,8 +78,8 @@ routes: [ # 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/*", + # paths defines the paths to match. They can contain wildcards. + paths: ["/static", "/static/*"], # static defines the static file serving settings for this path. This conflicts with proxy and redirect. # static > proxy > redirect in terms of precedence. static: { @@ -79,37 +91,41 @@ routes: [ } }, { - # path defines the path to match. They can contain wildcards. - path: "/proxy/*", + # paths defines the paths to match. They can contain wildcards. + paths: ["/proxy", "/proxy/*"], # proxy defines the proxy settings for this path. This conflicts with static and redirect. # static > proxy > redirect in terms of 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 + stripPrefix: true, headers: { # forbid defines the headers to forbid from being sent to the proxied server. - forbid: ["Authorization"], - # preserve defines the headers to preserve when sending to the client. - preserve: [X-Powered-By", "Server"] - # host defines whether the host / :authority header should be sent to the proxied server. - host: true, + forbid: [ "User-Agent" ], + # preserveServer defines whether to preserve the server header from the proxied server. + preserveServer: true, + # preserveAltSvc defines whether to preserve the Alt-Svc header from the proxied server. + preserveAltSvc: true, + # preserveXPoweredBy defines whether to preserve the X-Powered-By header from the proxied server. + preserveXPoweredBy: true, + # passHost defines whether the host / :authority header should be sent to the proxied server. + passHost: true, # xForward defines whether to send the X-Forwarded-For and X-Forwarded-Proto headers. - xForward: true + xForward: false } }, - { - # path defines the path to match. They can contain wildcards. - path: "/redirect/*", - # redirect defines the redirect settings for this path. This conflicts with proxy and static. - # static > proxy > redirect in terms of precedence. - redirect: { - # url defines the URL to redirect to. - url: "https://www.google.com", - # permanent defines whether the redirect is permanent (301) or temporary (302). - permanent: true - } + }, + { + # paths defines the paths to match. They can contain wildcards. + paths: ["/redirect", "/redirect/*"], + # redirect defines the redirect settings for this path. This conflicts with proxy and static. + # static > proxy > redirect in terms of precedence. + redirect: { + # url defines the URL to redirect to. + url: "https://www.ailur.dev", + # permanent defines whether the redirect is permanent (301) or temporary (302). + permanent: true } } ] diff --git a/main.go b/main.go index 7ef5978..906c615 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,10 @@ package main import ( + "fmt" library "git.ailur.dev/ailur/fg-library/v2" + "os/signal" + "syscall" "errors" "io" @@ -39,20 +42,11 @@ import ( type Config struct { Global struct { - 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 `yaml:"algorithm" validate:"omitempty,oneof=gzip brotli zstd"` - Level float64 `yaml:"level" validate:"omitempty,min=1,max=22"` - } `yaml:"compression"` - HTTPS struct { - CertificatePath string `yaml:"certificate" validate:"required"` - KeyPath string `yaml:"key" validate:"required"` - } `yaml:"https"` - Logging 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"` @@ -72,6 +66,7 @@ type Config struct { } } `yaml:"global" validate:"required"` Routes []struct { + Port string `yaml:"port" validate:"required"` Subdomain string `yaml:"subdomain" validate:"required"` Services []string `yaml:"services"` Paths []struct { @@ -94,10 +89,7 @@ type Config struct { CertificatePath string `yaml:"certificate" validate:"required"` KeyPath string `yaml:"key" validate:"required"` } `yaml:"https"` - Compression struct { - Algorithm string `yaml:"algorithm" validate:"omitempty,oneof=gzip brotli zstd"` - Level float64 `yaml:"level" validate:"omitempty,min=1,max=22"` - } `yaml:"compression"` + Compression CompressionSettings `yaml:"compression"` } `yaml:"routes"` Services map[string]interface{} `yaml:"services"` } @@ -119,22 +111,75 @@ type Service struct { } type CompressionSettings struct { - Level int - Algorithm string + Algorithm string `yaml:"algorithm" validate:"omitempty,oneof=gzip brotli zstd"` + Level float64 `yaml:"level" validate:"omitempty,min=1,max=22"` } -func checkCompressionAlgorithm(algorithm string, handler http.Handler, request *http.Request) http.Handler { - var compressionLevel int - compressionSettings, ok := compression[request.Host] +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) { + fmt.Println(subdomain) + pr.routers[subdomain] = RouterAndCompression{Router: router, Compression: compression} + fmt.Println(pr.routers) + 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 { - compressionLevel = int(config.Global.Compression.Level) - } else { - compressionLevel = compressionSettings.Level + fmt.Println(pr.routers) + router, ok = pr.routers["none"] } - switch algorithm { + 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: compressionLevel}) + encoder, err := gzip.New(gzip.Options{Level: int(settings.Level)}) if err != nil { slog.Error("Error creating gzip encoder: " + err.Error()) return handler @@ -146,7 +191,7 @@ func checkCompressionAlgorithm(algorithm string, handler http.Handler, request * } return gzipHandler(handler) case "brotli": - encoder, err := brotli.New(brotli.Options{Quality: compressionLevel}) + encoder, err := brotli.New(brotli.Options{Quality: int(settings.Level)}) if err != nil { slog.Error("Error creating brotli encoder: " + err.Error()) return handler @@ -158,7 +203,7 @@ func checkCompressionAlgorithm(algorithm string, handler http.Handler, request * } return brotliHandler(handler) case "zstd": - encoder, err := zstd.New(kpzstd.WithEncoderLevel(kpzstd.EncoderLevelFromZstd(compressionLevel))) + encoder, err := zstd.New(kpzstd.WithEncoderLevel(kpzstd.EncoderLevelFromZstd(int(settings.Level)))) if err != nil { slog.Error("Error creating zstd encoder: " + err.Error()) return handler @@ -470,35 +515,13 @@ func serverError(w http.ResponseWriter, status int) { } } -func hostRouter(w http.ResponseWriter, r *http.Request) { - host := strings.Split(r.Host, ":")[0] - router, ok := subdomains[host] - if !ok { - router, ok = subdomains["none"] - if !ok { - serverError(w, 404) - slog.Error("No subdomain found for " + host) - } - - } - - compressionSettings, ok := compression[host] - if !ok { - checkCompressionAlgorithm(config.Global.Compression.Algorithm, router, r).ServeHTTP(w, r) - } else { - checkCompressionAlgorithm(compressionSettings.Algorithm, router, r).ServeHTTP(w, r) - } -} - var ( 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) + portRouters = make(map[string]*PortRouter) ) func loadTLSCertificate(certificatePath, keyPath string) (*tls.Certificate, error) { @@ -510,19 +533,6 @@ func loadTLSCertificate(certificatePath, keyPath string) (*tls.Certificate, erro } } -func getTLSCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { - cert, ok := certificates[hello.ServerName] - if !ok { - if config.Global.HTTPS.CertificatePath == "" || config.Global.HTTPS.KeyPath == "" { - return nil, errors.New("no certificate found") - } else { - return certificates["none"], nil - } - } else { - return cert, nil - } -} - func svInit(message library.InterServiceMessage) { // Service database initialization message // Check if the service has the necessary permissions @@ -883,11 +893,11 @@ func parseConfig(path string) Config { func iterateThroughSubdomains(globalOutbox chan library.InterServiceMessage) { for _, route := range config.Routes { - if route.Compression.Level != 0 { - compression[route.Subdomain] = CompressionSettings{ - Level: int(route.Compression.Level), - Algorithm: route.Compression.Algorithm, - } + var compressionSettings CompressionSettings + if route.Compression != (CompressionSettings{}) { + compressionSettings = route.Compression + } else { + compressionSettings = config.Global.Compression } // Create the subdomain router @@ -896,9 +906,22 @@ func iterateThroughSubdomains(globalOutbox chan library.InterServiceMessage) { serverError(w, 404) }) - subdomains[route.Subdomain] = subdomainRouter - subdomains[route.Subdomain].Use(logger) - subdomains[route.Subdomain].Use(serverChanger) + _, 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 { @@ -953,15 +976,6 @@ func iterateThroughSubdomains(globalOutbox chan library.InterServiceMessage) { } } } - - // Add the TLS certificate - if route.HTTPS.CertificatePath != "" && route.HTTPS.KeyPath != "" { - certificate, err := loadTLSCertificate(route.HTTPS.CertificatePath, route.HTTPS.KeyPath) - if err != nil { - slog.Error("Error loading TLS certificate: " + err.Error() + ", TLS will not be available for subdomain " + route.Subdomain) - } - certificates[route.Subdomain] = certificate - } } } @@ -1075,16 +1089,6 @@ func main() { } } - // Check if the root TLS certificate exists - if config.Global.HTTPS.CertificatePath != "" && config.Global.HTTPS.KeyPath != "" { - certificate, err := loadTLSCertificate(config.Global.HTTPS.CertificatePath, config.Global.HTTPS.KeyPath) - if err != nil { - slog.Error("Error loading TLS certificate: " + err.Error() + ", TLS will not be available unless specified in the subdomains") - } - - certificates["none"] = certificate - } - // Walk through the service directory and load the plugins err := registerServices() if err != nil { @@ -1105,31 +1109,37 @@ func main() { // 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) - go func() { - // Create the TLS server - server := &http.Server{ - Addr: config.Global.IP + ":" + config.Global.HTTPSPort, - Handler: http.HandlerFunc(hostRouter), - TLSConfig: &tls.Config{ - GetCertificate: getTLSCertificate, - }, + // Start the servers + for port, router := range portRouters { + slog.Info("Starting server on port " + port) + 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) + }() } - - // Start the TLS server - err = server.ListenAndServeTLS("", "") - slog.Error("Error starting HTTPS server: " + err.Error()) - os.Exit(1) - }() - - // Start the HTTP server - err = http.ListenAndServe(config.Global.IP+":"+config.Global.HTTPPort, http.HandlerFunc(hostRouter)) - if err != nil { - slog.Error("Error starting server: " + err.Error()) - } else { - // This should never happen - slog.Error("Bit flip error: Impossible service ID. Move away from radiation or use ECC memory.") } - os.Exit(1) + + // Wait for a signal to stop the server + signalChannel := make(chan os.Signal, 1) + signal.Notify(signalChannel, os.Interrupt, syscall.SIGTERM) + <-signalChannel }