From 677490bc4e8d61627bcab32ed801c10a27139f29 Mon Sep 17 00:00:00 2001
From: Tobi Smethurst <31960611+tsmethurst@users.noreply.github.com>
Date: Mon, 19 Jul 2021 18:03:07 +0200
Subject: [PATCH] Db tls (#102)

* go mod tidy

* complete example config

* add tls support for db connection

* add certpool to tlsConfig

* add some lil docker scripts
---
 cmd/gotosocial/main.go     |  12 +++
 dockerbuild.sh             |   3 +
 dockerpush.sh              |   3 +
 example/config.yaml        | 161 ++++++++++++++++++++++++++++++++++++-
 go.mod                     |   1 -
 go.sum                     |   4 -
 internal/config/config.go  |  64 +++++++++------
 internal/config/db.go      |  33 ++++++--
 internal/config/default.go |  14 ++--
 internal/db/pg/pg.go       |  52 ++++++++++++
 10 files changed, 302 insertions(+), 45 deletions(-)
 create mode 100755 dockerbuild.sh
 create mode 100755 dockerpush.sh

diff --git a/cmd/gotosocial/main.go b/cmd/gotosocial/main.go
index 9729f7706..fde83e623 100644
--- a/cmd/gotosocial/main.go
+++ b/cmd/gotosocial/main.go
@@ -117,6 +117,18 @@ func main() {
 				Value:   defaults.DbDatabase,
 				EnvVars: []string{envNames.DbDatabase},
 			},
+			&cli.StringFlag{
+				Name:    flagNames.DbTLSMode,
+				Usage:   "Database tls mode",
+				Value:   defaults.DBTlsMode,
+				EnvVars: []string{envNames.DbTLSMode},
+			},
+			&cli.StringFlag{
+				Name:    flagNames.DbTLSCACert,
+				Usage:   "Path to CA cert for db tls connection",
+				Value:   defaults.DBTlsCACert,
+				EnvVars: []string{envNames.DbTLSCACert},
+			},
 
 			// TEMPLATE FLAGS
 			&cli.StringFlag{
diff --git a/dockerbuild.sh b/dockerbuild.sh
new file mode 100755
index 000000000..87893c65c
--- /dev/null
+++ b/dockerbuild.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+docker build -t "superseriousbusiness/gotosocial:$(cat version)" .
diff --git a/dockerpush.sh b/dockerpush.sh
new file mode 100755
index 000000000..8377f8e4a
--- /dev/null
+++ b/dockerpush.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+docker push "superseriousbusiness/gotosocial:$(cat version)"
diff --git a/example/config.yaml b/example/config.yaml
index 81e4727e2..b26322812 100644
--- a/example/config.yaml
+++ b/example/config.yaml
@@ -18,7 +18,7 @@
 ##### GENERAL CONFIG ######
 ###########################
 # String. Log level to use throughout the application. Must be lower-case.
-# Options: ["debug","info","warn","error","fatal"]
+# Options: ["trace","debug","info","warn","error","fatal"]
 # Default: "info"
 logLevel: "info"
 
@@ -66,14 +66,29 @@ db:
   # REQUIRED
   # String. Password to use for the database connection
   # Examples: ["password123","verysafepassword","postgres"]
-  # Default: ""
-  password: ""
+  # Default: "postgres"
+  password: "postgres"
 
   # String. Name of the database to use within the provided database type.
   # Examples: ["mydb","postgres","gotosocial"]
   # Default: "postgres"
   database: "postgres"
 
+  # String. Disable, enable, or require SSL/TLS connection to the database.
+  # If "disable" then no TLS connection will be attempted.
+  # If "enable" then TLS will be tried, but the database certificate won't be checked (for self-signed certs).
+  # If "require" then TLS will be required to make a connection, and a valid certificate must be presented.
+  # Options: ["disable", "enable", "require"]
+  # Default: "disable"
+  tlsMode: "disable"
+
+  # String. Path to a CA certificate on the host machine for db certificate validation.
+  # If this is left empty, just the host certificates will be used.
+  # If filled in, the certificate will be loaded and added to host certificates.
+  # Examples: ["/path/to/some/cert.crt"]
+  # Default: ""
+  tlsCACert: ""
+
 ###############################
 ##### WEB TEMPLATE CONFIG #####
 ###############################
@@ -84,6 +99,11 @@ template:
   # Default: "./web/template/"
   baseDir: "./web/template/"
 
+  # String. Directory from which gotosocial will attempt to serve static web assets (images, scripts).
+  # Examples: ["/some/absolute/path/", "./relative/path/", "../../some/weird/path/"]
+  # Default: "./web/assets/"
+  assetBaseDir: "./web/assets/"
+
 ###########################
 ##### ACCOUNTS CONFIG #####
 ###########################
@@ -93,7 +113,142 @@ accounts:
   # Options: [true, false]
   # Default: true
   openRegistration: true
+
   # Bool. Do sign up requests require approval from an admin/moderator before an account can sign in/use the server?
   # Options: [true, false]
   # Default: true
   requireApproval: true
+
+  # Bool. Are sign up requests required to submit a reason for the request (eg., an explanation of why they want to join the instance)?
+  # Options: [true, false]
+  # Default: true
+  reasonRequired: true
+
+########################
+##### MEDIA CONFIG #####
+########################
+# Config pertaining to user media uploads (videos, image, image descriptions).
+media:
+  # Int. Maximum allowed image upload size in bytes.
+  # Examples: [2097152, 10485760]
+  # Default: 2097152 -- aka 2MB
+  maxImageSize: 2097152
+
+  # Int. Maximum allowed video upload size in bytes.
+  # Examples: [2097152, 10485760]
+  # Default: 10485760 -- aka 10MB
+  maxVideoSize: 10485760
+
+  # Int. Minimum amount of characters required as an image or video description.
+  # Examples: [500, 1000, 1500]
+  # Default: 0 (not required)
+  minDescriptionChars: 0
+
+  # Int. Maximum amount of characters permitted in an image or video description.
+  # Examples: [500, 1000, 1500]
+  # Default: 500
+  maxDescriptionChars: 500
+
+##########################
+##### STORAGE CONFIG #####
+##########################
+# Config pertaining to storage of user-created uploads (videos, images, etc).
+storage:
+  # String. Type of storage backend to use.
+  # Examples: ["local", "s3"]
+  # Default: "local" (storage on local disk)
+  # NOTE: s3 storage is not yet supported!
+  backend: "local"
+
+  # String. Directory to use as a base path for storing files.
+  # Make sure whatever user/group gotosocial is running as has permission to access
+  # this directly, and create new subdirectories and files with in.
+  # Examples: ["/home/gotosocial/storage", "/opt/gotosocial/datastorage"]
+  # Default: "/gotosocial/storage"
+  basePath: "/gotosocial/storage"
+
+  # String. Protocol to use for serving stored files.
+  # It's very unlikely that you'll need to change this ever, but there might be edge cases.
+  # Examples: ["http", "https"]
+  serveProtocol: "https"
+
+  # String. Host for serving stored files.
+  # If you're using local storage, this should be THE SAME as the value you've set for Host, above.
+  # It should only be a different value if you're serving stored files from a host
+  # other than the one your instance is running on.
+  # Examples: ["localhost", "example.org"]
+  # Default: "localhost" -- you should absolutely change this.
+  serveHost: "localhost"
+
+  # String. Base path for serving stored files. This will be added to serveHost and serveProtocol
+  # to form the prefix url of your stored files. Eg., https://example.org/fileserver/.....
+  # It's unlikely that you will need to change this.
+  # Examples: ["/fileserver", "/media"]
+  # Default: "/fileserver"
+  serveBasePath: "/fileserver"
+
+###########################
+##### STATUSES CONFIG #####
+###########################
+# Config pertaining to the creation of statuses/posts, and permitted limits.
+statuses:
+  # Int. Maximum amount of characters permitted for a new status.
+  # Note that going way higher than the default might break federation.
+  # Examples: [140, 500, 5000]
+  # Default: 5000
+  maxChars: 5000
+
+  # Int. Maximum amount of characters allowed in the CW/subject header of a status.
+  # Note that going way higher than the default might break federation.
+  # Examples: [100, 200]
+  # Default: 100
+  cwMaxChars: 100
+
+  # Int. Maximum amount of options to permit when creating a new poll.
+  # Note that going way higher than the default might break federation.
+  # Examples: [4, 6, 10]
+  # Default: 6
+  pollMaxOptions: 6
+
+  # Int. Maximum amount of characters to permit per poll option when creating a new poll.
+  # Note that going way higher than the default might break federation.
+  # Examples: [50, 100, 150]
+  # Default: 50
+  pollOptionMaxChars: 50
+
+  # Int. Maximum amount of media files that can be attached to a new status.
+  # Note that going way higher than the default might break federation.
+  # Examples: [4, 6, 10]
+  # Default: 6
+  maxMediaFiles: 6
+
+##############################
+##### LETSENCRYPT CONFIG #####
+##############################
+# Config pertaining to the automatic acquisition and use of LetsEncrypt HTTPS certificates.
+letsEncrypt:
+  # Bool. Whether or not letsencrypt should be enabled for the server.
+  # If true, the server will serve on port 443 (https) and obtain letsencrypt
+  # certificates automatically.
+  # If false, the server will serve on port 8080 (http), and the rest of the settings
+  # here will be ignored.
+  # You should only change this if you want to serve GoToSocial behind a reverse proxy
+  # like Traefik, HAProxy, or Nginx.
+  # Options: [true, false]
+  # Default: true 
+  enabled: true
+
+  # String. Directory in which to store LetsEncrypt certificates.
+  # It is a good move to make this a sub-path within your storage directory, as it makes
+  # backup easier, but you might wish to move them elsewhere if they're also accessed by other services.
+  # In any case, make sure GoToSocial has permissions to write to / read from this directory.
+  # Examples: ["/home/gotosocial/storage/certs", "/acmecerts"]
+  # Default: "/gotosocial/storage/certs"
+  certDir: "/gotosocial/storage/certs"
+
+  # String. Email address to use when registering LetsEncrypt certs.
+  # Most likely, this will be the email address of the instance administrator.
+  # LetsEncrypt will send notifications about expiring certificates etc to this address.
+  # Examples: ["admin@example.org"]
+  # Default: ""
+  emailAddress: ""
diff --git a/go.mod b/go.mod
index 31dfa9fbb..47b27f35e 100644
--- a/go.mod
+++ b/go.mod
@@ -15,7 +15,6 @@ require (
 	github.com/dsoprea/go-utility v0.0.0-20200717064901-2fccff4aa15e // indirect
 	github.com/gin-contrib/cors v1.3.1
 	github.com/gin-contrib/sessions v0.0.3
-	github.com/gin-contrib/static v0.0.1
 	github.com/gin-gonic/gin v1.7.2
 	github.com/go-errors/errors v1.4.0 // indirect
 	github.com/go-fed/activity v1.0.1-0.20210426194615-e0de0863dcc1
diff --git a/go.sum b/go.sum
index 6926a115a..08ee5d58e 100644
--- a/go.sum
+++ b/go.sum
@@ -69,10 +69,7 @@ github.com/gin-contrib/sessions v0.0.3 h1:PoBXki+44XdJdlgDqDrY5nDVe3Wk7wDV/UCOuL
 github.com/gin-contrib/sessions v0.0.3/go.mod h1:8C/J6cad3Il1mWYYgtw0w+hqasmpvy25mPkXdOgeB9I=
 github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
 github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
-github.com/gin-contrib/static v0.0.1 h1:JVxuvHPuUfkoul12N7dtQw7KRn/pSMq7Ue1Va9Swm1U=
-github.com/gin-contrib/static v0.0.1/go.mod h1:CSxeF+wep05e0kCOsqWdAWbSszmc31zTIbD8TvWl7Hs=
 github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
-github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
 github.com/gin-gonic/gin v1.7.2 h1:Tg03T9yM2xa8j6I3Z3oqLaQRSmKvxPd6g/2HJ6zICFA=
 github.com/gin-gonic/gin v1.7.2/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
 github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
@@ -101,7 +98,6 @@ github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTM
 github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
 github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
 github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
-github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
 github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
 github.com/go-playground/validator/v10 v10.6.1 h1:W6TRDXt4WcWp4c4nf/G+6BkGdhiIo0k417gfr+V6u4I=
 github.com/go-playground/validator/v10 v10.6.1/go.mod h1:xm76BBt941f7yWdGnI2DVPFFg1UK3YY04qifoXU3lOk=
diff --git a/internal/config/config.go b/internal/config/config.go
index 28bbc8542..323b7de81 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -165,6 +165,14 @@ func (c *Config) ParseCLIFlags(f KeyedFlags, version string) error {
 		c.DBConfig.Database = f.String(fn.DbDatabase)
 	}
 
+	if c.DBConfig.TLSMode == DBTLSModeUnset || f.IsSet(fn.DbTLSMode) {
+		c.DBConfig.TLSMode = DBTLSMode(f.String(fn.DbTLSMode))
+	}
+
+	if c.DBConfig.TLSCACert == "" || f.IsSet(fn.DbTLSCACert) {
+		c.DBConfig.TLSCACert = f.String(fn.DbTLSCACert)
+	}
+
 	// template flags
 	if c.TemplateConfig.BaseDir == "" || f.IsSet(fn.TemplateBaseDir) {
 		c.TemplateConfig.BaseDir = f.String(fn.TemplateBaseDir)
@@ -284,12 +292,14 @@ type Flags struct {
 	Host            string
 	Protocol        string
 
-	DbType     string
-	DbAddress  string
-	DbPort     string
-	DbUser     string
-	DbPassword string
-	DbDatabase string
+	DbType      string
+	DbAddress   string
+	DbPort      string
+	DbUser      string
+	DbPassword  string
+	DbDatabase  string
+	DbTLSMode   string
+	DbTLSCACert string
 
 	TemplateBaseDir string
 	AssetBaseDir    string
@@ -329,12 +339,14 @@ type Defaults struct {
 	Protocol        string
 	SoftwareVersion string
 
-	DbType     string
-	DbAddress  string
-	DbPort     int
-	DbUser     string
-	DbPassword string
-	DbDatabase string
+	DbType      string
+	DbAddress   string
+	DbPort      int
+	DbUser      string
+	DbPassword  string
+	DbDatabase  string
+	DBTlsMode   string
+	DBTlsCACert string
 
 	TemplateBaseDir string
 	AssetBaseDir    string
@@ -375,12 +387,14 @@ func GetFlagNames() Flags {
 		Host:            "host",
 		Protocol:        "protocol",
 
-		DbType:     "db-type",
-		DbAddress:  "db-address",
-		DbPort:     "db-port",
-		DbUser:     "db-user",
-		DbPassword: "db-password",
-		DbDatabase: "db-database",
+		DbType:      "db-type",
+		DbAddress:   "db-address",
+		DbPort:      "db-port",
+		DbUser:      "db-user",
+		DbPassword:  "db-password",
+		DbDatabase:  "db-database",
+		DbTLSMode:   "db-tls-mode",
+		DbTLSCACert: "db-tls-ca-cert",
 
 		TemplateBaseDir: "template-basedir",
 		AssetBaseDir:    "asset-basedir",
@@ -422,12 +436,14 @@ func GetEnvNames() Flags {
 		Host:            "GTS_HOST",
 		Protocol:        "GTS_PROTOCOL",
 
-		DbType:     "GTS_DB_TYPE",
-		DbAddress:  "GTS_DB_ADDRESS",
-		DbPort:     "GTS_DB_PORT",
-		DbUser:     "GTS_DB_USER",
-		DbPassword: "GTS_DB_PASSWORD",
-		DbDatabase: "GTS_DB_DATABASE",
+		DbType:      "GTS_DB_TYPE",
+		DbAddress:   "GTS_DB_ADDRESS",
+		DbPort:      "GTS_DB_PORT",
+		DbUser:      "GTS_DB_USER",
+		DbPassword:  "GTS_DB_PASSWORD",
+		DbDatabase:  "GTS_DB_DATABASE",
+		DbTLSMode:   "GTS_DB_TLS_MODE",
+		DbTLSCACert: "GTS_DB_CA_CERT",
 
 		TemplateBaseDir: "GTS_TEMPLATE_BASEDIR",
 		AssetBaseDir:    "GTS_ASSET_BASEDIR",
diff --git a/internal/config/db.go b/internal/config/db.go
index fbde6fe82..7ea71a8b6 100644
--- a/internal/config/db.go
+++ b/internal/config/db.go
@@ -20,11 +20,30 @@ package config
 
 // DBConfig provides configuration options for the database connection
 type DBConfig struct {
-	Type            string `yaml:"type"`
-	Address         string `yaml:"address"`
-	Port            int    `yaml:"port"`
-	User            string `yaml:"user"`
-	Password        string `yaml:"password"`
-	Database        string `yaml:"database"`
-	ApplicationName string `yaml:"applicationName"`
+	Type            string    `yaml:"type"`
+	Address         string    `yaml:"address"`
+	Port            int       `yaml:"port"`
+	User            string    `yaml:"user"`
+	Password        string    `yaml:"password"`
+	Database        string    `yaml:"database"`
+	ApplicationName string    `yaml:"applicationName"`
+	TLSMode         DBTLSMode `yaml:"tlsMode"`
+	TLSCACert       string    `yaml:"tlsCACert"`
 }
+
+// DBTLSMode describes a mode of connecting to a database with or without TLS.
+type DBTLSMode string
+
+// DBTLSModeDisable does not attempt to make a TLS connection to the database.
+var DBTLSModeDisable DBTLSMode = "disable"
+
+// DBTLSModeEnable attempts to make a TLS connection to the database, but doesn't fail if
+// the certificate passed by the database isn't verified.
+var DBTLSModeEnable DBTLSMode = "enable"
+
+// DBTLSModeRequire attempts to make a TLS connection to the database, and requires
+// that the certificate presented by the database is valid.
+var DBTLSModeRequire DBTLSMode = "require"
+
+// DBTLSModeUnset means that the TLS mode has not been set.
+var DBTLSModeUnset DBTLSMode = ""
diff --git a/internal/config/default.go b/internal/config/default.go
index 40df4c57e..7a030beb5 100644
--- a/internal/config/default.go
+++ b/internal/config/default.go
@@ -120,12 +120,14 @@ func GetDefaults() Defaults {
 		Host:            "",
 		Protocol:        "https",
 
-		DbType:     "postgres",
-		DbAddress:  "localhost",
-		DbPort:     5432,
-		DbUser:     "postgres",
-		DbPassword: "postgres",
-		DbDatabase: "postgres",
+		DbType:      "postgres",
+		DbAddress:   "localhost",
+		DbPort:      5432,
+		DbUser:      "postgres",
+		DbPassword:  "postgres",
+		DbDatabase:  "postgres",
+		DBTlsMode:   "disable",
+		DBTlsCACert: "",
 
 		TemplateBaseDir: "./web/template/",
 		AssetBaseDir:    "./web/assets/",
diff --git a/internal/db/pg/pg.go b/internal/db/pg/pg.go
index ad75cef15..5301f0410 100644
--- a/internal/db/pg/pg.go
+++ b/internal/db/pg/pg.go
@@ -22,10 +22,14 @@ import (
 	"context"
 	"crypto/rand"
 	"crypto/rsa"
+	"crypto/tls"
+	"crypto/x509"
+	"encoding/pem"
 	"errors"
 	"fmt"
 	"net"
 	"net/mail"
+	"os"
 	"strings"
 	"time"
 
@@ -133,6 +137,53 @@ func derivePGOptions(c *config.Config) (*pg.Options, error) {
 		return nil, errors.New("no database set")
 	}
 
+	var tlsConfig *tls.Config
+	switch c.DBConfig.TLSMode {
+	case config.DBTLSModeDisable, config.DBTLSModeUnset:
+		break // nothing to do
+	case config.DBTLSModeEnable:
+		tlsConfig = &tls.Config{
+			InsecureSkipVerify: true,
+		}
+	case config.DBTLSModeRequire:
+		tlsConfig = &tls.Config{
+			InsecureSkipVerify: false,
+		}
+	}
+
+	if tlsConfig != nil && c.DBConfig.TLSCACert != "" {
+		// load the system cert pool first -- we'll append the given CA cert to this
+		certPool, err := x509.SystemCertPool()
+		if err != nil {
+			return nil, fmt.Errorf("error fetching system CA cert pool: %s", err)
+		}
+
+		// open the file itself and make sure there's something in it
+		caCertBytes, err := os.ReadFile(c.DBConfig.TLSCACert)
+		if err != nil {
+			return nil, fmt.Errorf("error opening CA certificate at %s: %s", c.DBConfig.TLSCACert, err)
+		}
+		if len(caCertBytes) == 0 {
+			return nil, fmt.Errorf("ca cert at %s was empty", c.DBConfig.TLSCACert)
+		}
+
+		// make sure we have a PEM block
+		caPem, _ := pem.Decode(caCertBytes)
+		if caPem == nil {
+			return nil, fmt.Errorf("could not parse cert at %s into PEM", c.DBConfig.TLSCACert)
+		}
+
+		// parse the PEM block into the certificate
+		caCert, err := x509.ParseCertificate(caPem.Bytes)
+		if err != nil {
+			return nil, fmt.Errorf("could not parse cert at %s into x509 certificate: %s", c.DBConfig.TLSCACert, err)
+		}
+
+		// we're happy, add it to the existing pool and then use this pool in our tls config
+		certPool.AddCert(caCert)
+		tlsConfig.RootCAs = certPool
+	}
+
 	// We can rely on the pg library we're using to set
 	// sensible defaults for everything we don't set here.
 	options := &pg.Options{
@@ -141,6 +192,7 @@ func derivePGOptions(c *config.Config) (*pg.Options, error) {
 		Password:        c.DBConfig.Password,
 		Database:        c.DBConfig.Database,
 		ApplicationName: c.ApplicationName,
+		TLSConfig:       tlsConfig,
 	}
 
 	return options, nil