From 71f0a0f8a061e0ae991daa357fde6c010aa7eb73 Mon Sep 17 00:00:00 2001 From: Ali Date: Fri, 17 Apr 2026 22:10:17 +0200 Subject: [PATCH 1/2] Upgrade emi version, some minor change to the http start --- go.mod | 2 +- go.sum | 6 +- modules/fireback/CoreUtils.go | 18 ++- .../EventBusSubscriptionAction.dyno.go | 2 +- modules/fireback/ReactiveSearchAction.dyno.go | 2 +- modules/fireback/VirtualDomains.go | 125 ++++++++++++++++++ modules/fireback/fireback-app.go | 2 + modules/fireback/httpServer.go | 100 ++++++++------ 8 files changed, 208 insertions(+), 49 deletions(-) create mode 100644 modules/fireback/VirtualDomains.go diff --git a/go.mod b/go.mod index 2b2ca2de3..fde3c9c1b 100644 --- a/go.mod +++ b/go.mod @@ -58,7 +58,7 @@ require ( github.com/tdewolff/minify/v2 v2.23.8 github.com/tidwall/gjson v1.18.0 github.com/tidwall/sjson v1.2.5 - github.com/torabian/emi v1.0.31 + github.com/torabian/emi v1.0.32 github.com/tus/tusd v1.10.0 github.com/urfave/cli v1.22.17 github.com/wk8/go-ordered-map/v2 v2.1.8 diff --git a/go.sum b/go.sum index ef0f05594..08c6b4047 100644 --- a/go.sum +++ b/go.sum @@ -750,10 +750,8 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= -github.com/torabian/emi v1.0.30 h1:rvUrSOR6HgaKJ6m7AW66cDfd3M+0O+T+1cxKivlC0YM= -github.com/torabian/emi v1.0.30/go.mod h1:jexKgoWHBqSBwWL6/pi2vOtDKFsXwud+2E7j8qKw8B4= -github.com/torabian/emi v1.0.31 h1:N6tGyme4y0DYRxKJUY8+Wa14M+s8QWUKWPEahfbRglw= -github.com/torabian/emi v1.0.31/go.mod h1:jexKgoWHBqSBwWL6/pi2vOtDKFsXwud+2E7j8qKw8B4= +github.com/torabian/emi v1.0.32 h1:OPBKhEgjsQtKjDcEVOzqZAaoyuqvGX+ljrCVSBjiMOk= +github.com/torabian/emi v1.0.32/go.mod h1:jexKgoWHBqSBwWL6/pi2vOtDKFsXwud+2E7j8qKw8B4= github.com/tus/tusd v1.10.0 h1:oQIxjxrSD6mjvYkqIjDlB3KVoEA1DWSzqCgWcTPP/ok= github.com/tus/tusd v1.10.0/go.mod h1:2k5gtwQX7v1FbeYcCk1O5Sp/sOL9D9iBBtQ7n6XPyBo= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= diff --git a/modules/fireback/CoreUtils.go b/modules/fireback/CoreUtils.go index d2e99abd3..e4df18017 100644 --- a/modules/fireback/CoreUtils.go +++ b/modules/fireback/CoreUtils.go @@ -11,6 +11,7 @@ import ( "log" "regexp" "strconv" + "strings" "github.com/gin-gonic/gin" "github.com/manifoldco/promptui" @@ -340,6 +341,10 @@ func GetHttpCommand(engineFn func(cfg2 HttpServerInstanceConfig) *gin.Engine) cl Name: "watch", Usage: "Monitor server stat using charts and interactive graphics", }, + &cli.StringFlag{ + Name: "domains", + Usage: "Mimics the runtime on specific domains locally, for example: test.com,test.pl. If ssl enabled, the certificate needs to include the domains.", + }, &cli.BoolFlag{ Name: "ssl", Usage: "Runs ssl server on 443 port", @@ -365,19 +370,22 @@ func GetHttpCommand(engineFn func(cfg2 HttpServerInstanceConfig) *gin.Engine) cl }, Name: "start", Aliases: []string{"s"}, - Usage: "Starts http server only", + Usage: "Starts http(s) server only, has few configuration", Action: func(c *cli.Context) error { initLogger() if !config.Production { Doctor() } + cfg2 := HttpServerInstanceConfig{ - Monitor: c.Bool("watch"), - Port: c.Int64("port"), - SSL: c.Bool("ssl"), - Slow: c.Bool("slow"), + Monitor: c.Bool("watch"), + Port: c.Int64("port"), + SSL: c.Bool("ssl"), + Slow: c.Bool("slow"), + VirtualDomains: strings.Split(c.String("domains"), ","), } + engine := engineFn(cfg2) CreateHttpServer(engine, cfg2) diff --git a/modules/fireback/EventBusSubscriptionAction.dyno.go b/modules/fireback/EventBusSubscriptionAction.dyno.go index baf728d48..927b3cb00 100644 --- a/modules/fireback/EventBusSubscriptionAction.dyno.go +++ b/modules/fireback/EventBusSubscriptionAction.dyno.go @@ -146,7 +146,7 @@ func EventBusSubscriptionActionReactiveHandler(factory func( return func(ctx *gin.Context) { read := make(chan EventBusSubscriptionActionReadChan) done := make(chan bool) - c, err := Upgrader.Upgrade(ctx.Writer, ctx.Request, nil) + c, err := upgraderEventBusSubscriptionAction.Upgrade(ctx.Writer, ctx.Request, nil) if err != nil { c.WriteMessage(websocket.TextMessage, []byte(err.Error())) c.Close() diff --git a/modules/fireback/ReactiveSearchAction.dyno.go b/modules/fireback/ReactiveSearchAction.dyno.go index 1a52c2e68..2bd5350d3 100644 --- a/modules/fireback/ReactiveSearchAction.dyno.go +++ b/modules/fireback/ReactiveSearchAction.dyno.go @@ -146,7 +146,7 @@ func ReactiveSearchActionReactiveHandler(factory func( return func(ctx *gin.Context) { read := make(chan ReactiveSearchActionReadChan) done := make(chan bool) - c, err := Upgrader.Upgrade(ctx.Writer, ctx.Request, nil) + c, err := upgraderReactiveSearchAction.Upgrade(ctx.Writer, ctx.Request, nil) if err != nil { c.WriteMessage(websocket.TextMessage, []byte(err.Error())) c.Close() diff --git a/modules/fireback/VirtualDomains.go b/modules/fireback/VirtualDomains.go new file mode 100644 index 000000000..0784ab52b --- /dev/null +++ b/modules/fireback/VirtualDomains.go @@ -0,0 +1,125 @@ +package fireback + +import ( + "os" + "runtime" + "strings" +) + +func hostsPath() string { + switch runtime.GOOS { + case "windows": + return `C:\Windows\System32\drivers\etc\hosts` + default: + return "/etc/hosts" + } +} + +// This can be even changed per project +const markerStart = "# local-fb-domain-sim-start" +const markerEnd = "# local-fb-domain-sim-end" + +func EnableDomain(domain string) error { + path := hostsPath() + + data, err := os.ReadFile(path) + if err != nil { + return err + } + + content := string(data) + + domains := extractDomains(content) + domains[domain] = true + + newBlock := buildBlock(domains) + + content = removeBlock(content) + content = strings.TrimSpace(content) + "\n\n" + newBlock + "\n" + + return os.WriteFile(path, []byte(content), 0644) +} + +func DisableDomain(domain string) error { + path := hostsPath() + + data, err := os.ReadFile(path) + if err != nil { + return err + } + + content := string(data) + + domains := extractDomains(content) + delete(domains, domain) + + newBlock := buildBlock(domains) + + content = removeBlock(content) + content = strings.TrimSpace(content) + "\n\n" + newBlock + "\n" + + return os.WriteFile(path, []byte(content), 0644) +} + +func extractDomains(content string) map[string]bool { + lines := strings.Split(content, "\n") + + inBlock := false + domains := map[string]bool{} + + for _, line := range lines { + line = strings.TrimSpace(line) + + if line == markerStart { + inBlock = true + continue + } + if line == markerEnd { + inBlock = false + continue + } + + if inBlock && line != "" { + parts := strings.Fields(line) + if len(parts) == 2 { + domains[parts[1]] = true + } + } + } + + return domains +} + +func buildBlock(domains map[string]bool) string { + var sb strings.Builder + + sb.WriteString(markerStart + "\n") + for d := range domains { + sb.WriteString("127.0.0.1 " + d + "\n") + } + sb.WriteString(markerEnd) + + return sb.String() +} + +func removeBlock(content string) string { + lines := strings.Split(content, "\n") + var out []string + + skip := false + for _, line := range lines { + if strings.Contains(line, markerStart) { + skip = true + continue + } + if strings.Contains(line, markerEnd) { + skip = false + continue + } + if !skip { + out = append(out, line) + } + } + + return strings.Join(out, "\n") +} diff --git a/modules/fireback/fireback-app.go b/modules/fireback/fireback-app.go index 5064353de..f9c5ff25f 100644 --- a/modules/fireback/fireback-app.go +++ b/modules/fireback/fireback-app.go @@ -205,6 +205,8 @@ type HttpServerInstanceConfig struct { SSL bool Slow bool + + VirtualDomains []string } func SetupHttpServer(x *FirebackApp, cfg HttpServerInstanceConfig) *gin.Engine { diff --git a/modules/fireback/httpServer.go b/modules/fireback/httpServer.go index 388493ec1..67d32db46 100644 --- a/modules/fireback/httpServer.go +++ b/modules/fireback/httpServer.go @@ -1,10 +1,14 @@ package fireback import ( + "context" "fmt" "log" "net" "net/http" + "os" + "os/signal" + "syscall" "time" "github.com/gin-gonic/gin" @@ -39,63 +43,85 @@ var LOG *zap.Logger // Other one is used for public, anyone who wants to use their software, // create account, etc. func CreateHttpServer(handler *gin.Engine, config2 HttpServerInstanceConfig) { - port := config.Port - if config2.Port != 0 { port = config2.Port } - forceSSL := config2.SSL || config.UseSSL - - if forceSSL { - port = 443 + for _, vd := range config2.VirtualDomains { + if vd == "" { + continue + } - go func() { - redirectServer := &http.Server{ - Addr: ":80", - Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - target := "https://" + r.Host + r.URL.Path - if r.URL.RawQuery != "" { - target += "?" + r.URL.RawQuery - } - http.Redirect(w, r, target, http.StatusMovedPermanently) - }), - } - log.Fatal(redirectServer.ListenAndServe()) - }() + fmt.Println("Starting virtual domain: ", vd, EnableDomain(vd)) } - server01 := &http.Server{ - Addr: ":" + fmt.Sprintf("%v", port), + forceSSL := config2.SSL || config.UseSSL + + mainServer := &http.Server{ + Addr: fmt.Sprintf(":%d", port), Handler: handler, ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, } - LOG.Info("Http server running on:", zap.String("url", "http://localhost"+server01.Addr+"/")) - LOG.Info("Ping available on:", zap.String("url", "http://localhost"+server01.Addr+"/ping")) - LOG.Info("Internal server ip: ** slash char \"/\" in the end is important in some sdks we generate depend on it **") + var redirectServer *http.Server - // Get's the local IP. - ipData := GetOutboundIP() - if ipData != nil { - url := "http://" + ipData.String() + server01.Addr + "/" - LOG.Info("Local network address:", zap.String("url", url)) + if forceSSL { + mainServer.Addr = ":443" + + redirectServer = &http.Server{ + Addr: ":80", + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + target := "https://" + r.Host + r.URL.RequestURI() + http.Redirect(w, r, target, http.StatusMovedPermanently) + }), + } + + go func() { + if err := redirectServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatal(err) + } + }() } - g.Go(func() error { + // Run main server + go func() { + var err error if forceSSL { - return server01.ListenAndServeTLS(config.CertFile, config.KeyFile) + err = mainServer.ListenAndServeTLS(config.CertFile, config.KeyFile) + } else { + err = mainServer.ListenAndServe() + } + if err != nil && err != http.ErrServerClosed { + log.Fatal(err) + } + }() + + // --- Graceful shutdown --- + stop := make(chan os.Signal, 1) + signal.Notify(stop, os.Interrupt, syscall.SIGTERM) + + <-stop + LOG.Info("Shutting down...") + + for _, vd := range config2.VirtualDomains { + if vd == "" { + continue } - return server01.ListenAndServe() - }) + fmt.Println("Stopping virtual domain: ", vd, DisableDomain(vd)) + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() - if config2.Monitor { - go monitor() + if err := mainServer.Shutdown(ctx); err != nil { + LOG.Error("Main server shutdown failed", zap.Error(err)) } - if err := g.Wait(); err != nil { - log.Fatal(err) + if redirectServer != nil { + _ = redirectServer.Shutdown(ctx) } + + LOG.Info("Server exited properly") } From 112d44155a1838d59075482a636245fc98cd19bd Mon Sep 17 00:00:00 2001 From: Ali Date: Fri, 17 Apr 2026 22:33:26 +0200 Subject: [PATCH 2/2] Minor change to include certificate --- modules/fireback/httpServer.go | 74 ++++++++++++++++++++++++++++++++-- 1 file changed, 71 insertions(+), 3 deletions(-) diff --git a/modules/fireback/httpServer.go b/modules/fireback/httpServer.go index 67d32db46..10e6ebfb0 100644 --- a/modules/fireback/httpServer.go +++ b/modules/fireback/httpServer.go @@ -2,8 +2,14 @@ package fireback import ( "context" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "encoding/pem" "fmt" "log" + "math/big" "net" "net/http" "os" @@ -56,8 +62,6 @@ func CreateHttpServer(handler *gin.Engine, config2 HttpServerInstanceConfig) { fmt.Println("Starting virtual domain: ", vd, EnableDomain(vd)) } - forceSSL := config2.SSL || config.UseSSL - mainServer := &http.Server{ Addr: fmt.Sprintf(":%d", port), Handler: handler, @@ -65,6 +69,27 @@ func CreateHttpServer(handler *gin.Engine, config2 HttpServerInstanceConfig) { WriteTimeout: 10 * time.Second, } + forceSSL := config2.SSL || config.UseSSL + + var useVirtualCert bool + + if forceSSL { + useVirtualCert = config.CertFile == "" || config.KeyFile == "" + + if useVirtualCert { + cert, err := GenerateSelfSignedCert(config2.VirtualDomains) + if err != nil { + log.Fatal("failed to generate self-signed cert:", err) + } + + mainServer.TLSConfig = &tls.Config{ + Certificates: []tls.Certificate{cert}, + } + + fmt.Println("Using self-signed certificate (no cert files provided)") + } + } + var redirectServer *http.Server if forceSSL { @@ -89,7 +114,11 @@ func CreateHttpServer(handler *gin.Engine, config2 HttpServerInstanceConfig) { go func() { var err error if forceSSL { - err = mainServer.ListenAndServeTLS(config.CertFile, config.KeyFile) + if useVirtualCert { + err = mainServer.ListenAndServeTLS("", "") + } else { + err = mainServer.ListenAndServeTLS(config.CertFile, config.KeyFile) + } } else { err = mainServer.ListenAndServe() } @@ -125,3 +154,42 @@ func CreateHttpServer(handler *gin.Engine, config2 HttpServerInstanceConfig) { LOG.Info("Server exited properly") } + +func GenerateSelfSignedCert(domains []string) (tls.Certificate, error) { + priv, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return tls.Certificate{}, err + } + + if len(domains) == 0 { + domains = []string{"localhost"} + } + + template := x509.Certificate{ + SerialNumber: big.NewInt(1), + NotBefore: time.Now(), + NotAfter: time.Now().Add(365 * 24 * time.Hour), + + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + + BasicConstraintsValid: true, + DNSNames: domains, + } + + // add localhost fallback + template.DNSNames = append(template.DNSNames, "localhost") + + // add IP support + template.IPAddresses = []net.IP{net.ParseIP("127.0.0.1")} + + derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) + if err != nil { + return tls.Certificate{}, err + } + + certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}) + + return tls.X509KeyPair(certPEM, keyPEM) +}