mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-31 05:12:25 -05:00 
			
		
		
		
	start working on letsencrypt cert handling
This commit is contained in:
		
					parent
					
						
							
								3c539cdfd6
							
						
					
				
			
			
				commit
				
					
						d5c9c50e52
					
				
			
		
					 5 changed files with 168 additions and 24 deletions
				
			
		|  | @ -228,6 +228,26 @@ func main() { | ||||||
| 				Value:   defaults.StatusesMaxMediaFiles, | 				Value:   defaults.StatusesMaxMediaFiles, | ||||||
| 				EnvVars: []string{envNames.StatusesMaxMediaFiles}, | 				EnvVars: []string{envNames.StatusesMaxMediaFiles}, | ||||||
| 			}, | 			}, | ||||||
|  | 
 | ||||||
|  | 			// LETSENCRYPT FLAGS | ||||||
|  | 			&cli.BoolFlag{ | ||||||
|  | 				Name:    flagNames.LetsEncryptEnabled, | ||||||
|  | 				Usage:   "Enable letsencrypt TLS certs for this server. If set to true, then cert dir also needs to be set (or take the default).", | ||||||
|  | 				Value:   defaults.LetsEncryptEnabled, | ||||||
|  | 				EnvVars: []string{envNames.LetsEncryptEnabled}, | ||||||
|  | 			}, | ||||||
|  | 			&cli.StringFlag{ | ||||||
|  | 				Name:    flagNames.LetsEncryptCertDir, | ||||||
|  | 				Usage:   "Directory to store acquired letsencrypt certificates.", | ||||||
|  | 				Value:   defaults.LetsEncryptCertDir, | ||||||
|  | 				EnvVars: []string{envNames.LetsEncryptCertDir}, | ||||||
|  | 			}, | ||||||
|  | 			&cli.StringFlag{ | ||||||
|  | 				Name:    flagNames.LetsEncryptEmailAddress, | ||||||
|  | 				Usage:   "Email address to use when requesting letsencrypt certs. Will receive updates on cert expiry etc.", | ||||||
|  | 				Value:   defaults.LetsEncryptEmailAddress, | ||||||
|  | 				EnvVars: []string{envNames.LetsEncryptEmailAddress}, | ||||||
|  | 			}, | ||||||
| 		}, | 		}, | ||||||
| 		Commands: []*cli.Command{ | 		Commands: []*cli.Command{ | ||||||
| 			{ | 			{ | ||||||
|  |  | ||||||
|  | @ -37,6 +37,7 @@ type Config struct { | ||||||
| 	MediaConfig       *MediaConfig       `yaml:"media"` | 	MediaConfig       *MediaConfig       `yaml:"media"` | ||||||
| 	StorageConfig     *StorageConfig     `yaml:"storage"` | 	StorageConfig     *StorageConfig     `yaml:"storage"` | ||||||
| 	StatusesConfig    *StatusesConfig    `yaml:"statuses"` | 	StatusesConfig    *StatusesConfig    `yaml:"statuses"` | ||||||
|  | 	LetsEncryptConfig *LetsEncryptConfig `yaml:"letsEncrypt"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // FromFile returns a new config from a file, or an error if something goes amiss. | // FromFile returns a new config from a file, or an error if something goes amiss. | ||||||
|  | @ -200,6 +201,19 @@ func (c *Config) ParseCLIFlags(f KeyedFlags) { | ||||||
| 	if c.StatusesConfig.MaxMediaFiles == 0 || f.IsSet(fn.StatusesMaxMediaFiles) { | 	if c.StatusesConfig.MaxMediaFiles == 0 || f.IsSet(fn.StatusesMaxMediaFiles) { | ||||||
| 		c.StatusesConfig.MaxMediaFiles = f.Int(fn.StatusesMaxMediaFiles) | 		c.StatusesConfig.MaxMediaFiles = f.Int(fn.StatusesMaxMediaFiles) | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
|  | 	// letsencrypt flags | ||||||
|  | 	if f.IsSet(fn.LetsEncryptEnabled) { | ||||||
|  | 		c.LetsEncryptConfig.Enabled = f.Bool(fn.LetsEncryptEnabled) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if c.LetsEncryptConfig.CertDir == "" || f.IsSet(fn.LetsEncryptCertDir) { | ||||||
|  | 		c.LetsEncryptConfig.CertDir = f.String(fn.LetsEncryptCertDir) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if c.LetsEncryptConfig.EmailAddress == "" || f.IsSet(fn.LetsEncryptEmailAddress) { | ||||||
|  | 		c.LetsEncryptConfig.EmailAddress = f.String(fn.LetsEncryptEmailAddress) | ||||||
|  | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // KeyedFlags is a wrapper for any type that can store keyed flags and give them back. | // KeyedFlags is a wrapper for any type that can store keyed flags and give them back. | ||||||
|  | @ -249,6 +263,10 @@ type Flags struct { | ||||||
| 	StatusesPollMaxOptions     string | 	StatusesPollMaxOptions     string | ||||||
| 	StatusesPollOptionMaxChars string | 	StatusesPollOptionMaxChars string | ||||||
| 	StatusesMaxMediaFiles      string | 	StatusesMaxMediaFiles      string | ||||||
|  | 
 | ||||||
|  | 	LetsEncryptEnabled      string | ||||||
|  | 	LetsEncryptCertDir      string | ||||||
|  | 	LetsEncryptEmailAddress string | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Defaults contains all the default values for a gotosocial config | // Defaults contains all the default values for a gotosocial config | ||||||
|  | @ -288,6 +306,10 @@ type Defaults struct { | ||||||
| 	StatusesPollMaxOptions     int | 	StatusesPollMaxOptions     int | ||||||
| 	StatusesPollOptionMaxChars int | 	StatusesPollOptionMaxChars int | ||||||
| 	StatusesMaxMediaFiles      int | 	StatusesMaxMediaFiles      int | ||||||
|  | 
 | ||||||
|  | 	LetsEncryptEnabled      bool | ||||||
|  | 	LetsEncryptCertDir      string | ||||||
|  | 	LetsEncryptEmailAddress string | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // GetFlagNames returns a struct containing the names of the various flags used for | // GetFlagNames returns a struct containing the names of the various flags used for | ||||||
|  | @ -329,6 +351,10 @@ func GetFlagNames() Flags { | ||||||
| 		StatusesPollMaxOptions:     "statuses-poll-max-options", | 		StatusesPollMaxOptions:     "statuses-poll-max-options", | ||||||
| 		StatusesPollOptionMaxChars: "statuses-poll-option-max-chars", | 		StatusesPollOptionMaxChars: "statuses-poll-option-max-chars", | ||||||
| 		StatusesMaxMediaFiles:      "statuses-max-media-files", | 		StatusesMaxMediaFiles:      "statuses-max-media-files", | ||||||
|  | 
 | ||||||
|  | 		LetsEncryptEnabled:      "letsencrypt-enabled", | ||||||
|  | 		LetsEncryptCertDir:      "letsencrypt-cert-dir", | ||||||
|  | 		LetsEncryptEmailAddress: "letsencrypt-email", | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -371,5 +397,9 @@ func GetEnvNames() Flags { | ||||||
| 		StatusesPollMaxOptions:     "GTS_STATUSES_POLL_MAX_OPTIONS", | 		StatusesPollMaxOptions:     "GTS_STATUSES_POLL_MAX_OPTIONS", | ||||||
| 		StatusesPollOptionMaxChars: "GTS_STATUSES_POLL_OPTION_MAX_CHARS", | 		StatusesPollOptionMaxChars: "GTS_STATUSES_POLL_OPTION_MAX_CHARS", | ||||||
| 		StatusesMaxMediaFiles:      "GTS_STATUSES_MAX_MEDIA_FILES", | 		StatusesMaxMediaFiles:      "GTS_STATUSES_MAX_MEDIA_FILES", | ||||||
|  | 
 | ||||||
|  | 		LetsEncryptEnabled:      "GTS_LETSENCRYPT_ENABLED", | ||||||
|  | 		LetsEncryptCertDir:      "GTS_LETSENCRYPT_CERT_DIR", | ||||||
|  | 		LetsEncryptEmailAddress: "GTS_LETSENCRYPT_EMAIL", | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -45,6 +45,11 @@ func TestDefault() *Config { | ||||||
| 			PollOptionMaxChars: defaults.StatusesPollOptionMaxChars, | 			PollOptionMaxChars: defaults.StatusesPollOptionMaxChars, | ||||||
| 			MaxMediaFiles:      defaults.StatusesMaxMediaFiles, | 			MaxMediaFiles:      defaults.StatusesMaxMediaFiles, | ||||||
| 		}, | 		}, | ||||||
|  | 		LetsEncryptConfig: &LetsEncryptConfig{ | ||||||
|  | 			Enabled:      defaults.LetsEncryptEnabled, | ||||||
|  | 			CertDir:      defaults.LetsEncryptCertDir, | ||||||
|  | 			EmailAddress: defaults.LetsEncryptEmailAddress, | ||||||
|  | 		}, | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -93,6 +98,11 @@ func Default() *Config { | ||||||
| 			PollOptionMaxChars: defaults.StatusesPollOptionMaxChars, | 			PollOptionMaxChars: defaults.StatusesPollOptionMaxChars, | ||||||
| 			MaxMediaFiles:      defaults.StatusesMaxMediaFiles, | 			MaxMediaFiles:      defaults.StatusesMaxMediaFiles, | ||||||
| 		}, | 		}, | ||||||
|  | 		LetsEncryptConfig: &LetsEncryptConfig{ | ||||||
|  | 			Enabled:      defaults.LetsEncryptEnabled, | ||||||
|  | 			CertDir:      defaults.LetsEncryptCertDir, | ||||||
|  | 			EmailAddress: defaults.LetsEncryptEmailAddress, | ||||||
|  | 		}, | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -135,6 +145,10 @@ func GetDefaults() Defaults { | ||||||
| 		StatusesPollMaxOptions:     6, | 		StatusesPollMaxOptions:     6, | ||||||
| 		StatusesPollOptionMaxChars: 50, | 		StatusesPollOptionMaxChars: 50, | ||||||
| 		StatusesMaxMediaFiles:      6, | 		StatusesMaxMediaFiles:      6, | ||||||
|  | 
 | ||||||
|  | 		LetsEncryptEnabled:      true, | ||||||
|  | 		LetsEncryptCertDir:      "/gotosocial/storage/certs", | ||||||
|  | 		LetsEncryptEmailAddress: "", | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -176,5 +190,9 @@ func GetTestDefaults() Defaults { | ||||||
| 		StatusesPollMaxOptions:     6, | 		StatusesPollMaxOptions:     6, | ||||||
| 		StatusesPollOptionMaxChars: 50, | 		StatusesPollOptionMaxChars: 50, | ||||||
| 		StatusesMaxMediaFiles:      6, | 		StatusesMaxMediaFiles:      6, | ||||||
|  | 
 | ||||||
|  | 		LetsEncryptEnabled:      false, | ||||||
|  | 		LetsEncryptCertDir:      "", | ||||||
|  | 		LetsEncryptEmailAddress: "", | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										11
									
								
								internal/config/letsencrypt.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								internal/config/letsencrypt.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,11 @@ | ||||||
|  | package config | ||||||
|  | 
 | ||||||
|  | // LetsEncryptConfig wraps everything needed to manage letsencrypt certificates from within gotosocial. | ||||||
|  | type LetsEncryptConfig struct { | ||||||
|  | 	// Should letsencrypt certificate fetching be enabled? | ||||||
|  | 	Enabled bool | ||||||
|  | 	// Where should certificates be stored? | ||||||
|  | 	CertDir string | ||||||
|  | 	// Email address to pass to letsencrypt for notifications about certificate expiry etc. | ||||||
|  | 	EmailAddress string | ||||||
|  | } | ||||||
|  | @ -31,6 +31,7 @@ import ( | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/sirupsen/logrus" | 	"github.com/sirupsen/logrus" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||||
|  | 	"golang.org/x/crypto/acme/autocert" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // Router provides the REST interface for gotosocial, using gin. | // Router provides the REST interface for gotosocial, using gin. | ||||||
|  | @ -50,16 +51,41 @@ type router struct { | ||||||
| 	logger      *logrus.Logger | 	logger      *logrus.Logger | ||||||
| 	engine      *gin.Engine | 	engine      *gin.Engine | ||||||
| 	srv         *http.Server | 	srv         *http.Server | ||||||
|  | 	config      *config.Config | ||||||
|  | 	certManager *autocert.Manager | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Start starts the router nicely | // Start starts the router nicely. | ||||||
|  | // | ||||||
|  | // Different ports and handlers will be served depending on whether letsencrypt is enabled or not. | ||||||
|  | // If it is enabled, then port 80 will be used for handling LE requests, and port 443 will be used | ||||||
|  | // for serving actual requests. | ||||||
|  | // | ||||||
|  | // If letsencrypt is not being used, then port 8080 only will be used for serving requests. | ||||||
| func (r *router) Start() { | func (r *router) Start() { | ||||||
|  | 	if r.config.LetsEncryptConfig.Enabled { | ||||||
|  | 		// serve the http handler on port 80 for receiving letsencrypt requests and solving their devious riddles | ||||||
|  | 		go func() { | ||||||
|  | 			if err := http.ListenAndServe(":http", r.certManager.HTTPHandler(http.HandlerFunc(httpsRedirect))); err != nil && err != http.ErrServerClosed { | ||||||
|  | 				r.logger.Fatalf("listen: %s", err) | ||||||
|  | 			} | ||||||
|  | 		}() | ||||||
|  | 
 | ||||||
|  | 		// and serve the actual TLS handler on port 443  | ||||||
|  | 		go func() { | ||||||
|  | 			if err := r.srv.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed { | ||||||
|  | 				r.logger.Fatalf("listen: %s", err) | ||||||
|  | 			} | ||||||
|  | 		}() | ||||||
|  | 	} else { | ||||||
|  | 		// no tls required so just serve on port 8080 | ||||||
| 		go func() { | 		go func() { | ||||||
| 			if err := r.srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { | 			if err := r.srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { | ||||||
| 				r.logger.Fatalf("listen: %s", err) | 				r.logger.Fatalf("listen: %s", err) | ||||||
| 			} | 			} | ||||||
| 		}() | 		}() | ||||||
| 	} | 	} | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| // Stop shuts down the router nicely | // Stop shuts down the router nicely | ||||||
| func (r *router) Stop(ctx context.Context) error { | func (r *router) Stop(ctx context.Context) error { | ||||||
|  | @ -93,6 +119,8 @@ func New(config *config.Config, logger *logrus.Logger) (Router, error) { | ||||||
| 	default: | 	default: | ||||||
| 		gin.SetMode(gin.ReleaseMode) | 		gin.SetMode(gin.ReleaseMode) | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
|  | 	// create the actual engine here -- this is the core request routing handler for gts | ||||||
| 	engine := gin.Default() | 	engine := gin.Default() | ||||||
| 
 | 
 | ||||||
| 	// create a new session store middleware | 	// create a new session store middleware | ||||||
|  | @ -111,13 +139,40 @@ func New(config *config.Config, logger *logrus.Logger) (Router, error) { | ||||||
| 	logger.Debugf("loading templates from %s", tmPath) | 	logger.Debugf("loading templates from %s", tmPath) | ||||||
| 	engine.LoadHTMLGlob(tmPath) | 	engine.LoadHTMLGlob(tmPath) | ||||||
| 
 | 
 | ||||||
|  | 	// create the actual http server here | ||||||
|  | 	var s *http.Server | ||||||
|  | 	var m *autocert.Manager | ||||||
|  | 
 | ||||||
|  | 	// We need to spawn the underlying server slightly differently depending on whether lets encrypt is enabled or not. | ||||||
|  | 	// In either case, the gin engine will still be used for routing requests. | ||||||
|  | 	if config.LetsEncryptConfig.Enabled { | ||||||
|  | 		// le IS enabled, so roll up an autocert manager for handling letsencrypt requests | ||||||
|  | 		m = &autocert.Manager{ | ||||||
|  | 			Prompt:     autocert.AcceptTOS, | ||||||
|  | 			HostPolicy: autocert.HostWhitelist(config.Host), | ||||||
|  | 			Cache:      autocert.DirCache(config.LetsEncryptConfig.CertDir), | ||||||
|  | 			Email:      config.LetsEncryptConfig.EmailAddress, | ||||||
|  | 		} | ||||||
|  | 		// and create an HTTPS server | ||||||
|  | 		s = &http.Server{ | ||||||
|  | 			Addr:      ":https", | ||||||
|  | 			TLSConfig: m.TLSConfig(), | ||||||
|  | 			Handler:   engine, | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		// le is NOT enabled, so just serve bare requests on port 8080 | ||||||
|  | 		s = &http.Server{ | ||||||
|  | 			Addr:    ":8080", | ||||||
|  | 			Handler: engine, | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	return &router{ | 	return &router{ | ||||||
| 		logger:      logger, | 		logger:      logger, | ||||||
| 		engine:      engine, | 		engine:      engine, | ||||||
| 		srv: &http.Server{ | 		srv:         s, | ||||||
| 			Addr:    ":8080", | 		config:      config, | ||||||
| 			Handler: engine, | 		certManager: m, | ||||||
| 		}, |  | ||||||
| 	}, nil | 	}, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -136,3 +191,13 @@ func sessionStore() (memstore.Store, error) { | ||||||
| 
 | 
 | ||||||
| 	return memstore.NewStore(auth, crypt), nil | 	return memstore.NewStore(auth, crypt), nil | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func httpsRedirect(w http.ResponseWriter, req *http.Request) { | ||||||
|  | 	target := "https://" + req.Host + req.URL.Path | ||||||
|  | 
 | ||||||
|  | 	if len(req.URL.RawQuery) > 0 { | ||||||
|  | 		target += "?" + req.URL.RawQuery | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	http.Redirect(w, req, target, http.StatusTemporaryRedirect) | ||||||
|  | } | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue