Letsencrypt (#17)

This commit is contained in:
Tobi Smethurst 2021-05-09 11:25:13 +02:00 committed by GitHub
parent 3c539cdfd6
commit 0cbab627c7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 204 additions and 30 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.
@ -54,12 +55,13 @@ func FromFile(path string) (*Config, error) {
// Empty just returns a new empty config
func Empty() *Config {
return &Config{
DBConfig: &DBConfig{},
TemplateConfig: &TemplateConfig{},
AccountsConfig: &AccountsConfig{},
MediaConfig: &MediaConfig{},
StorageConfig: &StorageConfig{},
StatusesConfig: &StatusesConfig{},
DBConfig: &DBConfig{},
TemplateConfig: &TemplateConfig{},
AccountsConfig: &AccountsConfig{},
MediaConfig: &MediaConfig{},
StorageConfig: &StorageConfig{},
StatusesConfig: &StatusesConfig{},
LetsEncryptConfig: &LetsEncryptConfig{},
}
}
@ -200,6 +202,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 +264,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 +307,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 +352,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 +398,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

@ -40,6 +40,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/federation"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/media"
"github.com/superseriousbusiness/gotosocial/internal/message"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
@ -49,6 +50,28 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
)
var models []interface{} = []interface{}{
&gtsmodel.Account{},
&gtsmodel.Application{},
&gtsmodel.Block{},
&gtsmodel.DomainBlock{},
&gtsmodel.EmailDomainBlock{},
&gtsmodel.Follow{},
&gtsmodel.FollowRequest{},
&gtsmodel.MediaAttachment{},
&gtsmodel.Mention{},
&gtsmodel.Status{},
&gtsmodel.StatusFave{},
&gtsmodel.StatusBookmark{},
&gtsmodel.StatusMute{},
&gtsmodel.StatusPin{},
&gtsmodel.Tag{},
&gtsmodel.User{},
&gtsmodel.Emoji{},
&oauth.Token{},
&oauth.Client{},
}
// Run creates and starts a gotosocial server
var Run action.GTSAction = func(ctx context.Context, c *config.Config, log *logrus.Logger) error {
dbService, err := db.NewPostgresService(ctx, c, log)
@ -109,6 +132,12 @@ var Run action.GTSAction = func(ctx context.Context, c *config.Config, log *logr
}
}
for _, m := range models {
if err := dbService.CreateTable(m); err != nil {
return fmt.Errorf("table creation error: %s", err)
}
}
if err := dbService.CreateInstanceAccount(); err != nil {
return fmt.Errorf("error creating instance account: %s", err)
}

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