diff --git a/README.md b/README.md index 243bbba..04f5996 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,15 @@ vim config.toml go run cmd/sea/main.go > sea.conf ``` +### TLS / Let's Encrypt + +If you want to run SEA behind HTTPS using Certbot, set `listen_ssl`, +`ssl_certificate`, and `ssl_certificate_key` in `config.toml`. Enable the +`letsencrypt` and `redirect_http` flags to generate a second server block for +port 80 that redirects to HTTPS. The resulting configuration can be safely +included in your `nginx` setup, and Certbot may modify only the certificate +paths when certificates renew. + ## Development Requirements - Go >= 1.22 diff --git a/cmd/sea/main.go b/cmd/sea/main.go index 5f6a781..ffe56e9 100644 --- a/cmd/sea/main.go +++ b/cmd/sea/main.go @@ -14,9 +14,14 @@ import ( // Config represents the TOML configuration structure. type Config struct { - Listen int `toml:"listen"` - ServerName string `toml:"server_name"` - CustomKeywords []KeywordRule `toml:"custom_keywords"` + Listen int `toml:"listen"` + ListenSSL int `toml:"listen_ssl"` + ServerName string `toml:"server_name"` + SSLCertificate string `toml:"ssl_certificate"` + SSLCertificateKey string `toml:"ssl_certificate_key"` + LetsEncrypt bool `toml:"letsencrypt"` + RedirectHTTP bool `toml:"redirect_http"` + CustomKeywords []KeywordRule `toml:"custom_keywords"` } // TemplateData combines the parsed configuration with the destination @@ -56,7 +61,11 @@ func main() { } func loadConfig(path string) (Config, error) { - cfg := Config{Listen: 80, ServerName: "search.localhost"} + cfg := Config{ + Listen: 80, + ListenSSL: 0, + ServerName: "search.localhost", + } data, err := os.ReadFile(path) if err != nil { if os.IsNotExist(err) { diff --git a/cmd/sea/main_test.go b/cmd/sea/main_test.go index e157e1b..59c62cb 100644 --- a/cmd/sea/main_test.go +++ b/cmd/sea/main_test.go @@ -18,8 +18,15 @@ func TestLoadConfig(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "cfg.toml") data := []byte(`listen = 8080 +listen_ssl = 443 server_name = "example.com" +redirect_http = true + +ssl_certificate = "/tmp/full.pem" +ssl_certificate_key = "/tmp/key.pem" +letsencrypt = true + [[custom_keywords]] phrase = "foo bar" dest = "google"`) @@ -33,6 +40,18 @@ dest = "google"`) if cfg.Listen != 8080 { t.Errorf("expected listen 8080, got %d", cfg.Listen) } + if cfg.ListenSSL != 443 { + t.Errorf("expected listen_ssl 443, got %d", cfg.ListenSSL) + } + if !cfg.RedirectHTTP { + t.Error("expected redirect_http true") + } + if cfg.SSLCertificate != "/tmp/full.pem" || cfg.SSLCertificateKey != "/tmp/key.pem" { + t.Errorf("unexpected certificate paths %+v", cfg) + } + if !cfg.LetsEncrypt { + t.Error("expected letsencrypt true") + } if cfg.ServerName != "example.com" { t.Errorf("expected server name example.com, got %s", cfg.ServerName) } @@ -45,7 +64,16 @@ dest = "google"`) } func TestGenerateNginx(t *testing.T) { - cfg := Config{Listen: 8080, ServerName: "example.com", CustomKeywords: []KeywordRule{{Phrase: "foo", Dest: "google"}}} + cfg := Config{ + Listen: 8080, + ListenSSL: 8443, + ServerName: "example.com", + RedirectHTTP: true, + CustomKeywords: []KeywordRule{{ + Phrase: "foo", + Dest: "google", + }}, + } out, err := generateNginx(cfg) if err != nil { t.Fatalf("generateNginx returned error: %v", err) @@ -53,6 +81,9 @@ func TestGenerateNginx(t *testing.T) { if !strings.Contains(out, "server_name example.com;") { t.Errorf("generated config missing server name: %s", out) } + if !strings.Contains(out, "listen 8443 ssl") { + t.Errorf("generated config missing ssl server: %s", out) + } if !strings.Contains(out, "~*(?i)^foo$") { t.Errorf("generated config missing custom rule: %s", out) } diff --git a/cmd/sea/nginx.conf.tmpl b/cmd/sea/nginx.conf.tmpl index a32e4de..96850ff 100644 --- a/cmd/sea/nginx.conf.tmpl +++ b/cmd/sea/nginx.conf.tmpl @@ -41,6 +41,40 @@ map $dest $target { {{- end }} } +{{ if gt .ListenSSL 0 }} +server { + listen {{ .ListenSSL }} ssl http2; + server_name {{ .ServerName }}; +{{- if .SSLCertificate }} + ssl_certificate {{ .SSLCertificate }}; + ssl_certificate_key {{ .SSLCertificateKey }}; +{{- if .LetsEncrypt }} + include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; +{{- end }} +{{- end }} + + location / { + return 302 $target; + } +} +{{ if .RedirectHTTP }} +server { + listen {{ .Listen }}; + server_name {{ .ServerName }}; + return 301 https://$host$request_uri; +} +{{ else }} +server { + listen {{ .Listen }}; + server_name {{ .ServerName }}; + + location / { + return 302 $target; + } +} +{{ end }} +{{ else }} server { listen {{ .Listen }}; server_name {{ .ServerName }}; @@ -49,4 +83,5 @@ server { return 302 $target; } } +{{ end }} diff --git a/config-example.toml b/config-example.toml index 35d7486..2ec545a 100644 --- a/config-example.toml +++ b/config-example.toml @@ -1,5 +1,10 @@ -listen = 3001 +listen = 80 +listen_ssl = 443 server_name = "your.site" +# ssl_certificate = "/etc/letsencrypt/live/your.site/fullchain.pem" +# ssl_certificate_key = "/etc/letsencrypt/live/your.site/privkey.pem" +# letsencrypt = true +# redirect_http = true [[custom_keywords]] phrase = "nginx" diff --git a/nginx.conf b/nginx.conf index 5712a7b..4e343cb 100644 --- a/nginx.conf +++ b/nginx.conf @@ -40,6 +40,7 @@ map $dest $target { wikipedia https://en.wikipedia.org/wiki/$arg_q; } + server { listen 80; server_name search.localhost; @@ -49,3 +50,4 @@ server { } } +