1 // Package normalize is a set of tools for cleaning up URL s and formatting
2 // nostr OK and CLOSED messages.
3 package normalize
4 5 import (
6 "bytes"
7 "errors"
8 "fmt"
9 "net/url"
10 11 "next.orly.dev/pkg/nostr/encoders/ints"
12 "next.orly.dev/pkg/nostr/utils/constraints"
13 "next.orly.dev/pkg/lol/chk"
14 "next.orly.dev/pkg/lol/log"
15 )
16 17 var (
18 hp = bytes.HasPrefix
19 WS = []byte("ws://")
20 WSS = []byte("wss://")
21 HTTP = []byte("http://")
22 HTTPS = []byte("https://")
23 )
24 25 // URL normalizes the URL
26 //
27 // - Adds wss:// to addresses without a port, or with 443 that have no protocol
28 // prefix
29 //
30 // - Adds ws:// to addresses with any other port
31 //
32 // - Converts http/s to ws/s
33 func URL[V constraints.Bytes](v V) (b []byte) {
34 u := []byte(v)
35 if len(u) == 0 {
36 return nil
37 }
38 u = bytes.TrimSpace(u)
39 u = bytes.ToLower(u)
40 // if the address has a port number, we can probably assume it is insecure
41 // websocket as most public or production relays have a domain name and a
42 // well-known port 80 or 443 and thus no port number.
43 //
44 // if a protocol prefix is present, we assume it is already complete.
45 // Converting http/s to websocket-equivalent will be done later anyway.
46 if bytes.Contains(u, []byte(":")) &&
47 !(hp(u, HTTP) || hp(u, HTTPS) || hp(u, WS) || hp(u, WSS)) {
48 49 split := bytes.Split(u, []byte(":"))
50 if len(split) != 2 {
51 log.D.F("Error: more than one ':' in URL: '%s'", u)
52 // this is a malformed URL if it has more than one ":", return empty
53 // since this function does not return an error explicitly.
54 return
55 }
56 p := ints.New(0)
57 _, err := p.Unmarshal(split[1])
58 if chk.E(err) {
59 log.D.F("Error normalizing URL '%s': %s", u, err)
60 // again, without an error, we must return nil
61 return
62 }
63 if p.Uint64() > 65535 {
64 log.D.F(
65 "Port on address %d: greater than maximum 65535",
66 p.Uint64(),
67 )
68 return
69 }
70 // if the port is explicitly set to 443 we assume it is wss:// and drop
71 // the port.
72 if p.Uint16() == 443 {
73 u = append(WSS, split[0]...)
74 } else {
75 // non-443 port likely means insecure websocket (local dev, etc.)
76 u = append(WS, u...)
77 }
78 }
79 80 // if the prefix isn't specified as http/s or websocket, assume secure
81 // websocket and add wss prefix (this is the most common).
82 if !(hp(u, HTTP) || hp(u, HTTPS) || hp(u, WS) || hp(u, WSS)) {
83 u = append(WSS, u...)
84 }
85 var err error
86 var p *url.URL
87 if p, err = url.Parse(string(u)); chk.E(err) {
88 return
89 }
90 // convert http/s to ws/s
91 switch p.Scheme {
92 case "https":
93 p.Scheme = "wss"
94 case "http":
95 p.Scheme = "ws"
96 }
97 // remove trailing path slash
98 p.Path = string(bytes.TrimRight([]byte(p.Path), "/"))
99 return []byte(p.String())
100 }
101 102 // Msg constructs a properly formatted message with a machine-readable prefix
103 // for OK and CLOSED envelopes.
104 func Msg(prefix Reason, format string, params ...any) []byte {
105 if len(prefix) < 1 {
106 prefix = Error
107 }
108 return []byte(fmt.Sprintf(prefix.S()+": "+format, params...))
109 }
110 111 // MsgString constructs a properly formatted message with a machine-readable prefix
112 // for OK and CLOSED envelopes.
113 func MsgString(prefix Reason, format string, params ...any) string {
114 if len(prefix) < 1 {
115 prefix = Error
116 }
117 return fmt.Sprintf(prefix.S()+": "+format, params...)
118 }
119 120 // Reason is the machine-readable prefix before the colon in an OK or CLOSED
121 // envelope message. Below are the most common kinds that are mentioned in
122 // NIP-01.
123 type Reason []byte
124 125 var (
126 AuthRequired = Reason("auth-required")
127 PoW = Reason("pow")
128 Duplicate = Reason("duplicate")
129 Blocked = Reason("blocked")
130 RateLimited = Reason("rate-limited")
131 Invalid = Reason("invalid")
132 Error = Reason("error")
133 Unsupported = Reason("unsupported")
134 Restricted = Reason("restricted")
135 )
136 137 // S returns the Reason as a string
138 func (r Reason) S() string { return string(r) }
139 140 // B returns the Reason as a byte slice.
141 func (r Reason) B() []byte { return r }
142 143 // IsPrefix returns whether a text contains the same Reason prefix.
144 func (r Reason) IsPrefix(reason []byte) bool {
145 return bytes.HasPrefix(
146 reason, r.B(),
147 )
148 }
149 150 // F allows creation of a full Reason text with a printf style format.
151 func (r Reason) F(format string, params ...any) []byte {
152 return Msg(
153 r, format, params...,
154 )
155 }
156 157 // Errorf allows creation of a full Reason text with a printf style as an error.
158 func (r Reason) Errorf(format string, params ...any) (err error) {
159 return errors.New(
160 MsgString(
161 r, format, params...,
162 ),
163 )
164 }
165