website/pkg/config/variable.go
Noah Petherbridge 7869ff83ba Signed and Authenticated Static Photo URLs
* Add support for authenticated static photo URLs, leveraging the NGINX module
  ngx_http_auth_request. The README is updated with an example NGINX config
  how to set this up on the proxy side.
* In settings.json a new SignedPhoto section is added: not enabled by default.
* PhotoURL will append a ?jwt= token to the /static/photos/ path for the
  current user, which expires after 30 seconds.
* When SignedPhoto is enabled, it will enforce that the JWT token is valid and
  matches the username of the current logged-in user, or else will return with
  a 403 Forbidden error.
2024-10-03 18:04:14 -07:00

224 lines
5.5 KiB
Go

package config
import (
"bytes"
"encoding/json"
"fmt"
"os"
"code.nonshy.com/nonshy/website/pkg/encryption/coldstorage"
"code.nonshy.com/nonshy/website/pkg/encryption/keygen"
"code.nonshy.com/nonshy/website/pkg/log"
"github.com/SherClockHolmes/webpush-go"
"github.com/google/uuid"
)
// Version of the config format - when new fields are added, it will attempt
// to write the settings.toml to disk so new defaults populate.
var currentVersion = 5
// Current loaded settings.json
var Current = DefaultVariable()
// Variable configuration attributes (loaded from settings.json).
type Variable struct {
Version int
BaseURL string
AdminEmail string
CronAPIKey string
Mail Mail
Redis Redis
Database Database
BareRTC BareRTC
Maintenance Maintenance
Encryption Encryption
SignedPhoto SignedPhoto
WebPush WebPush
Turnstile Turnstile
UseXForwardedFor bool
}
// DefaultVariable returns the default settings.json data.
func DefaultVariable() Variable {
return Variable{
BaseURL: "http://localhost:8080",
Mail: Mail{
Enabled: false,
Host: "localhost",
Port: 25,
From: "no-reply@localhost",
},
Redis: Redis{
Host: "localhost",
Port: 6379,
},
Database: Database{
SQLite: "database.sqlite",
Postgres: "host=localhost user=nonshy password=nonshy dbname=nonshy port=5679 sslmode=disable TimeZone=America/Los_Angeles",
},
CronAPIKey: uuid.New().String(),
}
}
// LoadSettings loads the settings.json file or, if not existing, creates it with the default settings.
func LoadSettings() {
var writeSettings bool
if _, err := os.Stat(SettingsPath); !os.IsNotExist(err) {
log.Info("Loading settings from %s", SettingsPath)
content, err := os.ReadFile(SettingsPath)
if err != nil {
panic(fmt.Sprintf("LoadSettings: couldn't read settings.json: %s", err))
}
var v Variable
err = json.Unmarshal(content, &v)
if err != nil {
panic(fmt.Sprintf("LoadSettings: couldn't parse settings.json: %s", err))
}
Current = v
} else {
WriteSettings()
log.Warn("NOTICE: Created default settings.json file - review it and configure mail servers and database!")
}
// If there is no DB configured, exit now.
if !Current.Database.IsSQLite && !Current.Database.IsPostgres {
log.Error("No database configured in settings.json. Choose SQLite or Postgres and update the DB connector string!")
os.Exit(1)
}
// Initialize the AES encryption key.
if len(Current.Encryption.AESKey) == 0 {
log.Warn("NOTICE: rolling a random 32-byte (256-bit) AES encryption key for the settings file")
aesKey, err := keygen.NewAESKey()
if err != nil {
log.Error("Couldn't generate AES key: %s", err)
os.Exit(1)
}
Current.Encryption.AESKey = aesKey
writeSettings = true
}
// Initialize the cold storage RSA keys.
if len(Current.Encryption.ColdStorageRSAPublicKey) == 0 {
x509publicKey, err := coldstorage.Initialize()
if err != nil {
log.Error("Initializing cold storage: %s", err)
os.Exit(1)
}
// Store the public key in the settings.json.
Current.Encryption.ColdStorageRSAPublicKey = x509publicKey
writeSettings = true
}
// Initialize the VAPID keys for Web Push Notification.
if len(Current.WebPush.VAPIDPublicKey) == 0 {
privateKey, publicKey, err := webpush.GenerateVAPIDKeys()
if err != nil {
log.Error("Initializing VAPID keys for Web Push: %s", err)
os.Exit(1)
}
Current.WebPush.VAPIDPrivateKey = privateKey
Current.WebPush.VAPIDPublicKey = publicKey
writeSettings = true
}
// Initialize JWT token for SignedPhoto feature.
if Current.SignedPhoto.JWTSecret == "" {
Current.SignedPhoto.JWTSecret = uuid.New().String()
writeSettings = true
}
// Have we added new config fields? Save the settings.json.
if Current.Version != currentVersion || writeSettings {
log.Warn("New options are available for your settings.json file. Your settings will be re-saved now.")
Current.Version = currentVersion
if err := WriteSettings(); err != nil {
log.Error("Couldn't write your settings.json file: %s", err)
}
}
}
// WriteSettings will commit the settings.json to disk.
func WriteSettings() error {
log.Error("Note: initial settings.json was written to disk.")
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetIndent("", " ")
err := enc.Encode(Current)
if err != nil {
panic(fmt.Sprintf("WriteSettings: couldn't marshal settings: %s", err))
}
return os.WriteFile(SettingsPath, buf.Bytes(), 0600)
}
// Mail settings.
type Mail struct {
Enabled bool
Host string // localhost
Port int // 25
From string // noreply@localhost
Username string // SMTP credentials
Password string
}
// Redis settings.
type Redis struct {
Host string
Port int
DB int
}
// Database settings.
type Database struct {
IsSQLite bool
IsPostgres bool
SQLite string
Postgres string
}
// BareRTC chat room settings.
type BareRTC struct {
JWTSecret string
URL string
}
// Maintenance mode settings.
type Maintenance struct {
PauseSignup bool
PauseLogin bool
PauseChat bool
PauseInteraction bool
}
// Encryption settings.
type Encryption struct {
AESKey []byte
ColdStorageRSAPublicKey []byte
}
// SignedPhoto settings.
type SignedPhoto struct {
Enabled bool
JWTSecret string
}
// WebPush settings.
type WebPush struct {
VAPIDPublicKey string
VAPIDPrivateKey string
}
// Turnstile (Cloudflare CAPTCHA) settings.
type Turnstile struct {
Enabled bool
SiteKey string
SecretKey string
}