package main import ( "fmt" "net/http" "os" "path/filepath" "sort" "strings" "sync" "time" "acme-reverseproxy/config" "acme-reverseproxy/nfs" "acme-reverseproxy/proxymap" "github.com/BurntSushi/toml" "github.com/sirupsen/logrus" "github.com/urfave/cli" "golang.org/x/crypto/acme/autocert" ) var cfg config.Config func main() { app := cli.NewApp() app.Name = "acme-reverseproxy" app.Usage = "A TLS-serving reverse-proxy, with the certificates generated from LetsEncrypt" app.Authors = []cli.Author{{Name: "Forc", Email: "redirsvr@mail.ru"}} app.Flags = []cli.Flag{cli.BoolFlag{Name: "debug,D", Usage: "debug output"}} app.Commands = []cli.Command{ { Name: "gen", Description: "generators of sorts", Subcommands: []cli.Command{ { Name: "config", Description: "generate a sample mapping configuration", Action: GenConfigAction, }, }, }, { Name: "srv", Description: "Start the reverseproxy server", Action: SrvCommand, Flags: []cli.Flag{ cli.StringFlag{ Name: "config", Value: filepath.Join(os.Getenv("HOME"), ".acme-reverseproxy.toml"), Usage: "Configuration of mapping of hostname -> listener", }, }, Before: BeforeAction, }, } sort.Sort(cli.FlagsByName(app.Flags)) sort.Sort(cli.CommandsByName(app.Commands)) app.Run(os.Args) } func BeforeAction(c *cli.Context) error { if c.GlobalBool("debug") { logrus.SetLevel(logrus.DebugLevel) logrus.Info("Debug mode enabled") } else { logrus.SetLevel(logrus.InfoLevel) } logrus.Infof("Loading config from: %s", c.String("config")) buf, err := os.ReadFile(c.String("config")) if err != nil { if os.IsNotExist(err) { logrus.Errorf("No config file found at %q. Try 'gen config'", c.String("config")) } return err } tmpConfig := config.Config{} if err := toml.Unmarshal(buf, &tmpConfig); err != nil { logrus.Errorf("Failed to parse config: %v", err) return err } cfg = tmpConfig logrus.Infof("Config loaded: CA.Email=%s, CA.CacheDir=%s, WellKnownDir=%s", cfg.CA.Email, cfg.CA.CacheDir, cfg.WellKnownDir) logrus.Infof("Configured mappings: %d domains", len(cfg.Mapping)) for host, target := range cfg.Mapping { logrus.Infof(" %s -> %s", host, target) } return nil } func GenConfigAction(c *cli.Context) error { tmpConfig := config.Config{ CA: config.CA{ Email: "admin@p42.ru", CacheDir: "/tmp/acme-reverseproxy", }, Mapping: map[string]string{ "p42.ru": "http://localhost:5000", }, WellKnownDir: "/tmp/.well-known", NFS: config.NFSConfig{ Enabled: false, Server: "192.168.1.100", ExportPath: "/export/acme", MountPoint: "/mnt/acme-wellknown", Options: "rw,vers=4.1,timeo=50,retrans=2", }, } e := toml.NewEncoder(os.Stdout) if err := e.Encode(tmpConfig); err != nil { return err } return nil } func SrvCommand(c *cli.Context) error { // Инициализируем NFS менеджер nfs.Init(nfs.Config{ Enabled: cfg.NFS.Enabled, Server: cfg.NFS.Server, ExportPath: cfg.NFS.ExportPath, MountPoint: cfg.NFS.MountPoint, Options: cfg.NFS.Options, }) // Если NFS включен, монтируем его при старте if cfg.NFS.Enabled { logrus.Info("Initializing NFS mount at startup...") if err := nfs.EnsureMounted(); err != nil { logrus.Warnf("NFS mount failed at startup (will retry on demand): %v", err) } else { logrus.Info("NFS mounted successfully at startup") } } list := []string{} for key := range cfg.Mapping { if key != "" { list = append(list, key) } } if len(list) == 0 { logrus.Error("No domains configured in Mapping section") return cli.NewExitError("No domains configured", 2) } logrus.Infof("Initializing reverse proxy for %d domains: %s", len(list), strings.Join(list, ", ")) rpm, err := proxymap.ToReverseProxyMap(cfg.Mapping) if err != nil { logrus.Errorf("Failed to create reverse proxy map: %v", err) return cli.NewExitError(err, 2) } wellKnownDir := cfg.WellKnownDir if wellKnownDir == "" { wellKnownDir = "/tmp/.well-known" } // Если NFS включен и указан MountPoint, используем его для .well-known if cfg.NFS.Enabled && cfg.NFS.MountPoint != "" { wellKnownDir = cfg.NFS.MountPoint logrus.Infof("Using NFS mount point for .well-known: %s", wellKnownDir) } logrus.Infof("Well-known directory: %s", wellKnownDir) rph := proxymap.NewReverseProxiesHandlerWithWellKnown(rpm, wellKnownDir) logrus.Infof("ACME whitelist: %s", strings.Join(list, ", ")) m := autocert.Manager{ Prompt: autocert.AcceptTOS, HostPolicy: autocert.HostWhitelist(list...), } if cfg.CA.Email != "" { m.Email = cfg.CA.Email logrus.Infof("ACME email: %s", cfg.CA.Email) } if cfg.CA.CacheDir != "" { m.Cache = autocert.DirCache(cfg.CA.CacheDir) logrus.Infof("ACME cache directory: %s", cfg.CA.CacheDir) } httpsListener := m.Listener() httpsServer := &http.Server{ Handler: rph, } httpHandler := m.HTTPHandler(rph) httpServer := &http.Server{Addr: ":80", Handler: httpHandler} logrus.Info(strings.Repeat("=", 61)) logrus.Info("Starting reverse proxy servers...") logrus.Info(strings.Repeat("=", 61)) var wg sync.WaitGroup var firstError error var mu sync.Mutex wg.Add(2) go func() { defer wg.Done() logrus.Info("Starting HTTPS server on port 443...") if err := httpsServer.Serve(httpsListener); err != nil { mu.Lock() if firstError == nil { firstError = err } mu.Unlock() logrus.Errorf("HTTPS server stopped with error: %v", err) } else { logrus.Info("HTTPS server stopped normally") } }() go func() { defer wg.Done() logrus.Info("Starting HTTP server on port 80 (redirecting to HTTPS)...") if err := httpServer.ListenAndServe(); err != nil { mu.Lock() if firstError == nil { firstError = err } mu.Unlock() logrus.Errorf("HTTP server stopped with error: %v", err) } else { logrus.Info("HTTP server stopped normally") } }() time.Sleep(100 * time.Millisecond) logrus.Info("Servers started successfully. Waiting for requests...") logrus.Info("Press Ctrl+C to stop") wg.Wait() // При остановке размонтируем NFS если он был смонтирован if cfg.NFS.Enabled { if err := nfs.DefaultManager.Unmount(); err != nil { logrus.Warnf("Failed to unmount NFS on shutdown: %v", err) } } if firstError != nil { logrus.Errorf("Server exited with error: %v", firstError) return cli.NewExitError(firstError, 2) } logrus.Info("All servers stopped") return nil } func MakeDir(path string) string { err := os.MkdirAll(path, 0777) if err != nil { fmt.Println(err, path) return path } return path }