| 1 | package domains |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "net/http" |
| 7 | "net/url" |
| 8 | "strings" |
| 9 | "unicode" |
| 10 | ) |
| 11 | |
| 12 | // AddressLookup returns the Nostr filter for a validated web path. The |
| 13 | // daemon supplies this from its event/site index; the domains package keeps |
| 14 | // the discovery wire contract independent of storage. |
| 15 | type AddressLookup func(context.Context, string) (map[string]any, bool, error) |
| 16 | |
| 17 | type AddressConfig struct { |
| 18 | RelayURL string |
| 19 | Authorized func(*http.Request) bool |
| 20 | ReadAllowed func(*http.Request) bool |
| 21 | Lookup AddressLookup |
| 22 | } |
| 23 | |
| 24 | func WebAddressHandler(cfg AddressConfig) http.Handler { |
| 25 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { serveWebAddress(w, r, cfg) }) |
| 26 | } |
| 27 | |
| 28 | func 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 | return |
| 33 | } |
| 34 | if r.Method != http.MethodGet && r.Method != http.MethodHead { |
| 35 | w.Header().Set("Allow" , "GET, HEAD, OPTIONS" ) |
| 36 | w.WriteHeader(http.StatusMethodNotAllowed) |
| 37 | return |
| 38 | } |
| 39 | path, ok := requestedPath(r.URL) |
| 40 | if !ok { |
| 41 | writeAddress(w, r, http.StatusBadRequest, map[string]any{}) |
| 42 | return |
| 43 | } |
| 44 | if cfg.Authorized != nil && !cfg.Authorized(r) { |
| 45 | writeAddress(w, r, http.StatusUnauthorized, map[string]any{}) |
| 46 | return |
| 47 | } |
| 48 | if cfg.ReadAllowed != nil && !cfg.ReadAllowed(r) { |
| 49 | writeAddress(w, r, http.StatusForbidden, map[string]any{}) |
| 50 | return |
| 51 | } |
| 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 | return |
| 58 | } |
| 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 | } |
| 65 | |
| 66 | func requestedPath(raw *url.URL) (string, bool) { |
| 67 | values, ok := raw.Query()["path" ] |
| 68 | if !ok || len(values) != 1 { |
| 69 | return "" , false |
| 70 | } |
| 71 | path := values[0] |
| 72 | if len(path) > 4096 || !strings.HasPrefix(path, "/" ) || strings.HasPrefix(path, "//" ) { |
| 73 | return "" , false |
| 74 | } |
| 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 "" , false |
| 77 | } |
| 78 | decoded, err := url.PathUnescape(path) |
| 79 | if err != nil { |
| 80 | return "" , false |
| 81 | } |
| 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 "" , false |
| 84 | } |
| 85 | for _, segment := range strings.Split(decoded, "/" ) { |
| 86 | if segment == "." || segment == ".." { |
| 87 | return "" , false |
| 88 | } |
| 89 | for _, r := range segment { |
| 90 | if unicode.IsSpace(r) { |
| 91 | return "" , false |
| 92 | } |
| 93 | } |
| 94 | } |
| 95 | return path, true |
| 96 | } |
| 97 | |
| 98 | func 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 | _ = method |
| 106 | } |
| 107 | |
| 108 | func writeAddress(w http.ResponseWriter, r *http.Request, status int, value map[string]any) { |
| 109 | w.WriteHeader(status) |
| 110 | if r.Method == http.MethodHead { |
| 111 | return |
| 112 | } |
| 113 | _ = json.NewEncoder(w).Encode(value) |
| 114 | } |