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
|
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package util
import (
"fmt"
"net/url"
"strings"
"github.com/superseriousbusiness/gotosocial/internal/regexes"
"github.com/superseriousbusiness/gotosocial/internal/uris"
)
// ExtractNamestringParts extracts the username test_user and
// the domain example.org from a string like @test_user@example.org.
//
// If nothing is matched, it will return an error.
func ExtractNamestringParts(namestring string) (username, host string, err error) {
matches := regexes.MentionName.FindStringSubmatch(namestring)
switch len(matches) {
case 2:
return matches[1], "", nil
case 3:
return matches[1], matches[2], nil
default:
return "", "", fmt.Errorf("couldn't match namestring %s", namestring)
}
}
// ExtractWebfingerParts returns the username and domain from the "subject"
// part of a webfinger response: either an account namestring or an actor URI.
//
// All AP implementations in the wild perform webfinger account resource
// queries with the "acct" scheme and without a leading "@"" on the username.
// This is also the format the "subject" in a webfinger response adheres to.
//
// Despite this fact, we're permissive about a single leading @. This makes
// a query for "acct:user@domain.tld" and "acct:@user@domain.tld" equivalent.
//
// We also permit a resource of "user@domain.tld" or "@user@domain.tld", without
// a scheme. In that case it gets interpreted as if it was using "acct:".
//
// Will error if parsing fails, or if the extracted username or domain are empty.
func ExtractWebfingerParts(subject string) (
string, // username
string, // domain
error,
) {
u, err := url.ParseRequestURI(subject)
if err != nil {
// Most likely reason for failing to parse is if
// the "acct" scheme was missing but a :port was
// included. So try an extra time with the scheme.
u, err = url.ParseRequestURI("acct:" + subject)
}
if err != nil {
return "", "", fmt.Errorf("failed to parse %s: %w", subject, err)
}
switch u.Scheme {
// Subject looks like
// "https://example.org/users/whatever"
// or "https://example.org/@whatever".
case "http", "https":
return partsFromURI(u)
// Subject looks like
// "acct:whatever@example.org"
// or "acct:@whatever@example.org".
case "acct":
// Pass string without "acct:" prefix.
return partsFromNamestring(u.Opaque)
// Subject was probably a relative URL.
// Fail since we need the domain.
case "":
return "", "", fmt.Errorf("no scheme for resource %s", subject)
}
return "", "", fmt.Errorf("unsupported scheme %s for resource %s", u.Scheme, subject)
}
// partsFromNamestring returns the username
// and host parts extracted from a passed-in actor
// namestring of the format "whatever@example.org".
//
// The function returns an error if username or
// host cannot be extracted.
func partsFromNamestring(namestring string) (
string, // username
string, // host
error,
) {
// Trim all leading "@" symbols,
// and then inject just one "@".
namestring = strings.TrimLeft(namestring, "@")
namestring = "@" + namestring
username, host, err := ExtractNamestringParts(namestring)
if err != nil {
return "", "", err
}
if username == "" {
err := fmt.Errorf("failed to extract username from: %s", namestring)
return "", "", err
}
if host == "" {
err := fmt.Errorf("failed to extract domain from: %s", namestring)
return "", "", err
}
return username, host, nil
}
// partsFromURI returns the username and host
// extracted from the passed in actor URI.
//
// The username will be extracted from one of
// the patterns "/@whatever" or "/users/whatever".
// These paths match the "aliases" and "links"
// we include in our own webfinger responses.
//
// This function tries to be permissive with
// regard to leading "@" symbols. Nevertheless,
// an error will be returned if username or host
// cannot be extracted.
func partsFromURI(uri *url.URL) (
string, // username
string, // host
error,
) {
host := uri.Host
if host == "" {
err := fmt.Errorf("failed to extract domain from: %s", uri)
return "", "", err
}
// Copy the URL, taking
// only the parts we need.
short := &url.URL{
Path: uri.Path,
}
// Try "/users/whatever".
username, err := uris.ParseUserPath(short)
if err == nil && username != "" {
return username, host, nil
}
// Try "/@whatever"
username, err = uris.ParseUserWebPath(short)
if err == nil && username != "" {
return username, host, nil
}
// Try some exotic fallbacks like
// "/users/@whatever", "/@@whatever", etc.
short.Path = strings.TrimLeft(short.Path, "/")
segs := strings.Split(short.Path, "/")
if segs[0] == "users" {
username = segs[1]
} else {
username = segs[0]
}
username = strings.TrimLeft(username, "@")
if username != "" {
return username, host, nil
}
return "", "", fmt.Errorf("failed to extract username from: %s", uri)
}
|