1package domains23import (4 "context"5 "encoding/json"6 "net/http"7 "net/url"8 "strings"9 "unicode"10)1112// AddressLookup returns the Nostr filter for a validated web path. The13// daemon supplies this from its event/site index; the domains package keeps14// the discovery wire contract independent of storage.15type AddressLookup func(context.Context, string) (map[string]any, bool, error)1617type AddressConfig struct {18 RelayURL string19 Authorized func(*http.Request) bool20 ReadAllowed func(*http.Request) bool21 Lookup AddressLookup22}2324func WebAddressHandler(cfg AddressConfig) http.Handler {25 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { serveWebAddress(w, r, cfg) })26}2728func serveWebAddress(w http.ResponseWriter, r *http.Request, cfg AddressConfig) {29 setAddressHeaders(w, r.Method)30 if r.Method == http.MethodOptions {31 w.WriteHeader(http.StatusOK)32 return33 }34 if r.Method != http.MethodGet && r.Method != http.MethodHead {35 w.Header().Set("Allow", "GET, HEAD, OPTIONS")36 w.WriteHeader(http.StatusMethodNotAllowed)37 return38 }39 path, ok := requestedPath(r.URL)40 if !ok {41 writeAddress(w, r, http.StatusBadRequest, map[string]any{})42 return43 }44 if cfg.Authorized != nil && !cfg.Authorized(r) {45 writeAddress(w, r, http.StatusUnauthorized, map[string]any{})46 return47 }48 if cfg.ReadAllowed != nil && !cfg.ReadAllowed(r) {49 writeAddress(w, r, http.StatusForbidden, map[string]any{})50 return51 }52 result := map[string]any{}53 if cfg.Lookup != nil {54 filter, found, err := cfg.Lookup(r.Context(), path)55 if err != nil {56 http.Error(w, "lookup failed", http.StatusInternalServerError)57 return58 }59 if found {60 result[path] = map[string]any{"filter": filter, "relays": []string{strings.TrimRight(cfg.RelayURL, "/")}}61 }62 }63 writeAddress(w, r, http.StatusOK, result)64}6566func requestedPath(raw *url.URL) (string, bool) {67 values, ok := raw.Query()["path"]68 if !ok || len(values) != 1 {69 return "", false70 }71 path := values[0]72 if len(path) > 4096 || !strings.HasPrefix(path, "/") || strings.HasPrefix(path, "//") {73 return "", false74 }75 if strings.ContainsAny(path, "\\?#\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x7f") {76 return "", false77 }78 decoded, err := url.PathUnescape(path)79 if err != nil {80 return "", false81 }82 if strings.HasPrefix(decoded, "//") || strings.ContainsAny(decoded, "\\\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x7f") {83 return "", false84 }85 for _, segment := range strings.Split(decoded, "/") {86 if segment == "." || segment == ".." {87 return "", false88 }89 for _, r := range segment {90 if unicode.IsSpace(r) {91 return "", false92 }93 }94 }95 return path, true96}9798func setAddressHeaders(w http.ResponseWriter, method string) {99 w.Header().Set("Content-Type", "application/json; charset=utf-8")100 w.Header().Set("Cache-Control", "private, no-store")101 w.Header().Set("Access-Control-Allow-Origin", "*")102 w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS")103 w.Header().Set("Access-Control-Allow-Headers", "authorization, accept")104 w.Header().Set("X-Content-Type-Options", "nosniff")105 _ = method106}107108func writeAddress(w http.ResponseWriter, r *http.Request, status int, value map[string]any) {109 w.WriteHeader(status)110 if r.Method == http.MethodHead {111 return112 }113 _ = json.NewEncoder(w).Encode(value)114}