-
Notifications
You must be signed in to change notification settings - Fork 17
/
codec.go
358 lines (326 loc) · 8.36 KB
/
codec.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
package lnurl
import (
"errors"
"fmt"
"net"
"net/url"
"strings"
"golang.org/x/net/idna"
"golang.org/x/net/publicsuffix"
)
var lud17ValidSchemes = map[string]struct{}{"lnurla": {}, "lnurlp": {}, "lnurlw": {}, "lnurlc": {}, "keyauth": {}}
// LNURLDecode takes a bech32-encoded lnurl string and returns a plain-text https URL.
func LNURLDecode(code string) (string, error) {
code = strings.ToLower(code)
switch {
case strings.HasPrefix(code, "lnurl1"):
// bech32
tag, data, err := decode(code)
if err != nil {
return "", err
}
if tag != "lnurl" {
return "", errors.New("tag is not 'lnurl', but '" + tag + "'")
}
converted, err := convertBits(data, 5, 8, false)
if err != nil {
return "", err
}
return string(converted), nil
case strings.HasPrefix(code, "lnurlp://"),
strings.HasPrefix(code, "lnurlw://"),
strings.HasPrefix(code, "lnurlc://"),
strings.HasPrefix(code, "keyauth://"),
strings.HasPrefix(code, "https://"):
u := "https://" + strings.SplitN(code, "://", 2)[1]
if parsed, err := url.Parse(u); err == nil &&
strings.HasSuffix(parsed.Host, ".onion") {
u = "https://" + strings.SplitN(code, "://", 2)[1]
}
return u, nil
}
return "", errors.New("unrecognized lnurl format: " + code)
}
// LNURLEncode takes a plain-text https URL and returns a bech32-encoded uppercased lnurl string.
func LNURLEncode(actualurl string) (lnurl string, err error) {
asbytes := []byte(actualurl)
converted, err := convertBits(asbytes, 8, 5, true)
if err != nil {
return
}
lnurl, err = encode("lnurl", converted)
return strings.ToUpper(lnurl), err
}
// LURL embeds net/url and adds extra fields ontop
type lnUrl struct {
subdomain, domain, tld, port, publicSuffix string
icann, isDomain, isIp bool
*url.URL
}
func (u lnUrl) String() string {
decodedValue, err := url.QueryUnescape(u.URL.String())
if err != nil {
return ""
}
return decodedValue
}
// parse mirrors net/url.Parse except instead it returns
// a tld.URL, which contains extra fields.
func parse(s string) (*lnUrl, error) {
s = addDefaultScheme(s)
parsedUrl, err := url.Parse(s)
if err != nil {
return nil, err
}
if parsedUrl.Host == "" {
return &lnUrl{URL: parsedUrl}, nil
}
dom, port := domainPort(parsedUrl.Host)
//etld+1
etld1, err := publicsuffix.EffectiveTLDPlusOne(dom)
if err != nil {
return nil, err
}
//convert to domain name, and tld
i := strings.Index(etld1, ".")
domName := etld1[0:i]
tld := etld1[i+1:]
//and subdomain
sub := ""
if rest := strings.TrimSuffix(dom, "."+etld1); rest != dom {
sub = rest
}
s, err = idna.New(idna.ValidateForRegistration()).ToASCII(dom)
if err != nil {
return nil, err
}
psuf, icann := publicsuffix.PublicSuffix(tld)
return &lnUrl{
subdomain: sub,
domain: domName,
tld: tld,
port: port,
URL: parsedUrl,
publicSuffix: psuf,
icann: icann,
isDomain: IsDomainName(s),
isIp: net.ParseIP(dom) != nil,
}, nil
}
// adds default scheme //, if nothing is defined at all
func addDefaultScheme(s string) string {
if strings.Index(s, "//") == -1 {
return fmt.Sprintf("//%s", s)
}
return s
}
// domainPort splits domain.com:8080
func domainPort(host string) (string, string) {
for i := len(host) - 1; i >= 0; i-- {
if host[i] == ':' {
return host[:i], host[i+1:]
} else if host[i] < '0' || host[i] > '9' {
return host, ""
}
}
//will only land here if the string is all digits,
//net/url should prevent that from happening
return host, ""
}
// LNURLDecodeStrict takes a string and returns a valid lnurl, if possible.
// code can be
func LNURLDecodeStrict(code string) (string, error) {
code = strings.ToLower(code)
switch {
case strings.HasPrefix(code, "lnurl1"):
// bech32
tag, data, err := decode(code)
if err != nil {
return "", err
}
if tag != "lnurl" {
return "", errors.New("tag is not 'lnurl', but '" + tag + "'")
}
converted, err := convertBits(data, 5, 8, false)
if err != nil {
return "", err
}
u, err := parse(string(converted))
if err != nil {
return string(converted), err
}
if u.isIp {
if u.Scheme != "https" {
err := fmt.Errorf("invalid scheme: %s", string(converted))
u.Scheme = "https"
return u.String(), err
}
return u.String(), nil
}
if !u.isDomain {
return string(converted), fmt.Errorf("invalid domain: %s", string(converted))
}
if setScheme(u) {
return u.String(), fmt.Errorf("invalid scheme: %s", u.Scheme)
}
return u.String(), nil
case strings.HasPrefix(code, "https://"):
return code, nil
default:
u, err := parse(code)
if err != nil {
return "", err
}
scheme := u.Scheme
lud17 := validLud17(scheme)
if setScheme(u) {
if !lud17 {
return u.String(), fmt.Errorf("invalid scheme: %s", scheme)
}
}
return u.String(), nil
}
}
// setScheme will parse string url to url.Url.
// if no scheme was found,
func setScheme(u *lnUrl) (updated bool) {
if u.tld == "onion" {
if u.Scheme != "http" {
u.Scheme = "http"
updated = true
}
} else {
if u.Scheme != "https" {
u.Scheme = "https"
updated = true
}
}
return
}
// validLud17 will return true, if scheme is valid for lud17
func validLud17(schema string) bool {
_, ok := lud17ValidSchemes[schema]
return ok
}
// LNURLEncodeStrict will encode the actualurl to lnurl.
// based on the input url, it will determine whether bech32 encoding / url manipulation is necessary
func LNURLEncodeStrict(actualurl string) (string, error) {
lnurl, err := parse(actualurl)
if err != nil {
enc, encErr := Encode(actualurl)
if encErr != nil {
return "", encErr
}
return enc, fmt.Errorf("invalid url: %s", actualurl)
}
if validLud17(lnurl.Scheme) {
return lnurl.String(), nil
}
if lnurl.isIp {
// actualurl is an ip. just change scheme
lnurl.Scheme = "https"
return Encode(lnurl.String())
}
if !lnurl.isDomain {
enc, encErr := Encode(actualurl)
if encErr != nil {
return "", encErr
}
return enc, fmt.Errorf("invalid domain: %s", lnurl.tld)
}
updated := false
if lnurl.tld != "onion" {
// check tld
if !lnurl.icann {
enc, encErr := Encode(actualurl)
if encErr != nil {
return "", encErr
}
return enc, fmt.Errorf("invalid tld: %s", lnurl.tld)
}
if lnurl.Scheme != "https" {
lnurl.Scheme = "https"
updated = true
}
} else {
if lnurl.Scheme != "http" {
lnurl.Scheme = "http"
updated = true
}
}
enc, err := Encode(lnurl.String())
if err != nil {
return enc, err
}
if updated {
return enc, fmt.Errorf("invalid protocol schema: %s", lnurl.Scheme)
}
return enc, err
}
func Encode(s string) (string, error) {
asbytes := []byte(s)
converted, err := convertBits(asbytes, 8, 5, true)
if err != nil {
return s, err
}
lnurl, err := encode("lnurl", converted)
return strings.ToUpper(lnurl), err
}
// IsDomainName (from net package) checks if a string is a presentation-format domain name
// (currently restricted to hostname-compatible "preferred name" LDH labels and
// SRV-like "underscore labels"; see golang.org/issue/12421).
func IsDomainName(s string) bool {
// The root domain name is valid. See golang.org/issue/45715.
if s == "." {
return true
}
// See RFC 1035, RFC 3696.
// Presentation format has dots before every label except the first, and the
// terminal empty label is optional here because we assume fully-qualified
// (absolute) input. We must therefore reserve space for the first and last
// labels' length octets in wire format, where they are necessary and the
// maximum total length is 255.
// So our _effective_ maximum is 253, but 254 is not rejected if the last
// character is a dot.
l := len(s)
if l == 0 || l > 254 || l == 254 && s[l-1] != '.' {
return false
}
last := byte('.')
nonNumeric := false // true once we've seen a letter or hyphen
partlen := 0
for i := 0; i < len(s); i++ {
c := s[i]
switch {
default:
return false
case 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '_':
nonNumeric = true
partlen++
case '0' <= c && c <= '9':
// fine
partlen++
case c == '-':
// Byte before dash cannot be dot.
if last == '.' {
return false
}
partlen++
nonNumeric = true
case c == '.':
// Byte before dot cannot be dot, dash.
if last == '.' || last == '-' {
return false
}
if partlen > 63 || partlen == 0 {
return false
}
partlen = 0
}
last = c
}
if last == '-' || partlen > 63 {
return false
}
return nonNumeric
}