start working on letsencrypt cert handling

This commit is contained in:
tsmethurst 2021-05-08 22:19:56 +02:00
parent 3c539cdfd6
commit d5c9c50e52
5 changed files with 168 additions and 24 deletions

View File

@ -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{
{

View File

@ -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",
}
}

View File

@ -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: "",
}
}

View 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
}

View File

@ -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)
}