Added support for custom ports

Signed-off-by: arzumify <jliwin98@danwin1210.de>
This commit is contained in:
Tracker-Friendly 2024-11-14 19:48:39 +00:00
parent d04a40f655
commit 5145c65d04
3 changed files with 179 additions and 153 deletions

View File

@ -30,4 +30,4 @@ clear
fancy "\033[1;105m" "Building Fulgens..." fancy "\033[1;105m" "Building Fulgens..."
go build --ldflags "-s -w" -o "$path/fulgens" || exit 1 go build --ldflags "-s -w" -o "$path/fulgens" || exit 1
clear clear
fancy "\033[1;102m" "Fulgensfas has been built successfully!" fancy "\033[1;102m" "Fulgens has been built successfully!"

View File

@ -4,10 +4,6 @@
global: { global: {
# IP defines the IP address to bind to. # IP defines the IP address to bind to.
ip: "0.0.0.0", 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 defines the directory to look for services in.
serviceDirectory: "./services", serviceDirectory: "./services",
# resourceDirectory defines the directory to look for resources in. # 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 defines the compression level to use, possible values are 1-9 for gzip, 0-11 for brotli and 1-22 for zstd.
level: 5 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 defines the logging settings.
logging: { logging: {
# enabled defines whether logging is enabled. # enabled defines whether logging is enabled.
@ -41,6 +30,23 @@ global: {
path: "./databases", path: "./databases",
# connectionString defines the connection string to use for the database (postgres only). # connectionString defines the connection string to use for the database (postgres only).
connectionString: "postgres://user:password@localhost:5432/database" 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). # none is a special subdomain that matches all requests without a subdomain (Host header).
subdomain: "none", 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. # 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. # Each service may not be used more than once globally. The server will fail to start if this is violated.
services: ["authentication"] services: ["authentication"]
@ -56,7 +64,11 @@ routes: [
{ {
# any subdomain value that isn't "none" will match that specific subdomain. # any subdomain value that isn't "none" will match that specific subdomain.
subdomain: "www.localhost", 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: { https: {
# certificate defines the path to the certificate file. # certificate defines the path to the certificate file.
certificate: "./certs/localhost.crt", 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 defines per-path settings (NOT for services, which MUST be defined on a per-subdomain basis).
paths: [ paths: [
{ {
# path defines the path to match. They can contain wildcards. # paths defines the paths to match. They can contain wildcards.
path: "/static/*", paths: ["/static", "/static/*"],
# static defines the static file serving settings for this path. This conflicts with proxy and redirect. # static defines the static file serving settings for this path. This conflicts with proxy and redirect.
# static > proxy > redirect in terms of precedence. # static > proxy > redirect in terms of precedence.
static: { static: {
@ -79,37 +91,41 @@ routes: [
} }
}, },
{ {
# path defines the path to match. They can contain wildcards. # paths defines the paths to match. They can contain wildcards.
path: "/proxy/*", paths: ["/proxy", "/proxy/*"],
# proxy defines the proxy settings for this path. This conflicts with static and redirect. # proxy defines the proxy settings for this path. This conflicts with static and redirect.
# static > proxy > redirect in terms of precedence. # static > proxy > redirect in terms of precedence.
proxy: { proxy: {
# url defines the URL to proxy requests to. # url defines the URL to proxy requests to.
url: "http://localhost:8000", url: "http://localhost:8000",
# stripPrefix defines whether to strip the prefix from the path before proxying. # stripPrefix defines whether to strip the prefix from the path before proxying.
stripPrefix: true stripPrefix: true,
headers: { headers: {
# forbid defines the headers to forbid from being sent to the proxied server. # forbid defines the headers to forbid from being sent to the proxied server.
forbid: ["Authorization"], forbid: [ "User-Agent" ],
# preserve defines the headers to preserve when sending to the client. # preserveServer defines whether to preserve the server header from the proxied server.
preserve: [X-Powered-By", "Server"] preserveServer: true,
# host defines whether the host / :authority header should be sent to the proxied server. # preserveAltSvc defines whether to preserve the Alt-Svc header from the proxied server.
host: true, 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 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/*", # paths defines the paths to match. They can contain wildcards.
# redirect defines the redirect settings for this path. This conflicts with proxy and static. paths: ["/redirect", "/redirect/*"],
# static > proxy > redirect in terms of precedence. # redirect defines the redirect settings for this path. This conflicts with proxy and static.
redirect: { # static > proxy > redirect in terms of precedence.
# url defines the URL to redirect to. redirect: {
url: "https://www.google.com", # url defines the URL to redirect to.
# permanent defines whether the redirect is permanent (301) or temporary (302). url: "https://www.ailur.dev",
permanent: true # permanent defines whether the redirect is permanent (301) or temporary (302).
} permanent: true
} }
} }
] ]

246
main.go
View File

@ -1,7 +1,10 @@
package main package main
import ( import (
"fmt"
library "git.ailur.dev/ailur/fg-library/v2" library "git.ailur.dev/ailur/fg-library/v2"
"os/signal"
"syscall"
"errors" "errors"
"io" "io"
@ -39,20 +42,11 @@ import (
type Config struct { type Config struct {
Global struct { Global struct {
IP string `yaml:"ip" validate:"required,ip_addr"` IP string `yaml:"ip" validate:"required,ip_addr"`
HTTPPort string `yaml:"httpPort" validate:"required"` ServiceDirectory string `yaml:"serviceDirectory" validate:"required"`
HTTPSPort string `yaml:"httpsPort" validate:"required"` ResourceDirectory string `yaml:"resourceDirectory" validate:"required"`
ServiceDirectory string `yaml:"serviceDirectory" validate:"required"` Compression CompressionSettings `yaml:"compression"`
ResourceDirectory string `yaml:"resourceDirectory" validate:"required"` Logging struct {
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 {
Enabled bool `yaml:"enabled"` Enabled bool `yaml:"enabled"`
File string `yaml:"file" validate:"required_if=Enabled true"` File string `yaml:"file" validate:"required_if=Enabled true"`
} `yaml:"logging"` } `yaml:"logging"`
@ -72,6 +66,7 @@ type Config struct {
} }
} `yaml:"global" validate:"required"` } `yaml:"global" validate:"required"`
Routes []struct { Routes []struct {
Port string `yaml:"port" validate:"required"`
Subdomain string `yaml:"subdomain" validate:"required"` Subdomain string `yaml:"subdomain" validate:"required"`
Services []string `yaml:"services"` Services []string `yaml:"services"`
Paths []struct { Paths []struct {
@ -94,10 +89,7 @@ type Config struct {
CertificatePath string `yaml:"certificate" validate:"required"` CertificatePath string `yaml:"certificate" validate:"required"`
KeyPath string `yaml:"key" validate:"required"` KeyPath string `yaml:"key" validate:"required"`
} `yaml:"https"` } `yaml:"https"`
Compression struct { Compression CompressionSettings `yaml:"compression"`
Algorithm string `yaml:"algorithm" validate:"omitempty,oneof=gzip brotli zstd"`
Level float64 `yaml:"level" validate:"omitempty,min=1,max=22"`
} `yaml:"compression"`
} `yaml:"routes"` } `yaml:"routes"`
Services map[string]interface{} `yaml:"services"` Services map[string]interface{} `yaml:"services"`
} }
@ -119,22 +111,75 @@ type Service struct {
} }
type CompressionSettings struct { type CompressionSettings struct {
Level int Algorithm string `yaml:"algorithm" validate:"omitempty,oneof=gzip brotli zstd"`
Algorithm string Level float64 `yaml:"level" validate:"omitempty,min=1,max=22"`
} }
func checkCompressionAlgorithm(algorithm string, handler http.Handler, request *http.Request) http.Handler { type RouterAndCompression struct {
var compressionLevel int Router *chi.Mux
compressionSettings, ok := compression[request.Host] 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 { if !ok {
compressionLevel = int(config.Global.Compression.Level) fmt.Println(pr.routers)
} else { router, ok = pr.routers["none"]
compressionLevel = compressionSettings.Level
} }
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": case "gzip":
encoder, err := gzip.New(gzip.Options{Level: compressionLevel}) encoder, err := gzip.New(gzip.Options{Level: int(settings.Level)})
if err != nil { if err != nil {
slog.Error("Error creating gzip encoder: " + err.Error()) slog.Error("Error creating gzip encoder: " + err.Error())
return handler return handler
@ -146,7 +191,7 @@ func checkCompressionAlgorithm(algorithm string, handler http.Handler, request *
} }
return gzipHandler(handler) return gzipHandler(handler)
case "brotli": case "brotli":
encoder, err := brotli.New(brotli.Options{Quality: compressionLevel}) encoder, err := brotli.New(brotli.Options{Quality: int(settings.Level)})
if err != nil { if err != nil {
slog.Error("Error creating brotli encoder: " + err.Error()) slog.Error("Error creating brotli encoder: " + err.Error())
return handler return handler
@ -158,7 +203,7 @@ func checkCompressionAlgorithm(algorithm string, handler http.Handler, request *
} }
return brotliHandler(handler) return brotliHandler(handler)
case "zstd": 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 { if err != nil {
slog.Error("Error creating zstd encoder: " + err.Error()) slog.Error("Error creating zstd encoder: " + err.Error())
return handler 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 ( var (
validate *validator.Validate validate *validator.Validate
lock sync.RWMutex lock sync.RWMutex
config Config config Config
registeredServices = make(map[string]Service) registeredServices = make(map[string]Service)
activeServices = make(map[uuid.UUID]Service) activeServices = make(map[uuid.UUID]Service)
certificates = make(map[string]*tls.Certificate) portRouters = make(map[string]*PortRouter)
compression = make(map[string]CompressionSettings)
subdomains = make(map[string]*chi.Mux)
) )
func loadTLSCertificate(certificatePath, keyPath string) (*tls.Certificate, error) { 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) { func svInit(message library.InterServiceMessage) {
// Service database initialization message // Service database initialization message
// Check if the service has the necessary permissions // Check if the service has the necessary permissions
@ -883,11 +893,11 @@ func parseConfig(path string) Config {
func iterateThroughSubdomains(globalOutbox chan library.InterServiceMessage) { func iterateThroughSubdomains(globalOutbox chan library.InterServiceMessage) {
for _, route := range config.Routes { for _, route := range config.Routes {
if route.Compression.Level != 0 { var compressionSettings CompressionSettings
compression[route.Subdomain] = CompressionSettings{ if route.Compression != (CompressionSettings{}) {
Level: int(route.Compression.Level), compressionSettings = route.Compression
Algorithm: route.Compression.Algorithm, } else {
} compressionSettings = config.Global.Compression
} }
// Create the subdomain router // Create the subdomain router
@ -896,9 +906,22 @@ func iterateThroughSubdomains(globalOutbox chan library.InterServiceMessage) {
serverError(w, 404) serverError(w, 404)
}) })
subdomains[route.Subdomain] = subdomainRouter _, ok := portRouters[route.Port]
subdomains[route.Subdomain].Use(logger) if !ok {
subdomains[route.Subdomain].Use(serverChanger) 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 // Check the services
if route.Services != nil { 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 // Walk through the service directory and load the plugins
err := registerServices() err := registerServices()
if err != nil { 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 // Iterate through the subdomains and create the routers as well as the compression levels and service maps
iterateThroughSubdomains(globalOutbox) iterateThroughSubdomains(globalOutbox)
// Start the server // Start the servers
slog.Info("Starting server on " + config.Global.IP + " with ports " + config.Global.HTTPPort + " and " + config.Global.HTTPSPort) for port, router := range portRouters {
go func() { slog.Info("Starting server on port " + port)
// Create the TLS server if !router.HTTPSEnabled() {
server := &http.Server{ go func() {
Addr: config.Global.IP + ":" + config.Global.HTTPSPort, // Start the HTTP server
Handler: http.HandlerFunc(hostRouter), err = http.ListenAndServe(config.Global.IP+":"+port, logger(serverChanger(http.HandlerFunc(router.Router))))
TLSConfig: &tls.Config{ slog.Error("Error starting server: " + err.Error())
GetCertificate: getTLSCertificate, 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
} }