@fedify/webfinger 2.3.0-dev.994 → 2.3.0-pr.809.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/deno.json +2 -2
- package/dist/lookup.test.cjs +518 -143
- package/dist/lookup.test.js +520 -145
- package/dist/mod.cjs +148 -18
- package/dist/mod.d.cts +33 -2
- package/dist/mod.d.ts +33 -2
- package/dist/mod.js +148 -18
- package/package.json +7 -7
- package/src/lookup.test.ts +793 -236
- package/src/lookup.ts +253 -22
package/dist/mod.cjs
CHANGED
|
@@ -4,7 +4,7 @@ let _logtape_logtape = require("@logtape/logtape");
|
|
|
4
4
|
let _opentelemetry_api = require("@opentelemetry/api");
|
|
5
5
|
//#region deno.json
|
|
6
6
|
var name = "@fedify/webfinger";
|
|
7
|
-
var version = "2.3.0-
|
|
7
|
+
var version = "2.3.0-pr.809.37+0574ba5c";
|
|
8
8
|
//#endregion
|
|
9
9
|
//#region src/lookup.ts
|
|
10
10
|
const logger = (0, _logtape_logtape.getLogger)([
|
|
@@ -13,6 +13,58 @@ const logger = (0, _logtape_logtape.getLogger)([
|
|
|
13
13
|
"lookup"
|
|
14
14
|
]);
|
|
15
15
|
const DEFAULT_MAX_REDIRECTION = 5;
|
|
16
|
+
const WEBFINGER_HISTOGRAM_BUCKETS = [
|
|
17
|
+
5,
|
|
18
|
+
10,
|
|
19
|
+
25,
|
|
20
|
+
50,
|
|
21
|
+
75,
|
|
22
|
+
100,
|
|
23
|
+
250,
|
|
24
|
+
500,
|
|
25
|
+
750,
|
|
26
|
+
1e3,
|
|
27
|
+
2500,
|
|
28
|
+
5e3,
|
|
29
|
+
7500,
|
|
30
|
+
1e4
|
|
31
|
+
];
|
|
32
|
+
const webFingerInstruments = /* @__PURE__ */ new WeakMap();
|
|
33
|
+
function getWebFingerInstruments(meterProvider) {
|
|
34
|
+
let instruments = webFingerInstruments.get(meterProvider);
|
|
35
|
+
if (instruments == null) {
|
|
36
|
+
const meter = meterProvider.getMeter(name, version);
|
|
37
|
+
instruments = {
|
|
38
|
+
lookup: meter.createCounter("webfinger.lookup", {
|
|
39
|
+
description: "Outgoing WebFinger lookup attempts.",
|
|
40
|
+
unit: "{lookup}"
|
|
41
|
+
}),
|
|
42
|
+
lookupDuration: meter.createHistogram("webfinger.lookup.duration", {
|
|
43
|
+
description: "Duration of outgoing WebFinger lookups.",
|
|
44
|
+
unit: "ms",
|
|
45
|
+
advice: { explicitBucketBoundaries: [...WEBFINGER_HISTOGRAM_BUCKETS] }
|
|
46
|
+
})
|
|
47
|
+
};
|
|
48
|
+
webFingerInstruments.set(meterProvider, instruments);
|
|
49
|
+
}
|
|
50
|
+
return instruments;
|
|
51
|
+
}
|
|
52
|
+
function getResourceScheme(resource) {
|
|
53
|
+
if (typeof resource === "string") {
|
|
54
|
+
const colon = resource.indexOf(":");
|
|
55
|
+
return colon > 0 ? resource.substring(0, colon).toLowerCase() : "";
|
|
56
|
+
}
|
|
57
|
+
return resource.protocol.replace(/:$/, "").toLowerCase();
|
|
58
|
+
}
|
|
59
|
+
const WEBFINGER_LOOKUP_SCHEME_WHITELIST = new Set([
|
|
60
|
+
"acct",
|
|
61
|
+
"http",
|
|
62
|
+
"https",
|
|
63
|
+
"mailto"
|
|
64
|
+
]);
|
|
65
|
+
function getMetricResourceScheme(scheme) {
|
|
66
|
+
return WEBFINGER_LOOKUP_SCHEME_WHITELIST.has(scheme) ? scheme : "other";
|
|
67
|
+
}
|
|
16
68
|
/**
|
|
17
69
|
* Looks up a WebFinger resource.
|
|
18
70
|
* @param resource The resource URL to look up.
|
|
@@ -21,17 +73,25 @@ const DEFAULT_MAX_REDIRECTION = 5;
|
|
|
21
73
|
* @since 0.2.0
|
|
22
74
|
*/
|
|
23
75
|
async function lookupWebFinger(resource, options = {}) {
|
|
24
|
-
|
|
76
|
+
const tracer = (options.tracerProvider ?? _opentelemetry_api.trace.getTracerProvider()).getTracer(name, version);
|
|
77
|
+
const scheme = getResourceScheme(resource);
|
|
78
|
+
return await tracer.startActiveSpan("webfinger.lookup", {
|
|
25
79
|
kind: _opentelemetry_api.SpanKind.CLIENT,
|
|
26
80
|
attributes: {
|
|
27
81
|
"webfinger.resource": resource.toString(),
|
|
28
|
-
"webfinger.resource.scheme":
|
|
82
|
+
"webfinger.resource.scheme": scheme
|
|
29
83
|
}
|
|
30
84
|
}, async (span) => {
|
|
85
|
+
const meterProvider = options.meterProvider;
|
|
86
|
+
const start = meterProvider == null ? 0 : performance.now();
|
|
87
|
+
let outcome = {
|
|
88
|
+
resource: null,
|
|
89
|
+
result: "error"
|
|
90
|
+
};
|
|
31
91
|
try {
|
|
32
|
-
|
|
33
|
-
span.setStatus({ code:
|
|
34
|
-
return
|
|
92
|
+
outcome = await lookupWebFingerInternal(resource, options);
|
|
93
|
+
span.setStatus({ code: outcome.resource === null ? _opentelemetry_api.SpanStatusCode.ERROR : _opentelemetry_api.SpanStatusCode.OK });
|
|
94
|
+
return outcome.resource;
|
|
35
95
|
} catch (error) {
|
|
36
96
|
span.setStatus({
|
|
37
97
|
code: _opentelemetry_api.SpanStatusCode.ERROR,
|
|
@@ -39,19 +99,41 @@ async function lookupWebFinger(resource, options = {}) {
|
|
|
39
99
|
});
|
|
40
100
|
throw error;
|
|
41
101
|
} finally {
|
|
102
|
+
if (meterProvider != null) recordWebFingerLookup(meterProvider, Math.max(0, performance.now() - start), scheme, outcome);
|
|
42
103
|
span.end();
|
|
43
104
|
}
|
|
44
105
|
});
|
|
45
106
|
}
|
|
107
|
+
function recordWebFingerLookup(meterProvider, durationMs, scheme, outcome) {
|
|
108
|
+
const attributes = {
|
|
109
|
+
"webfinger.lookup.result": outcome.result,
|
|
110
|
+
"webfinger.resource.scheme": getMetricResourceScheme(scheme)
|
|
111
|
+
};
|
|
112
|
+
if (outcome.remoteHost != null) attributes["activitypub.remote.host"] = outcome.remoteHost;
|
|
113
|
+
if (outcome.statusCode != null) attributes["http.response.status_code"] = outcome.statusCode;
|
|
114
|
+
const instruments = getWebFingerInstruments(meterProvider);
|
|
115
|
+
instruments.lookup.add(1, attributes);
|
|
116
|
+
instruments.lookupDuration.record(durationMs, attributes);
|
|
117
|
+
}
|
|
46
118
|
async function lookupWebFingerInternal(resource, options = {}) {
|
|
47
119
|
if (typeof resource === "string") resource = new URL(resource);
|
|
48
120
|
let protocol = "https:";
|
|
49
121
|
let server;
|
|
50
|
-
if (resource.protocol === "acct:") {
|
|
122
|
+
if (resource.protocol === "acct:" || resource.protocol === "mailto:") {
|
|
51
123
|
const atPos = resource.pathname.lastIndexOf("@");
|
|
52
|
-
if (atPos < 0) return
|
|
124
|
+
if (atPos < 0) return {
|
|
125
|
+
resource: null,
|
|
126
|
+
result: "invalid"
|
|
127
|
+
};
|
|
53
128
|
server = resource.pathname.substring(atPos + 1);
|
|
54
|
-
if (server === "") return
|
|
129
|
+
if (server === "" || /[/?#]/.test(server)) return {
|
|
130
|
+
resource: null,
|
|
131
|
+
result: "invalid"
|
|
132
|
+
};
|
|
133
|
+
if (resource.protocol === "acct:" && (resource.search !== "" || resource.hash !== "")) return {
|
|
134
|
+
resource: null,
|
|
135
|
+
result: "invalid"
|
|
136
|
+
};
|
|
55
137
|
} else {
|
|
56
138
|
protocol = resource.protocol;
|
|
57
139
|
server = resource.host;
|
|
@@ -60,6 +142,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
60
142
|
url.searchParams.set("resource", resource.href);
|
|
61
143
|
let redirected = 0;
|
|
62
144
|
while (true) {
|
|
145
|
+
const remoteHost = url.host;
|
|
63
146
|
logger.debug("Fetching WebFinger resource descriptor from {url}...", { url: url.href });
|
|
64
147
|
let response;
|
|
65
148
|
if (options.allowPrivateAddress !== true) try {
|
|
@@ -67,7 +150,11 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
67
150
|
} catch (e) {
|
|
68
151
|
if (e instanceof _fedify_vocab_runtime.UrlError) {
|
|
69
152
|
logger.error("Invalid URL for WebFinger resource descriptor: {error}", { error: e });
|
|
70
|
-
return
|
|
153
|
+
return {
|
|
154
|
+
resource: null,
|
|
155
|
+
result: "network_error",
|
|
156
|
+
remoteHost
|
|
157
|
+
};
|
|
71
158
|
}
|
|
72
159
|
throw e;
|
|
73
160
|
}
|
|
@@ -85,22 +172,50 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
85
172
|
url: url.href,
|
|
86
173
|
error
|
|
87
174
|
});
|
|
88
|
-
return
|
|
175
|
+
return {
|
|
176
|
+
resource: null,
|
|
177
|
+
result: "network_error",
|
|
178
|
+
remoteHost
|
|
179
|
+
};
|
|
89
180
|
}
|
|
90
181
|
if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
|
|
91
182
|
redirected++;
|
|
92
183
|
const maxRedirection = options.maxRedirection ?? DEFAULT_MAX_REDIRECTION;
|
|
93
|
-
if (redirected
|
|
184
|
+
if (redirected > maxRedirection) {
|
|
94
185
|
logger.error("Too many redirections ({redirections}) while fetching WebFinger resource descriptor.", { redirections: redirected });
|
|
95
|
-
return
|
|
186
|
+
return {
|
|
187
|
+
resource: null,
|
|
188
|
+
result: "invalid",
|
|
189
|
+
statusCode: response.status,
|
|
190
|
+
remoteHost
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
let redirectedUrl;
|
|
194
|
+
try {
|
|
195
|
+
redirectedUrl = new URL(response.headers.get("Location"), response.url == null || response.url === "" ? url : response.url);
|
|
196
|
+
} catch (e) {
|
|
197
|
+
logger.error("Invalid Location header while following WebFinger redirect: {error}", {
|
|
198
|
+
url: url.href,
|
|
199
|
+
error: e
|
|
200
|
+
});
|
|
201
|
+
return {
|
|
202
|
+
resource: null,
|
|
203
|
+
result: "invalid",
|
|
204
|
+
statusCode: response.status,
|
|
205
|
+
remoteHost
|
|
206
|
+
};
|
|
96
207
|
}
|
|
97
|
-
const redirectedUrl = new URL(response.headers.get("Location"), response.url == null || response.url === "" ? url : response.url);
|
|
98
208
|
if (redirectedUrl.protocol !== url.protocol) {
|
|
99
209
|
logger.error("Redirected to a different protocol ({protocol} to {redirectedProtocol}) while fetching WebFinger resource descriptor.", {
|
|
100
210
|
protocol: url.protocol,
|
|
101
211
|
redirectedProtocol: redirectedUrl.protocol
|
|
102
212
|
});
|
|
103
|
-
return
|
|
213
|
+
return {
|
|
214
|
+
resource: null,
|
|
215
|
+
result: "invalid",
|
|
216
|
+
statusCode: response.status,
|
|
217
|
+
remoteHost
|
|
218
|
+
};
|
|
104
219
|
}
|
|
105
220
|
url = redirectedUrl;
|
|
106
221
|
continue;
|
|
@@ -111,14 +226,29 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
111
226
|
status: response.status,
|
|
112
227
|
statusText: response.statusText
|
|
113
228
|
});
|
|
114
|
-
return
|
|
229
|
+
return {
|
|
230
|
+
resource: null,
|
|
231
|
+
result: response.status === 404 || response.status === 410 ? "not_found" : "error",
|
|
232
|
+
statusCode: response.status,
|
|
233
|
+
remoteHost
|
|
234
|
+
};
|
|
115
235
|
}
|
|
116
236
|
try {
|
|
117
|
-
return
|
|
237
|
+
return {
|
|
238
|
+
resource: await response.json(),
|
|
239
|
+
result: "found",
|
|
240
|
+
statusCode: response.status,
|
|
241
|
+
remoteHost
|
|
242
|
+
};
|
|
118
243
|
} catch (e) {
|
|
119
244
|
if (e instanceof SyntaxError) {
|
|
120
245
|
logger.debug("Failed to parse WebFinger resource descriptor as JSON: {error}", { error: e });
|
|
121
|
-
return
|
|
246
|
+
return {
|
|
247
|
+
resource: null,
|
|
248
|
+
result: "invalid",
|
|
249
|
+
statusCode: response.status,
|
|
250
|
+
remoteHost
|
|
251
|
+
};
|
|
122
252
|
}
|
|
123
253
|
throw e;
|
|
124
254
|
}
|
package/dist/mod.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { GetUserAgentOptions } from "@fedify/vocab-runtime";
|
|
2
|
-
import { TracerProvider } from "@opentelemetry/api";
|
|
2
|
+
import { MeterProvider, TracerProvider } from "@opentelemetry/api";
|
|
3
3
|
|
|
4
4
|
//#region src/jrd.d.ts
|
|
5
5
|
/**
|
|
@@ -63,6 +63,28 @@ interface Link {
|
|
|
63
63
|
//#endregion
|
|
64
64
|
//#region src/lookup.d.ts
|
|
65
65
|
/**
|
|
66
|
+
* The terminal classification of an outgoing {@link lookupWebFinger} call,
|
|
67
|
+
* recorded as the `webfinger.lookup.result` attribute on the
|
|
68
|
+
* `webfinger.lookup` counter and `webfinger.lookup.duration` histogram.
|
|
69
|
+
*
|
|
70
|
+
* - `found`: the lookup returned a {@link ResourceDescriptor}.
|
|
71
|
+
* - `not_found`: the remote responded with HTTP `404 Not Found` or
|
|
72
|
+
* `410 Gone`. Recorded together with `http.response.status_code`.
|
|
73
|
+
* - `invalid`: the remote responded with content Fedify could not parse
|
|
74
|
+
* into a {@link ResourceDescriptor} (JSON parse failure), the
|
|
75
|
+
* redirect chain exceeded `maxRedirection`, the remote redirected to
|
|
76
|
+
* a different protocol, or the queried `acct:` resource itself was
|
|
77
|
+
* malformed.
|
|
78
|
+
* - `network_error`: no HTTP response was received (the URL was
|
|
79
|
+
* rejected as a private address, `fetch()` threw, or an `AbortError`
|
|
80
|
+
* cancelled the request).
|
|
81
|
+
* - `error`: the remote responded with a non-2xx HTTP response that is
|
|
82
|
+
* neither `404` nor `410`, or any other unexpected failure bubbled up
|
|
83
|
+
* from the lookup.
|
|
84
|
+
* @since 2.3.0
|
|
85
|
+
*/
|
|
86
|
+
type WebFingerLookupResult = "found" | "not_found" | "invalid" | "network_error" | "error";
|
|
87
|
+
/**
|
|
66
88
|
* Options for {@link lookupWebFinger}.
|
|
67
89
|
* @since 1.3.0
|
|
68
90
|
*/
|
|
@@ -95,6 +117,15 @@ interface LookupWebFingerOptions {
|
|
|
95
117
|
*/
|
|
96
118
|
tracerProvider?: TracerProvider;
|
|
97
119
|
/**
|
|
120
|
+
* The OpenTelemetry meter provider used to record the `webfinger.lookup`
|
|
121
|
+
* counter and `webfinger.lookup.duration` histogram. If omitted, no
|
|
122
|
+
* metric measurements are emitted (the helper is opt-in to avoid
|
|
123
|
+
* touching the global meter provider for callers that do not use
|
|
124
|
+
* OpenTelemetry).
|
|
125
|
+
* @since 2.3.0
|
|
126
|
+
*/
|
|
127
|
+
meterProvider?: MeterProvider;
|
|
128
|
+
/**
|
|
98
129
|
* AbortSignal for cancelling the request.
|
|
99
130
|
* @since 1.8.0
|
|
100
131
|
*/
|
|
@@ -109,4 +140,4 @@ interface LookupWebFingerOptions {
|
|
|
109
140
|
*/
|
|
110
141
|
declare function lookupWebFinger(resource: URL | string, options?: LookupWebFingerOptions): Promise<ResourceDescriptor | null>;
|
|
111
142
|
//#endregion
|
|
112
|
-
export { Link, LookupWebFingerOptions, ResourceDescriptor, lookupWebFinger };
|
|
143
|
+
export { Link, LookupWebFingerOptions, ResourceDescriptor, WebFingerLookupResult, lookupWebFinger };
|
package/dist/mod.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { GetUserAgentOptions } from "@fedify/vocab-runtime";
|
|
2
|
-
import { TracerProvider } from "@opentelemetry/api";
|
|
2
|
+
import { MeterProvider, TracerProvider } from "@opentelemetry/api";
|
|
3
3
|
|
|
4
4
|
//#region src/jrd.d.ts
|
|
5
5
|
/**
|
|
@@ -63,6 +63,28 @@ interface Link {
|
|
|
63
63
|
//#endregion
|
|
64
64
|
//#region src/lookup.d.ts
|
|
65
65
|
/**
|
|
66
|
+
* The terminal classification of an outgoing {@link lookupWebFinger} call,
|
|
67
|
+
* recorded as the `webfinger.lookup.result` attribute on the
|
|
68
|
+
* `webfinger.lookup` counter and `webfinger.lookup.duration` histogram.
|
|
69
|
+
*
|
|
70
|
+
* - `found`: the lookup returned a {@link ResourceDescriptor}.
|
|
71
|
+
* - `not_found`: the remote responded with HTTP `404 Not Found` or
|
|
72
|
+
* `410 Gone`. Recorded together with `http.response.status_code`.
|
|
73
|
+
* - `invalid`: the remote responded with content Fedify could not parse
|
|
74
|
+
* into a {@link ResourceDescriptor} (JSON parse failure), the
|
|
75
|
+
* redirect chain exceeded `maxRedirection`, the remote redirected to
|
|
76
|
+
* a different protocol, or the queried `acct:` resource itself was
|
|
77
|
+
* malformed.
|
|
78
|
+
* - `network_error`: no HTTP response was received (the URL was
|
|
79
|
+
* rejected as a private address, `fetch()` threw, or an `AbortError`
|
|
80
|
+
* cancelled the request).
|
|
81
|
+
* - `error`: the remote responded with a non-2xx HTTP response that is
|
|
82
|
+
* neither `404` nor `410`, or any other unexpected failure bubbled up
|
|
83
|
+
* from the lookup.
|
|
84
|
+
* @since 2.3.0
|
|
85
|
+
*/
|
|
86
|
+
type WebFingerLookupResult = "found" | "not_found" | "invalid" | "network_error" | "error";
|
|
87
|
+
/**
|
|
66
88
|
* Options for {@link lookupWebFinger}.
|
|
67
89
|
* @since 1.3.0
|
|
68
90
|
*/
|
|
@@ -95,6 +117,15 @@ interface LookupWebFingerOptions {
|
|
|
95
117
|
*/
|
|
96
118
|
tracerProvider?: TracerProvider;
|
|
97
119
|
/**
|
|
120
|
+
* The OpenTelemetry meter provider used to record the `webfinger.lookup`
|
|
121
|
+
* counter and `webfinger.lookup.duration` histogram. If omitted, no
|
|
122
|
+
* metric measurements are emitted (the helper is opt-in to avoid
|
|
123
|
+
* touching the global meter provider for callers that do not use
|
|
124
|
+
* OpenTelemetry).
|
|
125
|
+
* @since 2.3.0
|
|
126
|
+
*/
|
|
127
|
+
meterProvider?: MeterProvider;
|
|
128
|
+
/**
|
|
98
129
|
* AbortSignal for cancelling the request.
|
|
99
130
|
* @since 1.8.0
|
|
100
131
|
*/
|
|
@@ -109,4 +140,4 @@ interface LookupWebFingerOptions {
|
|
|
109
140
|
*/
|
|
110
141
|
declare function lookupWebFinger(resource: URL | string, options?: LookupWebFingerOptions): Promise<ResourceDescriptor | null>;
|
|
111
142
|
//#endregion
|
|
112
|
-
export { Link, LookupWebFingerOptions, ResourceDescriptor, lookupWebFinger };
|
|
143
|
+
export { Link, LookupWebFingerOptions, ResourceDescriptor, WebFingerLookupResult, lookupWebFinger };
|
package/dist/mod.js
CHANGED
|
@@ -3,7 +3,7 @@ import { getLogger } from "@logtape/logtape";
|
|
|
3
3
|
import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
|
|
4
4
|
//#region deno.json
|
|
5
5
|
var name = "@fedify/webfinger";
|
|
6
|
-
var version = "2.3.0-
|
|
6
|
+
var version = "2.3.0-pr.809.37+0574ba5c";
|
|
7
7
|
//#endregion
|
|
8
8
|
//#region src/lookup.ts
|
|
9
9
|
const logger = getLogger([
|
|
@@ -12,6 +12,58 @@ const logger = getLogger([
|
|
|
12
12
|
"lookup"
|
|
13
13
|
]);
|
|
14
14
|
const DEFAULT_MAX_REDIRECTION = 5;
|
|
15
|
+
const WEBFINGER_HISTOGRAM_BUCKETS = [
|
|
16
|
+
5,
|
|
17
|
+
10,
|
|
18
|
+
25,
|
|
19
|
+
50,
|
|
20
|
+
75,
|
|
21
|
+
100,
|
|
22
|
+
250,
|
|
23
|
+
500,
|
|
24
|
+
750,
|
|
25
|
+
1e3,
|
|
26
|
+
2500,
|
|
27
|
+
5e3,
|
|
28
|
+
7500,
|
|
29
|
+
1e4
|
|
30
|
+
];
|
|
31
|
+
const webFingerInstruments = /* @__PURE__ */ new WeakMap();
|
|
32
|
+
function getWebFingerInstruments(meterProvider) {
|
|
33
|
+
let instruments = webFingerInstruments.get(meterProvider);
|
|
34
|
+
if (instruments == null) {
|
|
35
|
+
const meter = meterProvider.getMeter(name, version);
|
|
36
|
+
instruments = {
|
|
37
|
+
lookup: meter.createCounter("webfinger.lookup", {
|
|
38
|
+
description: "Outgoing WebFinger lookup attempts.",
|
|
39
|
+
unit: "{lookup}"
|
|
40
|
+
}),
|
|
41
|
+
lookupDuration: meter.createHistogram("webfinger.lookup.duration", {
|
|
42
|
+
description: "Duration of outgoing WebFinger lookups.",
|
|
43
|
+
unit: "ms",
|
|
44
|
+
advice: { explicitBucketBoundaries: [...WEBFINGER_HISTOGRAM_BUCKETS] }
|
|
45
|
+
})
|
|
46
|
+
};
|
|
47
|
+
webFingerInstruments.set(meterProvider, instruments);
|
|
48
|
+
}
|
|
49
|
+
return instruments;
|
|
50
|
+
}
|
|
51
|
+
function getResourceScheme(resource) {
|
|
52
|
+
if (typeof resource === "string") {
|
|
53
|
+
const colon = resource.indexOf(":");
|
|
54
|
+
return colon > 0 ? resource.substring(0, colon).toLowerCase() : "";
|
|
55
|
+
}
|
|
56
|
+
return resource.protocol.replace(/:$/, "").toLowerCase();
|
|
57
|
+
}
|
|
58
|
+
const WEBFINGER_LOOKUP_SCHEME_WHITELIST = new Set([
|
|
59
|
+
"acct",
|
|
60
|
+
"http",
|
|
61
|
+
"https",
|
|
62
|
+
"mailto"
|
|
63
|
+
]);
|
|
64
|
+
function getMetricResourceScheme(scheme) {
|
|
65
|
+
return WEBFINGER_LOOKUP_SCHEME_WHITELIST.has(scheme) ? scheme : "other";
|
|
66
|
+
}
|
|
15
67
|
/**
|
|
16
68
|
* Looks up a WebFinger resource.
|
|
17
69
|
* @param resource The resource URL to look up.
|
|
@@ -20,17 +72,25 @@ const DEFAULT_MAX_REDIRECTION = 5;
|
|
|
20
72
|
* @since 0.2.0
|
|
21
73
|
*/
|
|
22
74
|
async function lookupWebFinger(resource, options = {}) {
|
|
23
|
-
|
|
75
|
+
const tracer = (options.tracerProvider ?? trace.getTracerProvider()).getTracer(name, version);
|
|
76
|
+
const scheme = getResourceScheme(resource);
|
|
77
|
+
return await tracer.startActiveSpan("webfinger.lookup", {
|
|
24
78
|
kind: SpanKind.CLIENT,
|
|
25
79
|
attributes: {
|
|
26
80
|
"webfinger.resource": resource.toString(),
|
|
27
|
-
"webfinger.resource.scheme":
|
|
81
|
+
"webfinger.resource.scheme": scheme
|
|
28
82
|
}
|
|
29
83
|
}, async (span) => {
|
|
84
|
+
const meterProvider = options.meterProvider;
|
|
85
|
+
const start = meterProvider == null ? 0 : performance.now();
|
|
86
|
+
let outcome = {
|
|
87
|
+
resource: null,
|
|
88
|
+
result: "error"
|
|
89
|
+
};
|
|
30
90
|
try {
|
|
31
|
-
|
|
32
|
-
span.setStatus({ code:
|
|
33
|
-
return
|
|
91
|
+
outcome = await lookupWebFingerInternal(resource, options);
|
|
92
|
+
span.setStatus({ code: outcome.resource === null ? SpanStatusCode.ERROR : SpanStatusCode.OK });
|
|
93
|
+
return outcome.resource;
|
|
34
94
|
} catch (error) {
|
|
35
95
|
span.setStatus({
|
|
36
96
|
code: SpanStatusCode.ERROR,
|
|
@@ -38,19 +98,41 @@ async function lookupWebFinger(resource, options = {}) {
|
|
|
38
98
|
});
|
|
39
99
|
throw error;
|
|
40
100
|
} finally {
|
|
101
|
+
if (meterProvider != null) recordWebFingerLookup(meterProvider, Math.max(0, performance.now() - start), scheme, outcome);
|
|
41
102
|
span.end();
|
|
42
103
|
}
|
|
43
104
|
});
|
|
44
105
|
}
|
|
106
|
+
function recordWebFingerLookup(meterProvider, durationMs, scheme, outcome) {
|
|
107
|
+
const attributes = {
|
|
108
|
+
"webfinger.lookup.result": outcome.result,
|
|
109
|
+
"webfinger.resource.scheme": getMetricResourceScheme(scheme)
|
|
110
|
+
};
|
|
111
|
+
if (outcome.remoteHost != null) attributes["activitypub.remote.host"] = outcome.remoteHost;
|
|
112
|
+
if (outcome.statusCode != null) attributes["http.response.status_code"] = outcome.statusCode;
|
|
113
|
+
const instruments = getWebFingerInstruments(meterProvider);
|
|
114
|
+
instruments.lookup.add(1, attributes);
|
|
115
|
+
instruments.lookupDuration.record(durationMs, attributes);
|
|
116
|
+
}
|
|
45
117
|
async function lookupWebFingerInternal(resource, options = {}) {
|
|
46
118
|
if (typeof resource === "string") resource = new URL(resource);
|
|
47
119
|
let protocol = "https:";
|
|
48
120
|
let server;
|
|
49
|
-
if (resource.protocol === "acct:") {
|
|
121
|
+
if (resource.protocol === "acct:" || resource.protocol === "mailto:") {
|
|
50
122
|
const atPos = resource.pathname.lastIndexOf("@");
|
|
51
|
-
if (atPos < 0) return
|
|
123
|
+
if (atPos < 0) return {
|
|
124
|
+
resource: null,
|
|
125
|
+
result: "invalid"
|
|
126
|
+
};
|
|
52
127
|
server = resource.pathname.substring(atPos + 1);
|
|
53
|
-
if (server === "") return
|
|
128
|
+
if (server === "" || /[/?#]/.test(server)) return {
|
|
129
|
+
resource: null,
|
|
130
|
+
result: "invalid"
|
|
131
|
+
};
|
|
132
|
+
if (resource.protocol === "acct:" && (resource.search !== "" || resource.hash !== "")) return {
|
|
133
|
+
resource: null,
|
|
134
|
+
result: "invalid"
|
|
135
|
+
};
|
|
54
136
|
} else {
|
|
55
137
|
protocol = resource.protocol;
|
|
56
138
|
server = resource.host;
|
|
@@ -59,6 +141,7 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
59
141
|
url.searchParams.set("resource", resource.href);
|
|
60
142
|
let redirected = 0;
|
|
61
143
|
while (true) {
|
|
144
|
+
const remoteHost = url.host;
|
|
62
145
|
logger.debug("Fetching WebFinger resource descriptor from {url}...", { url: url.href });
|
|
63
146
|
let response;
|
|
64
147
|
if (options.allowPrivateAddress !== true) try {
|
|
@@ -66,7 +149,11 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
66
149
|
} catch (e) {
|
|
67
150
|
if (e instanceof UrlError) {
|
|
68
151
|
logger.error("Invalid URL for WebFinger resource descriptor: {error}", { error: e });
|
|
69
|
-
return
|
|
152
|
+
return {
|
|
153
|
+
resource: null,
|
|
154
|
+
result: "network_error",
|
|
155
|
+
remoteHost
|
|
156
|
+
};
|
|
70
157
|
}
|
|
71
158
|
throw e;
|
|
72
159
|
}
|
|
@@ -84,22 +171,50 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
84
171
|
url: url.href,
|
|
85
172
|
error
|
|
86
173
|
});
|
|
87
|
-
return
|
|
174
|
+
return {
|
|
175
|
+
resource: null,
|
|
176
|
+
result: "network_error",
|
|
177
|
+
remoteHost
|
|
178
|
+
};
|
|
88
179
|
}
|
|
89
180
|
if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
|
|
90
181
|
redirected++;
|
|
91
182
|
const maxRedirection = options.maxRedirection ?? DEFAULT_MAX_REDIRECTION;
|
|
92
|
-
if (redirected
|
|
183
|
+
if (redirected > maxRedirection) {
|
|
93
184
|
logger.error("Too many redirections ({redirections}) while fetching WebFinger resource descriptor.", { redirections: redirected });
|
|
94
|
-
return
|
|
185
|
+
return {
|
|
186
|
+
resource: null,
|
|
187
|
+
result: "invalid",
|
|
188
|
+
statusCode: response.status,
|
|
189
|
+
remoteHost
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
let redirectedUrl;
|
|
193
|
+
try {
|
|
194
|
+
redirectedUrl = new URL(response.headers.get("Location"), response.url == null || response.url === "" ? url : response.url);
|
|
195
|
+
} catch (e) {
|
|
196
|
+
logger.error("Invalid Location header while following WebFinger redirect: {error}", {
|
|
197
|
+
url: url.href,
|
|
198
|
+
error: e
|
|
199
|
+
});
|
|
200
|
+
return {
|
|
201
|
+
resource: null,
|
|
202
|
+
result: "invalid",
|
|
203
|
+
statusCode: response.status,
|
|
204
|
+
remoteHost
|
|
205
|
+
};
|
|
95
206
|
}
|
|
96
|
-
const redirectedUrl = new URL(response.headers.get("Location"), response.url == null || response.url === "" ? url : response.url);
|
|
97
207
|
if (redirectedUrl.protocol !== url.protocol) {
|
|
98
208
|
logger.error("Redirected to a different protocol ({protocol} to {redirectedProtocol}) while fetching WebFinger resource descriptor.", {
|
|
99
209
|
protocol: url.protocol,
|
|
100
210
|
redirectedProtocol: redirectedUrl.protocol
|
|
101
211
|
});
|
|
102
|
-
return
|
|
212
|
+
return {
|
|
213
|
+
resource: null,
|
|
214
|
+
result: "invalid",
|
|
215
|
+
statusCode: response.status,
|
|
216
|
+
remoteHost
|
|
217
|
+
};
|
|
103
218
|
}
|
|
104
219
|
url = redirectedUrl;
|
|
105
220
|
continue;
|
|
@@ -110,14 +225,29 @@ async function lookupWebFingerInternal(resource, options = {}) {
|
|
|
110
225
|
status: response.status,
|
|
111
226
|
statusText: response.statusText
|
|
112
227
|
});
|
|
113
|
-
return
|
|
228
|
+
return {
|
|
229
|
+
resource: null,
|
|
230
|
+
result: response.status === 404 || response.status === 410 ? "not_found" : "error",
|
|
231
|
+
statusCode: response.status,
|
|
232
|
+
remoteHost
|
|
233
|
+
};
|
|
114
234
|
}
|
|
115
235
|
try {
|
|
116
|
-
return
|
|
236
|
+
return {
|
|
237
|
+
resource: await response.json(),
|
|
238
|
+
result: "found",
|
|
239
|
+
statusCode: response.status,
|
|
240
|
+
remoteHost
|
|
241
|
+
};
|
|
117
242
|
} catch (e) {
|
|
118
243
|
if (e instanceof SyntaxError) {
|
|
119
244
|
logger.debug("Failed to parse WebFinger resource descriptor as JSON: {error}", { error: e });
|
|
120
|
-
return
|
|
245
|
+
return {
|
|
246
|
+
resource: null,
|
|
247
|
+
result: "invalid",
|
|
248
|
+
statusCode: response.status,
|
|
249
|
+
remoteHost
|
|
250
|
+
};
|
|
121
251
|
}
|
|
122
252
|
throw e;
|
|
123
253
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fedify/webfinger",
|
|
3
|
-
"version": "2.3.0-
|
|
3
|
+
"version": "2.3.0-pr.809.37+0574ba5c",
|
|
4
4
|
"homepage": "https://fedify.dev/",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -52,15 +52,15 @@
|
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@types/node": "^24.2.1",
|
|
54
54
|
"fetch-mock": "^12.5.4",
|
|
55
|
-
"tsdown": "^0.
|
|
56
|
-
"typescript": "^
|
|
55
|
+
"tsdown": "^0.22.0",
|
|
56
|
+
"typescript": "^6.0.0",
|
|
57
57
|
"@fedify/fixture": "2.0.0"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@logtape/logtape": "^2.0
|
|
61
|
-
"@opentelemetry/api": "^1.9.
|
|
62
|
-
"es-toolkit": "1.
|
|
63
|
-
"@fedify/vocab-runtime": "2.3.0-
|
|
60
|
+
"@logtape/logtape": "^2.1.0",
|
|
61
|
+
"@opentelemetry/api": "^1.9.1",
|
|
62
|
+
"es-toolkit": "1.46.1",
|
|
63
|
+
"@fedify/vocab-runtime": "2.3.0-pr.809.37+0574ba5c"
|
|
64
64
|
},
|
|
65
65
|
"scripts": {
|
|
66
66
|
"build:self": "tsdown",
|