summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMartin Fischer <martin@push-f.com>2025-04-06 09:40:30 +0200
committerMartin Fischer <martin@push-f.com>2025-04-13 23:18:01 +0200
commit281d0a867bc542bef52d5091fed3bf1b0d3b9ef6 (patch)
treedff4cb69b1560655257b44190a17f99ed5b5f62b
parent50ea018252ce69542eab6a107b99ea8179810d1e (diff)
build: introduce Nix package and NixOS service
-rw-r--r--.gitignore1
-rw-r--r--cc-tlds.txt251
-rw-r--r--default.nix13
-rw-r--r--infra/README.md24
-rw-r--r--infra/ccTLDs183
-rw-r--r--infra/ccTLDs284
-rw-r--r--infra/ccTLDs384
-rwxr-xr-xinfra/githooks/post-receive4
-rw-r--r--infra/lexsurf.service11
-rw-r--r--infra/nginx/lex.surf_dev35
-rw-r--r--infra/nginx/lex.surf_prod126
-rw-r--r--lex-serve/main.go17
-rw-r--r--service.nix139
13 files changed, 419 insertions, 453 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c4a847d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/result
diff --git a/cc-tlds.txt b/cc-tlds.txt
new file mode 100644
index 0000000..3147b3e
--- /dev/null
+++ b/cc-tlds.txt
@@ -0,0 +1,251 @@
+ac
+ad
+ae
+af
+ag
+ai
+al
+am
+ao
+aq
+ar
+as
+at
+au
+aw
+ax
+az
+ba
+bb
+bd
+be
+bf
+bg
+bh
+bi
+bj
+bl
+bm
+bn
+bo
+bq
+br
+bs
+bt
+bv
+bw
+by
+bz
+ca
+cc
+cd
+cf
+cg
+ch
+ci
+ck
+cl
+cm
+cn
+co
+cr
+cu
+cv
+cw
+cx
+cy
+cz
+de
+dj
+dk
+dm
+do
+dz
+ec
+ee
+eg
+eh
+er
+es
+et
+eu
+fi
+fj
+fk
+fm
+fo
+fr
+ga
+gd
+ge
+gf
+gg
+gh
+gi
+gl
+gm
+gn
+gp
+gq
+gr
+gs
+gt
+gu
+gw
+gy
+hk
+hm
+hn
+hr
+ht
+hu
+id
+ie
+il
+im
+in
+io
+iq
+ir
+is
+it
+je
+jm
+jo
+jp
+ke
+kg
+kh
+ki
+km
+kn
+kp
+kr
+kw
+ky
+kz
+la
+lb
+lc
+li
+lk
+lr
+ls
+lt
+lu
+lv
+ly
+ma
+mc
+md
+me
+mf
+mg
+mh
+mk
+ml
+mm
+mn
+mo
+mp
+mq
+mr
+ms
+mt
+mu
+mv
+mw
+mx
+my
+mz
+na
+nc
+ne
+nf
+ng
+ni
+nl
+no
+np
+nr
+nu
+nz
+om
+pa
+pe
+pf
+pg
+ph
+pk
+pl
+pm
+pn
+pr
+ps
+pt
+pw
+py
+qa
+re
+ro
+rs
+ru
+rw
+sa
+sb
+sc
+sd
+se
+sg
+sh
+si
+sj
+sk
+sl
+sm
+sn
+so
+sr
+ss
+st
+su
+sv
+sx
+sy
+sz
+tc
+td
+tf
+tg
+th
+tj
+tk
+tl
+tm
+tn
+to
+tr
+tt
+tv
+tw
+tz
+ua
+ug
+uk
+us
+uy
+uz
+va
+vc
+ve
+vg
+vi
+vn
+vu
+wf
+ws
+ye
+yt
+za
+zm
+zw
diff --git a/default.nix b/default.nix
new file mode 100644
index 0000000..c93187e
--- /dev/null
+++ b/default.nix
@@ -0,0 +1,13 @@
+{ pkgs ? import <nixpkgs> {} }:
+
+pkgs.buildGoModule {
+ pname = "lex-surf";
+ version = "git";
+ src = ./.;
+ vendorHash = "sha256-8FDvleOMW1EzFQ116fQR7iKsETj9ZhXC7LFgAbSoLM4=";
+
+ postInstall = ''
+ cp -r lex-serve/assets $out/
+ cp -r laws $out/
+ '';
+}
diff --git a/infra/README.md b/infra/README.md
deleted file mode 100644
index 5b76059..0000000
--- a/infra/README.md
+++ /dev/null
@@ -1,24 +0,0 @@
-# Infrastructure
-
-Let's encrypt only supports up to 100 domains
-per certificate so the country TLDs are split up
-into three files:
-
-* ccTLDs1
-* ccTLDs2
-* ccTLDs3
-
-```
-sudo certbot -d lex.surf
-sudo certbot --cert-name cc1.lex.surf $(for tld in `cat ccTLDs1`; do echo -d $tld.lex.surf; done)
-sudo certbot --cert-name cc2.lex.surf $(for tld in `cat ccTLDs2`; do echo -d $tld.lex.surf; done)
-sudo certbot --cert-name cc3.lex.surf $(for tld in `cat ccTLDs3`; do echo -d $tld.lex.surf; done)
-```
-
-Generate NGINX `server_name` rules with:
-
-```
-printf '%s' $(for tld in `cat ccTLDs2`; do echo "$tld|"; done)
-```
-
-`/var/www/lex.surf` must be a symlink pointing to this repository.
diff --git a/infra/ccTLDs1 b/infra/ccTLDs1
deleted file mode 100644
index 9c1652a..0000000
--- a/infra/ccTLDs1
+++ /dev/null
@@ -1,83 +0,0 @@
-ac
-ad
-ae
-af
-ag
-ai
-al
-am
-ao
-aq
-ar
-as
-at
-au
-aw
-ax
-az
-ba
-bb
-bd
-be
-bf
-bg
-bh
-bi
-bj
-bl
-bm
-bn
-bo
-bq
-br
-bs
-bt
-bv
-bw
-by
-bz
-ca
-cc
-cd
-cf
-cg
-ch
-ci
-ck
-cl
-cm
-cn
-co
-cr
-cu
-cv
-cw
-cx
-cy
-cz
-de
-dj
-dk
-dm
-do
-dz
-ec
-ee
-eg
-eh
-er
-es
-et
-eu
-fi
-fj
-fk
-fm
-fo
-fr
-ga
-gd
-ge
-gf
-gg
-gh
diff --git a/infra/ccTLDs2 b/infra/ccTLDs2
deleted file mode 100644
index 59ace82..0000000
--- a/infra/ccTLDs2
+++ /dev/null
@@ -1,84 +0,0 @@
-gi
-gl
-gm
-gn
-gp
-gq
-gr
-gs
-gt
-gu
-gw
-gy
-hk
-hm
-hn
-hr
-ht
-hu
-id
-ie
-il
-im
-in
-io
-iq
-ir
-is
-it
-je
-jm
-jo
-jp
-ke
-kg
-kh
-ki
-km
-kn
-kp
-kr
-kw
-ky
-kz
-la
-lb
-lc
-li
-lk
-lr
-ls
-lt
-lu
-lv
-ly
-ma
-mc
-md
-me
-mf
-mg
-mh
-mk
-ml
-mm
-mn
-mo
-mp
-mq
-mr
-ms
-mt
-mu
-mv
-mw
-mx
-my
-mz
-na
-nc
-ne
-nf
-ng
-ni
-nl
diff --git a/infra/ccTLDs3 b/infra/ccTLDs3
deleted file mode 100644
index 3346214..0000000
--- a/infra/ccTLDs3
+++ /dev/null
@@ -1,84 +0,0 @@
-no
-np
-nr
-nu
-nz
-om
-pa
-pe
-pf
-pg
-ph
-pk
-pl
-pm
-pn
-pr
-ps
-pt
-pw
-py
-qa
-re
-ro
-rs
-ru
-rw
-sa
-sb
-sc
-sd
-se
-sg
-sh
-si
-sj
-sk
-sl
-sm
-sn
-so
-sr
-ss
-st
-su
-sv
-sx
-sy
-sz
-tc
-td
-tf
-tg
-th
-tj
-tk
-tl
-tm
-tn
-to
-tr
-tt
-tv
-tw
-tz
-ua
-ug
-uk
-us
-uy
-uz
-va
-vc
-ve
-vg
-vi
-vn
-vu
-wf
-ws
-ye
-yt
-za
-zm
-zw
diff --git a/infra/githooks/post-receive b/infra/githooks/post-receive
deleted file mode 100755
index 527b5b1..0000000
--- a/infra/githooks/post-receive
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/bin/bash
-cd ..
-go build lexsurf.go
-sudo systemctl restart lexsurf
diff --git a/infra/lexsurf.service b/infra/lexsurf.service
deleted file mode 100644
index 3cdf6e4..0000000
--- a/infra/lexsurf.service
+++ /dev/null
@@ -1,11 +0,0 @@
-[Unit]
-Description=lex.surf
-
-[Service]
-WorkingDirectory=/var/www/lex.surf/
-ExecStart=/var/www/lex.surf/lexsurf
-Restart=always
-RestartSec=30
-
-[Install]
-WantedBy=default.target
diff --git a/infra/nginx/lex.surf_dev b/infra/nginx/lex.surf_dev
deleted file mode 100644
index a00ebba..0000000
--- a/infra/nginx/lex.surf_dev
+++ /dev/null
@@ -1,35 +0,0 @@
-server {
- listen 80;
- listen [::]:80;
- server_name lex.localhost;
-
- location / {
- proxy_pass http://127.0.0.1:8000;
- proxy_set_header Host lex.localhost;
- }
-
- root /var/www/lex.surf;
-
- location /assets/ {
- try_files $uri =404;
- }
-}
-
-server {
- listen 80;
- listen [::]:80;
- server_name ~^(?<cc>[a-z]+).lex.localhost$;
-
- location / {
- proxy_pass http://127.0.0.1:8000;
- proxy_set_header Host $cc.lex.localhost;
- }
-
- root /var/www/lex.surf;
-
- location = /laws.json {
- gzip on;
- gzip_types *;
- try_files /laws/$cc.json =404;
- }
-}
diff --git a/infra/nginx/lex.surf_prod b/infra/nginx/lex.surf_prod
deleted file mode 100644
index 4e203de..0000000
--- a/infra/nginx/lex.surf_prod
+++ /dev/null
@@ -1,126 +0,0 @@
-server {
- listen 80;
- listen [::]:80;
- server_name lex.surf;
- return 301 https://$host$request_uri;
-}
-
-server {
- listen [::]:443 ssl;
- listen 443 ssl;
- server_name lex.surf;
-
- access_log /var/log/nginx/lex.surf.access.log json;
- error_log /var/log/nginx/lex.surf.error.log;
-
- ssl_certificate /etc/letsencrypt/live/lex.surf/fullchain.pem;
- ssl_certificate_key /etc/letsencrypt/live/lex.surf/privkey.pem;
- include /etc/letsencrypt/options-ssl-nginx.conf;
- ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
-
- location / {
- proxy_pass http://127.0.0.1:8000;
- proxy_set_header Host $host;
- }
-
- root /var/www/lex.surf;
-
- location /assets/ {
- try_files $uri =404;
- }
-}
-
-server {
- listen 80;
- listen [::]:80;
- server_name ~^(ac|ad|ae|af|ag|ai|al|am|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bl|bm|bn|bo|bq|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cu|cv|cw|cx|cy|cz|de|dj|dk|dm|do|dz|ec|ee|eg|eh|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mf|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|ss|st|su|sv|sx|sy|sz|tc|td|tf|tg|th|tj|tk|tl|tm|tn|to|tr|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|za|zm|zw).lex.surf$;
- return 301 https://$host$request_uri;
-}
-
-server {
- listen 80;
- listen [::]:80;
- server_name ~\.lex\.surf$;
- return 302 https://lex.surf/cc404;
-}
-
-server {
- listen [::]:443 ssl;
- listen 443 ssl;
- server_name ~^(?<cc>ac|ad|ae|af|ag|ai|al|am|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bl|bm|bn|bo|bq|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cu|cv|cw|cx|cy|cz|de|dj|dk|dm|do|dz|ec|ee|eg|eh|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gd|ge|gf|gg|gh).lex.surf$;
-
- access_log /var/log/nginx/lex.surf.access.log json;
- error_log /var/log/nginx/lex.surf.error.log;
-
- ssl_certificate /etc/letsencrypt/live/cc1.lex.surf/fullchain.pem;
- ssl_certificate_key /etc/letsencrypt/live/cc1.lex.surf/privkey.pem;
- include /etc/letsencrypt/options-ssl-nginx.conf;
- ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
-
- location / {
- proxy_pass http://127.0.0.1:8000;
- proxy_set_header Host $host;
- }
-
- root /var/www/lex.surf;
-
- location = /laws.json {
- gzip on;
- gzip_types *;
- try_files /laws/$cc.json =404;
- }
-}
-
-server {
- listen [::]:443 ssl;
- listen 443 ssl;
- server_name ~^(?<cc>gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mf|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl).lex.surf$;
-
- access_log /var/log/nginx/lex.surf.access.log json;
- error_log /var/log/nginx/lex.surf.error.log;
-
- ssl_certificate /etc/letsencrypt/live/cc2.lex.surf/fullchain.pem;
- ssl_certificate_key /etc/letsencrypt/live/cc2.lex.surf/privkey.pem;
- include /etc/letsencrypt/options-ssl-nginx.conf;
- ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
-
- location / {
- proxy_pass http://127.0.0.1:8000;
- proxy_set_header Host $host;
- }
-
- root /var/www/lex.surf;
-
- location = /laws.json {
- gzip on;
- gzip_types *;
- try_files /laws/$cc.json =404;
- }
-}
-
-server {
- listen [::]:443 ssl;
- listen 443 ssl;
- server_name ~^(?<cc>no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|ss|st|su|sv|sx|sy|sz|tc|td|tf|tg|th|tj|tk|tl|tm|tn|to|tr|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|za|zm|zw).lex.surf$;
-
- access_log /var/log/nginx/lex.surf.access.log json;
- error_log /var/log/nginx/lex.surf.error.log;
-
- ssl_certificate /etc/letsencrypt/live/cc3.lex.surf/fullchain.pem;
- ssl_certificate_key /etc/letsencrypt/live/cc3.lex.surf/privkey.pem;
- include /etc/letsencrypt/options-ssl-nginx.conf;
- ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
-
- location / {
- proxy_pass http://127.0.0.1:8000;
- proxy_set_header Host $host;
- }
-
- root /var/www/lex.surf;
-
- location = /laws.json {
- gzip on;
- gzip_types *;
- try_files /laws/$cc.json =404;
- }
-}
diff --git a/lex-serve/main.go b/lex-serve/main.go
index d0fb690..826548f 100644
--- a/lex-serve/main.go
+++ b/lex-serve/main.go
@@ -5,6 +5,7 @@ import (
"encoding/json"
"io/ioutil"
"log"
+ "net"
"net/http"
"net/url"
"os"
@@ -16,6 +17,11 @@ import (
var countriesJSON []byte
func main() {
+ socketPath := os.Getenv("SOCKET_PATH")
+ if socketPath == "" {
+ log.Fatal("SOCKET_PATH must be set")
+ }
+
domain := os.Getenv("DOMAIN")
if domain == "" {
log.Fatal("DOMAIN environment variable must be set")
@@ -50,8 +56,15 @@ func main() {
}
}
http.HandleFunc("/", handler.handle)
- println("listening on 8000")
- log.Fatal(http.ListenAndServe(":8000", nil))
+ listener, err := net.Listen("unix", socketPath)
+ if err != nil {
+ log.Fatal(err)
+ }
+ if err := os.Chmod(socketPath, 0666); err != nil {
+ log.Fatalf("error setting socket permissions: %s", err)
+ return
+ }
+ log.Fatal(http.Serve(listener, nil))
}
//go:embed templates
diff --git a/service.nix b/service.nix
new file mode 100644
index 0000000..6024b5e
--- /dev/null
+++ b/service.nix
@@ -0,0 +1,139 @@
+{ config, lib, pkgs, ... }:
+
+let
+ lex_surf = pkgs.callPackage ./default.nix {};
+ cfg = config.services.lex-surf;
+in
+{
+ options.services.lex-surf = {
+ enable = lib.mkEnableOption "lex-surf";
+
+ domain = lib.mkOption {
+ type = lib.types.str;
+ description = "Domain under which lex-surf will be served.";
+ };
+
+ enableACME = lib.mkOption {
+ type = lib.types.bool;
+ description = "Whether to generate certificates.";
+ default = false;
+ };
+
+ nginx =
+ let
+ nginxOpts =
+ (import <nixpkgs/nixos/modules/services/web-servers/nginx/vhost-options.nix> {
+ inherit config lib;
+ }).options;
+ in
+ lib.mkOption {
+ type = lib.types.submodule {
+ options = lib.removeAttrs nginxOpts ["serverName" "useACMEHost" "acmeRoot"];
+ };
+ default = {};
+ };
+ };
+
+ config = lib.mkIf cfg.enable (
+ let
+ socketPath = "/run/lex-serve/http.sock";
+ ccTLDs = lib.splitString "\n" (lib.strings.trim (builtins.readFile ./cc-tlds.txt));
+ chunkList =
+ n: list:
+ if list == [ ] then [ ] else [ (lib.lists.take n list) ] ++ (chunkList n (lib.lists.drop n list));
+ # Let's Encrypt only supports 100 domains per certificate.
+ # We could also use a wildcard certificate but that requires
+ # the dns-01 challenge which takes more effort to set up.
+ ccChunks = chunkList 100 ccTLDs;
+ in
+ {
+ systemd.services.lex-serve = {
+ serviceConfig = {
+ ExecStart = "${lex_surf}/bin/lex-serve";
+ DynamicUser = true;
+ RuntimeDirectory = "lex-serve";
+ WorkingDirectory = lex_surf;
+ };
+
+ environment = {
+ SOCKET_PATH = socketPath;
+ DOMAIN = cfg.domain;
+ };
+ wantedBy = ["multi-user.target"];
+ };
+
+ security.acme.certs = lib.mkIf cfg.enableACME (
+ builtins.listToAttrs (
+ lib.imap0 (i: ccTLDs: {
+ name = "batch${toString i}.${cfg.domain}";
+ value =
+ let
+ domains = map (tld: "${tld}.${cfg.domain}") ccTLDs;
+ in
+ {
+ domain = builtins.head domains;
+ extraDomainNames = builtins.tail domains;
+ group = config.services.nginx.group;
+ webroot = "/var/lib/acme/acme-challenge";
+ };
+ }) ccChunks
+ ) // {
+ ${cfg.domain} = {
+ group = config.services.nginx.group;
+ webroot = "/var/lib/acme/acme-challenge";
+ };
+ }
+ );
+
+ services.nginx = {
+ enable = true;
+
+ virtualHosts =
+ {
+ ${cfg.domain} = lib.mkMerge [
+ cfg.nginx
+ {
+ locations."/" = {
+ proxyPass = "http://unix:${socketPath}";
+ recommendedProxySettings = true;
+ };
+ locations."/assets/" = {
+ root = lex_surf;
+ tryFiles = "$uri =404";
+ };
+ useACMEHost = if cfg.enableACME then cfg.domain else null;
+ }
+ ];
+ }
+ // (builtins.listToAttrs (
+ lib.imap0 (i: ccTLDs: {
+ name = "host${toString i}.${cfg.domain}";
+ value = lib.mkMerge [
+ cfg.nginx
+ {
+ serverName =
+ let
+ escapedDomain = builtins.replaceStrings ["."] ["\\."] cfg.domain;
+ in
+ "~^(?<cc>${lib.strings.concatStringsSep "|" ccTLDs})\\.${escapedDomain}$";
+ useACMEHost = if cfg.enableACME then "batch${toString i}.${cfg.domain}" else null;
+ locations."/" = {
+ proxyPass = "http://unix:${socketPath}";
+ recommendedProxySettings = true;
+ };
+ locations."=/laws.json" = {
+ root = lex_surf;
+ tryFiles = "/laws/$cc.json =404";
+ extraConfig = ''
+ gzip on;
+ gzip_types *;
+ '';
+ };
+ }
+ ];
+ }) ccChunks
+ ));
+ };
+ }
+ );
+}