package main import ( "bytes" "embed" "encoding/json" "html/template" "log/slog" "net" "net/http" "net/url" "os" "path" "slices" "strings" "sync" "push-f.com/lex-surf/internal/lex" "github.com/BurntSushi/toml" ) //go:embed countries.toml var countriesTOML []byte func main() { log := slog.New(slog.NewTextHandler(os.Stderr, nil)) socketPath := os.Getenv("SOCKET_PATH") if socketPath == "" { log.Error("SOCKET_PATH must be set") os.Exit(1) } domain := os.Getenv("DOMAIN") if domain == "" { log.Error("DOMAIN environment variable must be set") os.Exit(1) } lawsDir := os.Getenv("LAWS_DIR") if lawsDir == "" { log.Error("LAWS_DIR must be set") os.Exit(1) } var handler = handler{logger: log, domain: domain, lawsDir: lawsDir, lawsByCC: map[string]map[string]lex.Law{}} meta, err := toml.NewDecoder(bytes.NewReader(countriesTOML)).Decode(&handler.countries) if err != nil { log.Error("failed to parse countries.toml", Error(err)) os.Exit(1) } if len(meta.Undecoded()) != 0 { log.Error("unknown keys in countries.toml", "keys", meta.Undecoded()) os.Exit(1) } lawFiles, err := os.ReadDir(lawsDir) if err == nil { for _, file := range lawFiles { cc := strings.SplitN(file.Name(), ".", 2)[0] handler.loadLaws(cc) } } else if !os.IsNotExist(err) { log.Error("failed to read laws directory", Error(err), "path", lawsDir) os.Exit(1) } http.HandleFunc("/", handler.handle) listener, err := net.Listen("unix", socketPath) if err != nil { log.Error("failed to listen", Error(err)) os.Exit(1) } if err := os.Chmod(socketPath, 0666); err != nil { log.Error("failed to set socket permissions", Error(err), "path", socketPath) return } err = http.Serve(listener, nil) log.Error("failed to serve", Error(err)) } //go:embed templates var templates embed.FS var tpl, _ = template.New("").Option("missingkey=error").ParseFS(templates, "templates/*") type handler struct { logger *slog.Logger domain string countries map[string]country lawsDir string lawsByCC map[string]map[string]lex.Law lawsMu sync.RWMutex } func (h *handler) handle(w http.ResponseWriter, r *http.Request) { if r.Host == "internal.invalid" { // lex-fetch uses this special Host to notify us that the laws for a country should be (re-)loaded. h.loadLaws(r.URL.Query().Get("country")) return } if r.Host == h.domain { if r.URL.Path != "/" { w.WriteHeader(http.StatusNotFound) w.Write([]byte("page not found")) return } var firstTier []link var secondTier []link var thirdTier []link for key, country := range h.countries { link := link{ URL: "//" + key + "." + h.domain, Code: strings.ToUpper(key), Name: country.Name, } if len(h.lawsByCC[key]) > 0 { firstTier = append(firstTier, link) } else if country.HasPlaceholder() { secondTier = append(secondTier, link) } else { thirdTier = append(thirdTier, link) } } var html bytes.Buffer err := tpl.ExecuteTemplate(&html, "index.html.tmpl", map[string]any{ "FirstTier": slices.SortedFunc(slices.Values(firstTier), compareLink), "SecondTier": slices.SortedFunc(slices.Values(secondTier), compareLink), "ThirdTier": slices.SortedFunc(slices.Values(thirdTier), compareLink), "Domain": r.Host, }) if err != nil { h.logger.Error("failed to execute index template", Error(err)) http.Error(w, "internal server error", http.StatusInternalServerError) return } w.Write(html.Bytes()) return } cc, isSubdomain := strings.CutSuffix(r.Host, "."+h.domain) if !isSubdomain { h.logger.Error("request for unknown hosts", "host", r.Host) w.Write([]byte("unknown host")) return } key := strings.TrimLeft(r.URL.Path, "/") if len(key) > 0 { h.lawsMu.RLock() defer h.lawsMu.RUnlock() val, ok := h.lawsByCC[cc][key] if !ok { w.WriteHeader(http.StatusNotFound) w.Write([]byte("unknown law")) return } http.Redirect(w, r, val.URL, 302) } else { query := r.URL.Query().Get("q") if query != "" { country, ok := h.countries[cc] if !ok { w.WriteHeader(http.StatusNotFound) w.Write([]byte("search not implemented for this country")) return } if country.HasPlaceholder() { http.Redirect(w, r, strings.Replace(country.SearchURL, "%s", url.QueryEscape(query), 1), 302) return } else { w.WriteHeader(http.StatusBadRequest) } } h.lawsMu.RLock() defer h.lawsMu.RUnlock() _, hasJSONLaws := h.lawsByCC[cc] var html bytes.Buffer err := tpl.ExecuteTemplate(&html, "search.html.tmpl", map[string]any{ "TLD": cc, "Domain": h.domain, "Country": h.countries[cc], "HasJSONLaws": hasJSONLaws, }) if err != nil { h.logger.Error("failed to execute search template", Error(err)) http.Error(w, "internal server error", http.StatusInternalServerError) return } w.Write(html.Bytes()) } } func (h *handler) loadLaws(country string) { filePath := path.Join(h.lawsDir, country+".json") text, err := os.ReadFile(filePath) if err != nil { h.logger.Error("failed to read file", Error(err), "path", filePath) return } var laws []lex.Law err = json.Unmarshal([]byte(text), &laws) if err != nil { h.logger.Error("failed to parse file", Error(err), "path", filePath) return } h.lawsMu.Lock() defer h.lawsMu.Unlock() h.lawsByCC[country] = map[string]lex.Law{} for _, law := range laws { if law.Redir != "" { h.lawsByCC[country][law.Redir] = law } } } type country struct { Name string SearchURL string `toml:"search-url"` } func (c country) HasPlaceholder() bool { return strings.Contains(c.SearchURL, "%s") } type link struct { URL string Code string Name string } func compareLink(a, b link) int { if a.Code > b.Code { return 1 } else if a.Code < b.Code { return -1 } return 0 } func Error(value error) slog.Attr { return slog.String("error", value.Error()) }