tinyrelayfile
tinyrelay/internal/domains/nipad.go/

internal/domains/nipad.go raw
1package domains
2
3import (
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.
15type AddressLookup func(context.Context, string) (map[string]any, bool, error)
16
17type AddressConfig struct {
18 RelayURL string
19 Authorized func(*http.Request) bool
20 ReadAllowed func(*http.Request) bool
21 Lookup AddressLookup
22}
23
24func WebAddressHandler(cfg AddressConfig) http.Handler {
25 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { serveWebAddress(w, r, cfg) })
26}
27
28func 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
66func 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
98func 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
108func 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}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs