mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-31 04:02:26 -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, | ||||
| 				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{ | ||||
| 			{ | ||||
|  |  | |||
|  | @ -27,16 +27,17 @@ import ( | |||
| 
 | ||||
| // Config pulls together all the configuration needed to run gotosocial | ||||
| type Config struct { | ||||
| 	LogLevel        string          `yaml:"logLevel"` | ||||
| 	ApplicationName string          `yaml:"applicationName"` | ||||
| 	Host            string          `yaml:"host"` | ||||
| 	Protocol        string          `yaml:"protocol"` | ||||
| 	DBConfig        *DBConfig       `yaml:"db"` | ||||
| 	TemplateConfig  *TemplateConfig `yaml:"template"` | ||||
| 	AccountsConfig  *AccountsConfig `yaml:"accounts"` | ||||
| 	MediaConfig     *MediaConfig    `yaml:"media"` | ||||
| 	StorageConfig   *StorageConfig  `yaml:"storage"` | ||||
| 	StatusesConfig  *StatusesConfig `yaml:"statuses"` | ||||
| 	LogLevel          string             `yaml:"logLevel"` | ||||
| 	ApplicationName   string             `yaml:"applicationName"` | ||||
| 	Host              string             `yaml:"host"` | ||||
| 	Protocol          string             `yaml:"protocol"` | ||||
| 	DBConfig          *DBConfig          `yaml:"db"` | ||||
| 	TemplateConfig    *TemplateConfig    `yaml:"template"` | ||||
| 	AccountsConfig    *AccountsConfig    `yaml:"accounts"` | ||||
| 	MediaConfig       *MediaConfig       `yaml:"media"` | ||||
| 	StorageConfig     *StorageConfig     `yaml:"storage"` | ||||
| 	StatusesConfig    *StatusesConfig    `yaml:"statuses"` | ||||
| 	LetsEncryptConfig *LetsEncryptConfig `yaml:"letsEncrypt"` | ||||
| } | ||||
| 
 | ||||
| // 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) { | ||||
| 		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. | ||||
|  | @ -249,6 +263,10 @@ type Flags struct { | |||
| 	StatusesPollMaxOptions     string | ||||
| 	StatusesPollOptionMaxChars string | ||||
| 	StatusesMaxMediaFiles      string | ||||
| 
 | ||||
| 	LetsEncryptEnabled      string | ||||
| 	LetsEncryptCertDir      string | ||||
| 	LetsEncryptEmailAddress string | ||||
| } | ||||
| 
 | ||||
| // Defaults contains all the default values for a gotosocial config | ||||
|  | @ -288,6 +306,10 @@ type Defaults struct { | |||
| 	StatusesPollMaxOptions     int | ||||
| 	StatusesPollOptionMaxChars int | ||||
| 	StatusesMaxMediaFiles      int | ||||
| 
 | ||||
| 	LetsEncryptEnabled      bool | ||||
| 	LetsEncryptCertDir      string | ||||
| 	LetsEncryptEmailAddress string | ||||
| } | ||||
| 
 | ||||
| // 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", | ||||
| 		StatusesPollOptionMaxChars: "statuses-poll-option-max-chars", | ||||
| 		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", | ||||
| 		StatusesPollOptionMaxChars: "GTS_STATUSES_POLL_OPTION_MAX_CHARS", | ||||
| 		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, | ||||
| 			MaxMediaFiles:      defaults.StatusesMaxMediaFiles, | ||||
| 		}, | ||||
| 		LetsEncryptConfig: &LetsEncryptConfig{ | ||||
| 			Enabled:      defaults.LetsEncryptEnabled, | ||||
| 			CertDir:      defaults.LetsEncryptCertDir, | ||||
| 			EmailAddress: defaults.LetsEncryptEmailAddress, | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
|  | @ -93,6 +98,11 @@ func Default() *Config { | |||
| 			PollOptionMaxChars: defaults.StatusesPollOptionMaxChars, | ||||
| 			MaxMediaFiles:      defaults.StatusesMaxMediaFiles, | ||||
| 		}, | ||||
| 		LetsEncryptConfig: &LetsEncryptConfig{ | ||||
| 			Enabled:      defaults.LetsEncryptEnabled, | ||||
| 			CertDir:      defaults.LetsEncryptCertDir, | ||||
| 			EmailAddress: defaults.LetsEncryptEmailAddress, | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
|  | @ -135,6 +145,10 @@ func GetDefaults() Defaults { | |||
| 		StatusesPollMaxOptions:     6, | ||||
| 		StatusesPollOptionMaxChars: 50, | ||||
| 		StatusesMaxMediaFiles:      6, | ||||
| 
 | ||||
| 		LetsEncryptEnabled:      true, | ||||
| 		LetsEncryptCertDir:      "/gotosocial/storage/certs", | ||||
| 		LetsEncryptEmailAddress: "", | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
|  | @ -176,5 +190,9 @@ func GetTestDefaults() Defaults { | |||
| 		StatusesPollMaxOptions:     6, | ||||
| 		StatusesPollOptionMaxChars: 50, | ||||
| 		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/sirupsen/logrus" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||
| 	"golang.org/x/crypto/acme/autocert" | ||||
| ) | ||||
| 
 | ||||
| // Router provides the REST interface for gotosocial, using gin. | ||||
|  | @ -47,18 +48,43 @@ type Router interface { | |||
| 
 | ||||
| // router fulfils the Router interface using gin and logrus | ||||
| type router struct { | ||||
| 	logger *logrus.Logger | ||||
| 	engine *gin.Engine | ||||
| 	srv    *http.Server | ||||
| 	logger      *logrus.Logger | ||||
| 	engine      *gin.Engine | ||||
| 	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() { | ||||
| 	go func() { | ||||
| 		if err := r.srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { | ||||
| 			r.logger.Fatalf("listen: %s", err) | ||||
| 		} | ||||
| 	}() | ||||
| 	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() { | ||||
| 			if err := r.srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { | ||||
| 				r.logger.Fatalf("listen: %s", err) | ||||
| 			} | ||||
| 		}() | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Stop shuts down the router nicely | ||||
|  | @ -93,6 +119,8 @@ func New(config *config.Config, logger *logrus.Logger) (Router, error) { | |||
| 	default: | ||||
| 		gin.SetMode(gin.ReleaseMode) | ||||
| 	} | ||||
| 
 | ||||
| 	// create the actual engine here -- this is the core request routing handler for gts | ||||
| 	engine := gin.Default() | ||||
| 
 | ||||
| 	// 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) | ||||
| 	engine.LoadHTMLGlob(tmPath) | ||||
| 
 | ||||
| 	return &router{ | ||||
| 		logger: logger, | ||||
| 		engine: engine, | ||||
| 		srv: &http.Server{ | ||||
| 	// 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{ | ||||
| 		logger:      logger, | ||||
| 		engine:      engine, | ||||
| 		srv:         s, | ||||
| 		config:      config, | ||||
| 		certManager: m, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
|  | @ -136,3 +191,13 @@ func sessionStore() (memstore.Store, error) { | |||
| 
 | ||||
| 	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