+37
atproto/syntax/did.go
+37
atproto/syntax/did.go
···
1
+
package syntax
2
+
3
+
import (
4
+
"fmt"
5
+
"regexp"
6
+
"strings"
7
+
)
8
+
9
+
// Represents a syntaxtually valid DID identifier, as would pass Lexicon syntax validation.
10
+
//
11
+
// Syntax specification: https://atproto.com/specs/did
12
+
type DID string
13
+
14
+
func ParseDID(raw string) (DID, error) {
15
+
if len(raw) > 2*1024 {
16
+
return "", fmt.Errorf("DID is too long (2048 chars max)")
17
+
}
18
+
var didRegex = regexp.MustCompile(`^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$`)
19
+
if !didRegex.MatchString(raw) {
20
+
return "", fmt.Errorf("DID syntax didn't validate via regex")
21
+
}
22
+
return DID(raw), nil
23
+
}
24
+
25
+
// The "method" part of the DID, between the 'did:' prefix and the final identifier segment, normalized to lower-case.
26
+
func (d DID) Method() string {
27
+
// syntax guarantees that there are at least 3 parts of split
28
+
parts := strings.SplitN(string(d), ":", 3)
29
+
return strings.ToLower(parts[1])
30
+
}
31
+
32
+
// The final "identifier" segment of the DID
33
+
func (d DID) Identifier() string {
34
+
// syntax guarantees that there are at least 3 parts of split
35
+
parts := strings.SplitN(string(d), ":", 3)
36
+
return parts[2]
37
+
}
+58
atproto/syntax/did_test.go
+58
atproto/syntax/did_test.go
···
1
+
package syntax
2
+
3
+
import (
4
+
"bufio"
5
+
"fmt"
6
+
"os"
7
+
"testing"
8
+
9
+
"github.com/stretchr/testify/assert"
10
+
)
11
+
12
+
func TestDIDParts(t *testing.T) {
13
+
assert := assert.New(t)
14
+
d, err := ParseDID("did:example:123456789abcDEFghi")
15
+
assert.NoError(err)
16
+
assert.Equal("example", d.Method())
17
+
assert.Equal("123456789abcDEFghi", d.Identifier())
18
+
}
19
+
20
+
func TestInteropDIDsValid(t *testing.T) {
21
+
assert := assert.New(t)
22
+
file, err := os.Open("testdata/did_syntax_valid.txt")
23
+
assert.NoError(err)
24
+
defer file.Close()
25
+
scanner := bufio.NewScanner(file)
26
+
for scanner.Scan() {
27
+
line := scanner.Text()
28
+
if len(line) == 0 || line[0] == '#' {
29
+
continue
30
+
}
31
+
_, err := ParseDID(line)
32
+
if err != nil {
33
+
fmt.Println("GOOD: " + line)
34
+
}
35
+
assert.NoError(err)
36
+
}
37
+
assert.NoError(scanner.Err())
38
+
}
39
+
40
+
func TestInteropDIDsInvalid(t *testing.T) {
41
+
assert := assert.New(t)
42
+
file, err := os.Open("testdata/did_syntax_invalid.txt")
43
+
assert.NoError(err)
44
+
defer file.Close()
45
+
scanner := bufio.NewScanner(file)
46
+
for scanner.Scan() {
47
+
line := scanner.Text()
48
+
if len(line) == 0 || line[0] == '#' {
49
+
continue
50
+
}
51
+
_, err := ParseDID(line)
52
+
if err == nil {
53
+
fmt.Println("BAD: " + line)
54
+
}
55
+
assert.Error(err)
56
+
}
57
+
assert.NoError(scanner.Err())
58
+
}
+4
atproto/syntax/doc.go
+4
atproto/syntax/doc.go
···
1
+
// Package syntax provides types for identifiers and other string formats.
2
+
//
3
+
// These are primarily simple string alias types for parsing or verifying protocol-level syntax of identifiers, not routines for things like resolution or verification against application policies.
4
+
package syntax
+51
atproto/syntax/handle.go
+51
atproto/syntax/handle.go
···
1
+
package syntax
2
+
3
+
import (
4
+
"fmt"
5
+
"regexp"
6
+
"strings"
7
+
)
8
+
9
+
// String type which represents a syntaxtually valid handle identifier, as would pass Lexicon syntax validation.
10
+
//
11
+
// Syntax specification: https://atproto.com/specs/handle
12
+
type Handle string
13
+
14
+
func ParseHandle(raw string) (Handle, error) {
15
+
if len(raw) > 253 {
16
+
return "", fmt.Errorf("Handle is too long (253 chars max)")
17
+
}
18
+
var handleRegex = regexp.MustCompile(`^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$`)
19
+
if !handleRegex.MatchString(raw) {
20
+
return "", fmt.Errorf("Handle syntax didn't validate via regex")
21
+
}
22
+
return Handle(raw), nil
23
+
}
24
+
25
+
// Some top-level domains (TLDs) are disallowed for registration across the atproto ecosystem. The *syntax* is valid, but these should never be considered acceptable handles for account registration or linking.
26
+
func (h *Handle) AllowedTLD() bool {
27
+
switch h.TLD() {
28
+
case "local",
29
+
"arpa",
30
+
"invalid",
31
+
"localhost",
32
+
"internal",
33
+
"onion":
34
+
return false
35
+
}
36
+
return true
37
+
}
38
+
39
+
func (h Handle) TLD() string {
40
+
parts := strings.Split(string(h.Normalize()), ".")
41
+
return parts[len(parts)-1]
42
+
}
43
+
44
+
// Is this the special "handle.invalid" handle?
45
+
func (h Handle) IsInvalidHandle() bool {
46
+
return h.Normalize() == "handle.invalid"
47
+
}
48
+
49
+
func (h Handle) Normalize() Handle {
50
+
return Handle(strings.ToLower(string(h)))
51
+
}
+62
atproto/syntax/handle_test.go
+62
atproto/syntax/handle_test.go
···
1
+
package syntax
2
+
3
+
import (
4
+
"bufio"
5
+
"fmt"
6
+
"os"
7
+
"testing"
8
+
9
+
"github.com/stretchr/testify/assert"
10
+
)
11
+
12
+
func TestInteropHandlesValid(t *testing.T) {
13
+
assert := assert.New(t)
14
+
file, err := os.Open("testdata/handle_syntax_valid.txt")
15
+
assert.NoError(err)
16
+
defer file.Close()
17
+
scanner := bufio.NewScanner(file)
18
+
for scanner.Scan() {
19
+
line := scanner.Text()
20
+
if len(line) == 0 || line[0] == '#' {
21
+
continue
22
+
}
23
+
_, err := ParseHandle(line)
24
+
if err != nil {
25
+
fmt.Println("GOOD: " + line)
26
+
}
27
+
assert.NoError(err)
28
+
}
29
+
assert.NoError(scanner.Err())
30
+
}
31
+
32
+
func TestInteropHandlesInvalid(t *testing.T) {
33
+
assert := assert.New(t)
34
+
file, err := os.Open("testdata/handle_syntax_invalid.txt")
35
+
assert.NoError(err)
36
+
defer file.Close()
37
+
scanner := bufio.NewScanner(file)
38
+
for scanner.Scan() {
39
+
line := scanner.Text()
40
+
if len(line) == 0 || line[0] == '#' {
41
+
continue
42
+
}
43
+
_, err := ParseHandle(line)
44
+
if err == nil {
45
+
fmt.Println("BAD: " + line)
46
+
}
47
+
assert.Error(err)
48
+
}
49
+
assert.NoError(scanner.Err())
50
+
}
51
+
52
+
func TestHandleNormalize(t *testing.T) {
53
+
assert := assert.New(t)
54
+
55
+
handle, err := ParseHandle("JoHn.TeST")
56
+
assert.NoError(err)
57
+
assert.Equal(string(handle.Normalize()), "john.test")
58
+
assert.NoError(err)
59
+
60
+
_, err = ParseHandle("JoH!n.TeST")
61
+
assert.Error(err)
62
+
}
+19
atproto/syntax/testdata/did_syntax_invalid.txt
+19
atproto/syntax/testdata/did_syntax_invalid.txt
···
1
+
did
2
+
didmethodval
3
+
method:did:val
4
+
did:method:
5
+
didmethod:val
6
+
did:methodval)
7
+
:did:method:val
8
+
did.method.val
9
+
did:method:val:
10
+
did:method:val%
11
+
DID:method:val
12
+
did:METHOD:val
13
+
did:m123:val
14
+
did:method:val/two
15
+
did:method:val?two
16
+
did:method:val#two
17
+
did:method:val%
18
+
did:method:vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
19
+
+26
atproto/syntax/testdata/did_syntax_valid.txt
+26
atproto/syntax/testdata/did_syntax_valid.txt
···
1
+
did:method:val
2
+
did:method:VAL
3
+
did:method:val123
4
+
did:method:123
5
+
did:method:val-two
6
+
did:method:val_two
7
+
did:method:val.two
8
+
did:method:val:two
9
+
did:method:val%BB
10
+
did:method:vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
11
+
did:m:v
12
+
did:method::::val
13
+
did:method:-
14
+
did:method:-:_:.:%ab
15
+
did:method:.
16
+
did:method:_
17
+
did:method::.
18
+
19
+
# allows some real DID values
20
+
did:onion:2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid
21
+
did:example:123456789abcdefghi
22
+
did:plc:7iza6de2dwap2sbkpav7c6c6
23
+
did:web:example.com
24
+
did:web:localhost%3A1234
25
+
did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N
26
+
did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a
+57
atproto/syntax/testdata/handle_syntax_invalid.txt
+57
atproto/syntax/testdata/handle_syntax_invalid.txt
···
1
+
# throws on invalid handles
2
+
did:thing.test
3
+
did:thing
4
+
john-.test
5
+
john.0
6
+
john.-
7
+
xn--bcher-.tld
8
+
john..test
9
+
jo_hn.test
10
+
-john.test
11
+
.john.test
12
+
jo!hn.test
13
+
jo%hn.test
14
+
jo&hn.test
15
+
jo@hn.test
16
+
jo*hn.test
17
+
jo|hn.test
18
+
jo:hn.test
19
+
jo/hn.test
20
+
john💩.test
21
+
bücher.test
22
+
john .test
23
+
john.test.
24
+
john
25
+
john.
26
+
.john
27
+
john.test.
28
+
.john.test
29
+
john.test
30
+
john.test
31
+
joh-.test
32
+
john.-est
33
+
john.tes-
34
+
35
+
# TODO: short/long examples
36
+
37
+
# throws on "dotless" TLD handles
38
+
org
39
+
ai
40
+
gg
41
+
io
42
+
43
+
# correctly validates corner cases (modern vs. old RFCs)
44
+
cn.8
45
+
thing.0aa
46
+
thing.0aa
47
+
48
+
# does not allow IP addresses as handles
49
+
127.0.0.1
50
+
192.168.0.142
51
+
fe80::7325:8a97:c100:94b
52
+
2600:3c03::f03c:9100:feb0:af1f
53
+
54
+
# examples from stackoverflow
55
+
-notvalid.at-all
56
+
-thing.com
57
+
www.masełkowski.pl.com
+85
atproto/syntax/testdata/handle_syntax_valid.txt
+85
atproto/syntax/testdata/handle_syntax_valid.txt
···
1
+
# allows valid handles
2
+
A.ISI.EDU
3
+
XX.LCS.MIT.EDU
4
+
SRI-NIC.ARPA
5
+
john.test
6
+
jan.test
7
+
a234567890123456789.test
8
+
john2.test
9
+
john-john.test
10
+
john.bsky.app
11
+
jo.hn
12
+
a.co
13
+
a.org
14
+
joh.n
15
+
j0.h0
16
+
jaymome-johnber123456.test
17
+
jay.mome-johnber123456.test
18
+
john.test.bsky.app
19
+
# TODO: short and long
20
+
21
+
# NOTE: this probably isn't ever going to be a real domain, but my read of the RFC is that it would be possible
22
+
john.t
23
+
24
+
# allows .local and .arpa handles (proto-level)
25
+
laptop.local
26
+
laptop.arpa
27
+
28
+
# allows punycode handles
29
+
# 💩.test
30
+
xn--ls8h.test
31
+
# bücher.tld
32
+
xn--bcher-kva.tld
33
+
xn--3jk.com
34
+
xn--w3d.com
35
+
xn--vqb.com
36
+
xn--ppd.com
37
+
xn--cs9a.com
38
+
xn--8r9a.com
39
+
xn--cfd.com
40
+
xn--5jk.com
41
+
xn--2lb.com
42
+
43
+
# allows onion (Tor) handles
44
+
expyuzz4wqqyqhjn.onion
45
+
friend.expyuzz4wqqyqhjn.onion
46
+
g2zyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion
47
+
friend.g2zyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion
48
+
friend.g2zyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion
49
+
2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion
50
+
friend.2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion
51
+
52
+
# correctly validates corner cases (modern vs. old RFCs)
53
+
12345.test
54
+
8.cn
55
+
4chan.org
56
+
4chan.o-g
57
+
blah.4chan.org
58
+
thing.a01
59
+
120.0.0.1.com
60
+
0john.test
61
+
9sta--ck.com
62
+
99stack.com
63
+
0ohn.test
64
+
john.t--t
65
+
thing.0aa.thing
66
+
67
+
# examples from stackoverflow
68
+
stack.com
69
+
sta-ck.com
70
+
sta---ck.com
71
+
sta--ck9.com
72
+
stack99.com
73
+
sta99ck.com
74
+
google.com.uk
75
+
google.co.in
76
+
google.com
77
+
maselkowski.pl
78
+
m.maselkowski.pl
79
+
xn--masekowski-d0b.pl
80
+
xn--fiqa61au8b7zsevnm8ak20mc4a87e.xn--fiqs8s
81
+
xn--stackoverflow.com
82
+
stackoverflow.xn--com
83
+
stackoverflow.co.uk
84
+
xn--masekowski-d0b.pl
85
+
xn--fiqa61au8b7zsevnm8ak20mc4a87e.xn--fiqs8s