diff options
| -rw-r--r-- | cmd/gotosocial/main.go | 20 | ||||
| -rw-r--r-- | internal/config/config.go | 63 | ||||
| -rw-r--r-- | internal/config/default.go | 18 | ||||
| -rw-r--r-- | internal/config/letsencrypt.go | 11 | ||||
| -rw-r--r-- | internal/gotosocial/actions.go | 29 | ||||
| -rw-r--r-- | internal/router/router.go | 93 | 
6 files changed, 204 insertions, 30 deletions
diff --git a/cmd/gotosocial/main.go b/cmd/gotosocial/main.go index 337e7b785..948215276 100644 --- 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 index 2421290e7..23f7d0d1c 100644 --- 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 index b2d82110b..f63579753 100644 --- 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 new file mode 100644 index 000000000..ae40cb878 --- /dev/null +++ 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 index 8d3142f84..0cdb29de5 100644 --- 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 index 7ab208ef6..0f1f288bd 100644 --- 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) +}  | 
