commit 0cbab627c77002711029527f4697fc7ec6cd870d
parent 3c539cdfd61d9c0be4817025036347c5f9454747
Author: Tobi Smethurst <31960611+tsmethurst@users.noreply.github.com>
Date: Sun, 9 May 2021 11:25:13 +0200
Letsencrypt (#17)
Diffstat:
6 files changed, 204 insertions(+), 30 deletions(-)
diff --git a/cmd/gotosocial/main.go b/cmd/gotosocial/main.go
@@ -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{
{
diff --git a/internal/config/config.go b/internal/config/config.go
@@ -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",
}
}
diff --git a/internal/config/default.go b/internal/config/default.go
@@ -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: "",
}
}
diff --git a/internal/config/letsencrypt.go b/internal/config/letsencrypt.go
@@ -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
+}
diff --git a/internal/gotosocial/actions.go b/internal/gotosocial/actions.go
@@ -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{}{
+ >smodel.Account{},
+ >smodel.Application{},
+ >smodel.Block{},
+ >smodel.DomainBlock{},
+ >smodel.EmailDomainBlock{},
+ >smodel.Follow{},
+ >smodel.FollowRequest{},
+ >smodel.MediaAttachment{},
+ >smodel.Mention{},
+ >smodel.Status{},
+ >smodel.StatusFave{},
+ >smodel.StatusBookmark{},
+ >smodel.StatusMute{},
+ >smodel.StatusPin{},
+ >smodel.Tag{},
+ >smodel.User{},
+ >smodel.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)
}
diff --git a/internal/router/router.go b/internal/router/router.go
@@ -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)
+}