Letsencrypt (#17)
This commit is contained in:
parent
3c539cdfd6
commit
0cbab627c7
|
@ -228,6 +228,26 @@ func main() {
|
||||||
Value: defaults.StatusesMaxMediaFiles,
|
Value: defaults.StatusesMaxMediaFiles,
|
||||||
EnvVars: []string{envNames.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{
|
Commands: []*cli.Command{
|
||||||
{
|
{
|
||||||
|
|
|
@ -27,16 +27,17 @@ import (
|
||||||
|
|
||||||
// Config pulls together all the configuration needed to run gotosocial
|
// Config pulls together all the configuration needed to run gotosocial
|
||||||
type Config struct {
|
type Config struct {
|
||||||
LogLevel string `yaml:"logLevel"`
|
LogLevel string `yaml:"logLevel"`
|
||||||
ApplicationName string `yaml:"applicationName"`
|
ApplicationName string `yaml:"applicationName"`
|
||||||
Host string `yaml:"host"`
|
Host string `yaml:"host"`
|
||||||
Protocol string `yaml:"protocol"`
|
Protocol string `yaml:"protocol"`
|
||||||
DBConfig *DBConfig `yaml:"db"`
|
DBConfig *DBConfig `yaml:"db"`
|
||||||
TemplateConfig *TemplateConfig `yaml:"template"`
|
TemplateConfig *TemplateConfig `yaml:"template"`
|
||||||
AccountsConfig *AccountsConfig `yaml:"accounts"`
|
AccountsConfig *AccountsConfig `yaml:"accounts"`
|
||||||
MediaConfig *MediaConfig `yaml:"media"`
|
MediaConfig *MediaConfig `yaml:"media"`
|
||||||
StorageConfig *StorageConfig `yaml:"storage"`
|
StorageConfig *StorageConfig `yaml:"storage"`
|
||||||
StatusesConfig *StatusesConfig `yaml:"statuses"`
|
StatusesConfig *StatusesConfig `yaml:"statuses"`
|
||||||
|
LetsEncryptConfig *LetsEncryptConfig `yaml:"letsEncrypt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// FromFile returns a new config from a file, or an error if something goes amiss.
|
// 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
|
// Empty just returns a new empty config
|
||||||
func Empty() *Config {
|
func Empty() *Config {
|
||||||
return &Config{
|
return &Config{
|
||||||
DBConfig: &DBConfig{},
|
DBConfig: &DBConfig{},
|
||||||
TemplateConfig: &TemplateConfig{},
|
TemplateConfig: &TemplateConfig{},
|
||||||
AccountsConfig: &AccountsConfig{},
|
AccountsConfig: &AccountsConfig{},
|
||||||
MediaConfig: &MediaConfig{},
|
MediaConfig: &MediaConfig{},
|
||||||
StorageConfig: &StorageConfig{},
|
StorageConfig: &StorageConfig{},
|
||||||
StatusesConfig: &StatusesConfig{},
|
StatusesConfig: &StatusesConfig{},
|
||||||
|
LetsEncryptConfig: &LetsEncryptConfig{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -200,6 +202,19 @@ func (c *Config) ParseCLIFlags(f KeyedFlags) {
|
||||||
if c.StatusesConfig.MaxMediaFiles == 0 || f.IsSet(fn.StatusesMaxMediaFiles) {
|
if c.StatusesConfig.MaxMediaFiles == 0 || f.IsSet(fn.StatusesMaxMediaFiles) {
|
||||||
c.StatusesConfig.MaxMediaFiles = f.Int(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.
|
// 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
|
StatusesPollMaxOptions string
|
||||||
StatusesPollOptionMaxChars string
|
StatusesPollOptionMaxChars string
|
||||||
StatusesMaxMediaFiles string
|
StatusesMaxMediaFiles string
|
||||||
|
|
||||||
|
LetsEncryptEnabled string
|
||||||
|
LetsEncryptCertDir string
|
||||||
|
LetsEncryptEmailAddress string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Defaults contains all the default values for a gotosocial config
|
// Defaults contains all the default values for a gotosocial config
|
||||||
|
@ -288,6 +307,10 @@ type Defaults struct {
|
||||||
StatusesPollMaxOptions int
|
StatusesPollMaxOptions int
|
||||||
StatusesPollOptionMaxChars int
|
StatusesPollOptionMaxChars int
|
||||||
StatusesMaxMediaFiles int
|
StatusesMaxMediaFiles int
|
||||||
|
|
||||||
|
LetsEncryptEnabled bool
|
||||||
|
LetsEncryptCertDir string
|
||||||
|
LetsEncryptEmailAddress string
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFlagNames returns a struct containing the names of the various flags used for
|
// 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",
|
StatusesPollMaxOptions: "statuses-poll-max-options",
|
||||||
StatusesPollOptionMaxChars: "statuses-poll-option-max-chars",
|
StatusesPollOptionMaxChars: "statuses-poll-option-max-chars",
|
||||||
StatusesMaxMediaFiles: "statuses-max-media-files",
|
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",
|
StatusesPollMaxOptions: "GTS_STATUSES_POLL_MAX_OPTIONS",
|
||||||
StatusesPollOptionMaxChars: "GTS_STATUSES_POLL_OPTION_MAX_CHARS",
|
StatusesPollOptionMaxChars: "GTS_STATUSES_POLL_OPTION_MAX_CHARS",
|
||||||
StatusesMaxMediaFiles: "GTS_STATUSES_MAX_MEDIA_FILES",
|
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,
|
PollOptionMaxChars: defaults.StatusesPollOptionMaxChars,
|
||||||
MaxMediaFiles: defaults.StatusesMaxMediaFiles,
|
MaxMediaFiles: defaults.StatusesMaxMediaFiles,
|
||||||
},
|
},
|
||||||
|
LetsEncryptConfig: &LetsEncryptConfig{
|
||||||
|
Enabled: defaults.LetsEncryptEnabled,
|
||||||
|
CertDir: defaults.LetsEncryptCertDir,
|
||||||
|
EmailAddress: defaults.LetsEncryptEmailAddress,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,6 +98,11 @@ func Default() *Config {
|
||||||
PollOptionMaxChars: defaults.StatusesPollOptionMaxChars,
|
PollOptionMaxChars: defaults.StatusesPollOptionMaxChars,
|
||||||
MaxMediaFiles: defaults.StatusesMaxMediaFiles,
|
MaxMediaFiles: defaults.StatusesMaxMediaFiles,
|
||||||
},
|
},
|
||||||
|
LetsEncryptConfig: &LetsEncryptConfig{
|
||||||
|
Enabled: defaults.LetsEncryptEnabled,
|
||||||
|
CertDir: defaults.LetsEncryptCertDir,
|
||||||
|
EmailAddress: defaults.LetsEncryptEmailAddress,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,6 +145,10 @@ func GetDefaults() Defaults {
|
||||||
StatusesPollMaxOptions: 6,
|
StatusesPollMaxOptions: 6,
|
||||||
StatusesPollOptionMaxChars: 50,
|
StatusesPollOptionMaxChars: 50,
|
||||||
StatusesMaxMediaFiles: 6,
|
StatusesMaxMediaFiles: 6,
|
||||||
|
|
||||||
|
LetsEncryptEnabled: true,
|
||||||
|
LetsEncryptCertDir: "/gotosocial/storage/certs",
|
||||||
|
LetsEncryptEmailAddress: "",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,5 +190,9 @@ func GetTestDefaults() Defaults {
|
||||||
StatusesPollMaxOptions: 6,
|
StatusesPollMaxOptions: 6,
|
||||||
StatusesPollOptionMaxChars: 50,
|
StatusesPollOptionMaxChars: 50,
|
||||||
StatusesMaxMediaFiles: 6,
|
StatusesMaxMediaFiles: 6,
|
||||||
|
|
||||||
|
LetsEncryptEnabled: false,
|
||||||
|
LetsEncryptCertDir: "",
|
||||||
|
LetsEncryptEmailAddress: "",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -40,6 +40,7 @@ import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/message"
|
"github.com/superseriousbusiness/gotosocial/internal/message"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
|
@ -49,6 +50,28 @@ import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
"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
|
// Run creates and starts a gotosocial server
|
||||||
var Run action.GTSAction = func(ctx context.Context, c *config.Config, log *logrus.Logger) error {
|
var Run action.GTSAction = func(ctx context.Context, c *config.Config, log *logrus.Logger) error {
|
||||||
dbService, err := db.NewPostgresService(ctx, c, log)
|
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 {
|
if err := dbService.CreateInstanceAccount(); err != nil {
|
||||||
return fmt.Errorf("error creating instance account: %s", err)
|
return fmt.Errorf("error creating instance account: %s", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@ import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
|
"golang.org/x/crypto/acme/autocert"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Router provides the REST interface for gotosocial, using gin.
|
// 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
|
// router fulfils the Router interface using gin and logrus
|
||||||
type router struct {
|
type router struct {
|
||||||
logger *logrus.Logger
|
logger *logrus.Logger
|
||||||
engine *gin.Engine
|
engine *gin.Engine
|
||||||
srv *http.Server
|
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() {
|
func (r *router) Start() {
|
||||||
go func() {
|
if r.config.LetsEncryptConfig.Enabled {
|
||||||
if err := r.srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
// serve the http handler on port 80 for receiving letsencrypt requests and solving their devious riddles
|
||||||
r.logger.Fatalf("listen: %s", err)
|
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
|
// Stop shuts down the router nicely
|
||||||
|
@ -93,6 +119,8 @@ func New(config *config.Config, logger *logrus.Logger) (Router, error) {
|
||||||
default:
|
default:
|
||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// create the actual engine here -- this is the core request routing handler for gts
|
||||||
engine := gin.Default()
|
engine := gin.Default()
|
||||||
|
|
||||||
// create a new session store middleware
|
// 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)
|
logger.Debugf("loading templates from %s", tmPath)
|
||||||
engine.LoadHTMLGlob(tmPath)
|
engine.LoadHTMLGlob(tmPath)
|
||||||
|
|
||||||
return &router{
|
// create the actual http server here
|
||||||
logger: logger,
|
var s *http.Server
|
||||||
engine: engine,
|
var m *autocert.Manager
|
||||||
srv: &http.Server{
|
|
||||||
|
// 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",
|
Addr: ":8080",
|
||||||
Handler: engine,
|
Handler: engine,
|
||||||
},
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &router{
|
||||||
|
logger: logger,
|
||||||
|
engine: engine,
|
||||||
|
srv: s,
|
||||||
|
config: config,
|
||||||
|
certManager: m,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,3 +191,13 @@ func sessionStore() (memstore.Store, error) {
|
||||||
|
|
||||||
return memstore.NewStore(auth, crypt), nil
|
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…
Reference in New Issue