@fedify/webfinger 2.5.0-dev.2448 → 2.5.0-dev.2492
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 +1 -1
- package/dist/lookup.test.cjs +599 -227
- package/dist/lookup.test.js +600 -227
- package/dist/mod.cjs +226 -160
- package/dist/mod.d.cts +29 -1
- package/dist/mod.d.ts +30 -2
- package/dist/mod.js +227 -162
- package/package.json +3 -3
- package/src/lookup-impl.ts +338 -0
- package/src/lookup.test.ts +569 -80
- package/src/lookup.ts +34 -231
package/dist/mod.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { FetchError, UrlError, getUserAgent, validatePublicUrl } from "@fedify/vocab-runtime";
|
|
2
|
-
import { getLogger } from "@logtape/logtape";
|
|
3
1
|
import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
|
|
2
|
+
import { FetchError, UrlError, getUserAgent } from "@fedify/vocab-runtime";
|
|
3
|
+
import { openDirectFetch, openPublicFetch } from "@fedify/vocab-runtime/internal";
|
|
4
|
+
import { getLogger } from "@logtape/logtape";
|
|
4
5
|
//#region deno.json
|
|
5
6
|
var name = "@fedify/webfinger";
|
|
6
|
-
var version = "2.5.0-dev.
|
|
7
|
+
var version = "2.5.0-dev.2492+4648c04e";
|
|
7
8
|
//#endregion
|
|
8
9
|
//#region src/body.ts
|
|
9
10
|
/** Decoded JSON body limit: 16 MiB. @internal */
|
|
@@ -79,13 +80,230 @@ async function readBoundedText(message, maxBytes, url) {
|
|
|
79
80
|
}
|
|
80
81
|
}
|
|
81
82
|
//#endregion
|
|
82
|
-
//#region src/lookup.ts
|
|
83
|
+
//#region src/lookup-impl.ts
|
|
83
84
|
const logger = getLogger([
|
|
84
85
|
"fedify",
|
|
85
86
|
"webfinger",
|
|
86
87
|
"lookup"
|
|
87
88
|
]);
|
|
88
89
|
const DEFAULT_MAX_REDIRECTION = 5;
|
|
90
|
+
/**
|
|
91
|
+
* Looks up a WebFinger resource, opening a fetch session per hop. This is
|
|
92
|
+
* the implementation of {@link lookupWebFinger}, which is not exported from
|
|
93
|
+
* the package entry point.
|
|
94
|
+
* @param resource The resource URL to look up.
|
|
95
|
+
* @param options Extra options for looking up the resource.
|
|
96
|
+
* @param deps The dependencies.
|
|
97
|
+
* @returns The resource descriptor and terminal lookup classification.
|
|
98
|
+
* @internal
|
|
99
|
+
*/
|
|
100
|
+
async function lookupWebFingerInternal(resource, options = {}, deps = {}) {
|
|
101
|
+
if (typeof resource === "string") resource = new URL(resource);
|
|
102
|
+
let protocol = "https:";
|
|
103
|
+
let server;
|
|
104
|
+
if (resource.protocol === "acct:" || resource.protocol === "mailto:") {
|
|
105
|
+
const atPos = resource.pathname.lastIndexOf("@");
|
|
106
|
+
if (atPos < 0) return {
|
|
107
|
+
resource: null,
|
|
108
|
+
result: "invalid"
|
|
109
|
+
};
|
|
110
|
+
server = resource.pathname.substring(atPos + 1);
|
|
111
|
+
if (server === "" || /[/?#]/.test(server)) return {
|
|
112
|
+
resource: null,
|
|
113
|
+
result: "invalid"
|
|
114
|
+
};
|
|
115
|
+
if (resource.protocol === "acct:" && (resource.search !== "" || resource.hash !== "")) return {
|
|
116
|
+
resource: null,
|
|
117
|
+
result: "invalid"
|
|
118
|
+
};
|
|
119
|
+
} else {
|
|
120
|
+
protocol = resource.protocol;
|
|
121
|
+
server = resource.host;
|
|
122
|
+
}
|
|
123
|
+
let url = new URL(`${protocol}//${server}/.well-known/webfinger`);
|
|
124
|
+
url.searchParams.set("resource", resource.href);
|
|
125
|
+
const openHop = deps.openHop ?? ((hopUrl) => options.allowPrivateAddress === true ? openDirectFetch(hopUrl) : openPublicFetch(hopUrl, { signal: options.signal }));
|
|
126
|
+
let redirected = 0;
|
|
127
|
+
while (true) {
|
|
128
|
+
logger.debug("Fetching WebFinger resource descriptor from {url}...", { url: url.href });
|
|
129
|
+
const remoteHost = url.host;
|
|
130
|
+
const session = await openSession(url, openHop, options.signal);
|
|
131
|
+
if (session == null) return {
|
|
132
|
+
resource: null,
|
|
133
|
+
result: "network_error",
|
|
134
|
+
remoteHost
|
|
135
|
+
};
|
|
136
|
+
let hop;
|
|
137
|
+
try {
|
|
138
|
+
hop = await fetchHop(session, url, options);
|
|
139
|
+
} finally {
|
|
140
|
+
await session.close();
|
|
141
|
+
}
|
|
142
|
+
if ("outcome" in hop) return hop.outcome;
|
|
143
|
+
redirected++;
|
|
144
|
+
const maxRedirection = options.maxRedirection ?? DEFAULT_MAX_REDIRECTION;
|
|
145
|
+
if (redirected > maxRedirection) {
|
|
146
|
+
logger.error("Too many redirections ({redirections}) while fetching WebFinger resource descriptor.", { redirections: redirected });
|
|
147
|
+
return {
|
|
148
|
+
resource: null,
|
|
149
|
+
result: "invalid",
|
|
150
|
+
statusCode: hop.statusCode,
|
|
151
|
+
remoteHost
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
let redirectedUrl;
|
|
155
|
+
try {
|
|
156
|
+
redirectedUrl = new URL(hop.redirect, url);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
logger.error("Invalid Location header while following WebFinger redirect: {error}", {
|
|
159
|
+
url: url.href,
|
|
160
|
+
error
|
|
161
|
+
});
|
|
162
|
+
return {
|
|
163
|
+
resource: null,
|
|
164
|
+
result: "invalid",
|
|
165
|
+
statusCode: hop.statusCode,
|
|
166
|
+
remoteHost
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
if (redirectedUrl.protocol !== url.protocol) {
|
|
170
|
+
logger.error("Redirected to a different protocol ({protocol} to {redirectedProtocol}) while fetching WebFinger resource descriptor.", {
|
|
171
|
+
protocol: url.protocol,
|
|
172
|
+
redirectedProtocol: redirectedUrl.protocol
|
|
173
|
+
});
|
|
174
|
+
return {
|
|
175
|
+
resource: null,
|
|
176
|
+
result: "invalid",
|
|
177
|
+
statusCode: hop.statusCode,
|
|
178
|
+
remoteHost
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
url = redirectedUrl;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Opens a fetch session for a hop. A disallowed URL, a failed DNS lookup,
|
|
186
|
+
* or an aborted lookup yields `null` instead of an error.
|
|
187
|
+
*/
|
|
188
|
+
async function openSession(url, openHop, signal) {
|
|
189
|
+
try {
|
|
190
|
+
return await openHop(url.href);
|
|
191
|
+
} catch (e) {
|
|
192
|
+
if (e instanceof UrlError) {
|
|
193
|
+
if (e.reason === "dns") logger.debug("DNS lookup failed for {url}", {
|
|
194
|
+
url: url.href,
|
|
195
|
+
error: e
|
|
196
|
+
});
|
|
197
|
+
else logger.error("Invalid URL for WebFinger resource descriptor: {error}", { error: e });
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
if (signal?.aborted) {
|
|
201
|
+
logger.debug("Failed to fetch WebFinger resource descriptor: {error}", {
|
|
202
|
+
url: url.href,
|
|
203
|
+
error: e
|
|
204
|
+
});
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
throw e;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Fetches a hop with the given session, and consumes the response body or
|
|
212
|
+
* starts cancelling it before returning. The caller closes the session.
|
|
213
|
+
*/
|
|
214
|
+
async function fetchHop(session, url, options) {
|
|
215
|
+
let response;
|
|
216
|
+
try {
|
|
217
|
+
response = await session.fetch(url, {
|
|
218
|
+
headers: {
|
|
219
|
+
Accept: "application/jrd+json",
|
|
220
|
+
"User-Agent": typeof options.userAgent === "string" ? options.userAgent : getUserAgent(options.userAgent)
|
|
221
|
+
},
|
|
222
|
+
redirect: "manual",
|
|
223
|
+
signal: options.signal
|
|
224
|
+
});
|
|
225
|
+
} catch (error) {
|
|
226
|
+
logger.debug("Failed to fetch WebFinger resource descriptor: {error}", {
|
|
227
|
+
url: url.href,
|
|
228
|
+
error
|
|
229
|
+
});
|
|
230
|
+
return { outcome: {
|
|
231
|
+
resource: null,
|
|
232
|
+
result: "network_error",
|
|
233
|
+
remoteHost: url.host
|
|
234
|
+
} };
|
|
235
|
+
}
|
|
236
|
+
if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
|
|
237
|
+
cancelBody(response, url);
|
|
238
|
+
return {
|
|
239
|
+
redirect: response.headers.get("Location"),
|
|
240
|
+
statusCode: response.status
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
if (!response.ok) {
|
|
244
|
+
logger.debug("Failed to fetch WebFinger resource descriptor: {status} {statusText}.", {
|
|
245
|
+
url: url.href,
|
|
246
|
+
status: response.status,
|
|
247
|
+
statusText: response.statusText
|
|
248
|
+
});
|
|
249
|
+
cancelBody(response, url);
|
|
250
|
+
return { outcome: {
|
|
251
|
+
resource: null,
|
|
252
|
+
result: response.status === 404 || response.status === 410 ? "not_found" : "error",
|
|
253
|
+
statusCode: response.status,
|
|
254
|
+
remoteHost: url.host
|
|
255
|
+
} };
|
|
256
|
+
}
|
|
257
|
+
try {
|
|
258
|
+
const parsed = JSON.parse(await readBoundedText(response, MAX_BODY_SIZE, url));
|
|
259
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return { outcome: {
|
|
260
|
+
resource: null,
|
|
261
|
+
result: "invalid",
|
|
262
|
+
statusCode: response.status,
|
|
263
|
+
remoteHost: url.host
|
|
264
|
+
} };
|
|
265
|
+
return { outcome: {
|
|
266
|
+
resource: parsed,
|
|
267
|
+
result: "found",
|
|
268
|
+
statusCode: response.status,
|
|
269
|
+
remoteHost: url.host
|
|
270
|
+
} };
|
|
271
|
+
} catch (e) {
|
|
272
|
+
if (e instanceof BodyTooLargeError) return { outcome: {
|
|
273
|
+
resource: null,
|
|
274
|
+
result: "invalid",
|
|
275
|
+
statusCode: response.status,
|
|
276
|
+
remoteHost: url.host
|
|
277
|
+
} };
|
|
278
|
+
if (e instanceof SyntaxError) {
|
|
279
|
+
logger.debug("Failed to parse WebFinger resource descriptor as JSON: {error}", { error: e });
|
|
280
|
+
return { outcome: {
|
|
281
|
+
resource: null,
|
|
282
|
+
result: "invalid",
|
|
283
|
+
statusCode: response.status,
|
|
284
|
+
remoteHost: url.host
|
|
285
|
+
} };
|
|
286
|
+
}
|
|
287
|
+
throw e;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Starts cancelling a response body that is not read, without waiting for it
|
|
292
|
+
* to settle. The cancellation of a branch of a tee (e.g., a body cloned by
|
|
293
|
+
* a fetch mock) settles only after the other branch is cancelled too, per
|
|
294
|
+
* the Streams standard, so awaiting it could block forever. A failure is
|
|
295
|
+
* only logged.
|
|
296
|
+
*/
|
|
297
|
+
function cancelBody(response, url) {
|
|
298
|
+
response.body?.cancel().catch((error) => {
|
|
299
|
+
logger.debug("Failed to cancel the response body from {url}: {error}", {
|
|
300
|
+
url: url.href,
|
|
301
|
+
error
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
//#endregion
|
|
306
|
+
//#region src/lookup.ts
|
|
89
307
|
const WEBFINGER_HISTOGRAM_BUCKETS = [
|
|
90
308
|
5,
|
|
91
309
|
10,
|
|
@@ -139,6 +357,9 @@ function getMetricResourceScheme(scheme) {
|
|
|
139
357
|
return WEBFINGER_LOOKUP_SCHEME_WHITELIST.has(scheme) ? scheme : "other";
|
|
140
358
|
}
|
|
141
359
|
async function lookupWebFinger(resource, options = {}) {
|
|
360
|
+
return await lookupWebFingerWithDependencies(resource, options);
|
|
361
|
+
}
|
|
362
|
+
async function lookupWebFingerWithDependencies(resource, options = {}, deps = {}) {
|
|
142
363
|
const tracer = (options.tracerProvider ?? trace.getTracerProvider()).getTracer(name, version);
|
|
143
364
|
const scheme = getResourceScheme(resource);
|
|
144
365
|
return await tracer.startActiveSpan("webfinger.lookup", {
|
|
@@ -155,7 +376,7 @@ async function lookupWebFinger(resource, options = {}) {
|
|
|
155
376
|
result: "error"
|
|
156
377
|
};
|
|
157
378
|
try {
|
|
158
|
-
outcome = await lookupWebFingerInternal(resource, options);
|
|
379
|
+
outcome = await lookupWebFingerInternal(resource, options, deps);
|
|
159
380
|
span.setStatus({ code: outcome.resource === null ? SpanStatusCode.ERROR : SpanStatusCode.OK });
|
|
160
381
|
return options.withDiagnostics ? outcome : outcome.resource;
|
|
161
382
|
} catch (error) {
|
|
@@ -181,161 +402,5 @@ function recordWebFingerLookup(meterProvider, durationMs, scheme, outcome) {
|
|
|
181
402
|
instruments.lookup.add(1, attributes);
|
|
182
403
|
instruments.lookupDuration.record(durationMs, attributes);
|
|
183
404
|
}
|
|
184
|
-
async function lookupWebFingerInternal(resource, options = {}) {
|
|
185
|
-
if (typeof resource === "string") resource = new URL(resource);
|
|
186
|
-
let protocol = "https:";
|
|
187
|
-
let server;
|
|
188
|
-
if (resource.protocol === "acct:" || resource.protocol === "mailto:") {
|
|
189
|
-
const atPos = resource.pathname.lastIndexOf("@");
|
|
190
|
-
if (atPos < 0) return {
|
|
191
|
-
resource: null,
|
|
192
|
-
result: "invalid"
|
|
193
|
-
};
|
|
194
|
-
server = resource.pathname.substring(atPos + 1);
|
|
195
|
-
if (server === "" || /[/?#]/.test(server)) return {
|
|
196
|
-
resource: null,
|
|
197
|
-
result: "invalid"
|
|
198
|
-
};
|
|
199
|
-
if (resource.protocol === "acct:" && (resource.search !== "" || resource.hash !== "")) return {
|
|
200
|
-
resource: null,
|
|
201
|
-
result: "invalid"
|
|
202
|
-
};
|
|
203
|
-
} else {
|
|
204
|
-
protocol = resource.protocol;
|
|
205
|
-
server = resource.host;
|
|
206
|
-
}
|
|
207
|
-
let url = new URL(`${protocol}//${server}/.well-known/webfinger`);
|
|
208
|
-
url.searchParams.set("resource", resource.href);
|
|
209
|
-
let redirected = 0;
|
|
210
|
-
while (true) {
|
|
211
|
-
const remoteHost = url.host;
|
|
212
|
-
logger.debug("Fetching WebFinger resource descriptor from {url}...", { url: url.href });
|
|
213
|
-
let response;
|
|
214
|
-
if (options.allowPrivateAddress !== true) try {
|
|
215
|
-
await validatePublicUrl(url.href);
|
|
216
|
-
} catch (e) {
|
|
217
|
-
if (e instanceof UrlError) {
|
|
218
|
-
if (e.reason === "dns") logger.debug("DNS lookup failed for {url}", {
|
|
219
|
-
url: url.href,
|
|
220
|
-
error: e
|
|
221
|
-
});
|
|
222
|
-
else logger.error("Invalid URL for WebFinger resource descriptor: {error}", { error: e });
|
|
223
|
-
return {
|
|
224
|
-
resource: null,
|
|
225
|
-
result: "network_error",
|
|
226
|
-
remoteHost
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
throw e;
|
|
230
|
-
}
|
|
231
|
-
try {
|
|
232
|
-
response = await fetch(url, {
|
|
233
|
-
headers: {
|
|
234
|
-
Accept: "application/jrd+json",
|
|
235
|
-
"User-Agent": typeof options.userAgent === "string" ? options.userAgent : getUserAgent(options.userAgent)
|
|
236
|
-
},
|
|
237
|
-
redirect: "manual",
|
|
238
|
-
signal: options.signal
|
|
239
|
-
});
|
|
240
|
-
} catch (error) {
|
|
241
|
-
logger.debug("Failed to fetch WebFinger resource descriptor: {error}", {
|
|
242
|
-
url: url.href,
|
|
243
|
-
error
|
|
244
|
-
});
|
|
245
|
-
return {
|
|
246
|
-
resource: null,
|
|
247
|
-
result: "network_error",
|
|
248
|
-
remoteHost
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
|
|
252
|
-
redirected++;
|
|
253
|
-
const maxRedirection = options.maxRedirection ?? DEFAULT_MAX_REDIRECTION;
|
|
254
|
-
if (redirected > maxRedirection) {
|
|
255
|
-
logger.error("Too many redirections ({redirections}) while fetching WebFinger resource descriptor.", { redirections: redirected });
|
|
256
|
-
return {
|
|
257
|
-
resource: null,
|
|
258
|
-
result: "invalid",
|
|
259
|
-
statusCode: response.status,
|
|
260
|
-
remoteHost
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
let redirectedUrl;
|
|
264
|
-
try {
|
|
265
|
-
redirectedUrl = new URL(response.headers.get("Location"), response.url == null || response.url === "" ? url : response.url);
|
|
266
|
-
} catch (e) {
|
|
267
|
-
logger.error("Invalid Location header while following WebFinger redirect: {error}", {
|
|
268
|
-
url: url.href,
|
|
269
|
-
error: e
|
|
270
|
-
});
|
|
271
|
-
return {
|
|
272
|
-
resource: null,
|
|
273
|
-
result: "invalid",
|
|
274
|
-
statusCode: response.status,
|
|
275
|
-
remoteHost
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
if (redirectedUrl.protocol !== url.protocol) {
|
|
279
|
-
logger.error("Redirected to a different protocol ({protocol} to {redirectedProtocol}) while fetching WebFinger resource descriptor.", {
|
|
280
|
-
protocol: url.protocol,
|
|
281
|
-
redirectedProtocol: redirectedUrl.protocol
|
|
282
|
-
});
|
|
283
|
-
return {
|
|
284
|
-
resource: null,
|
|
285
|
-
result: "invalid",
|
|
286
|
-
statusCode: response.status,
|
|
287
|
-
remoteHost
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
url = redirectedUrl;
|
|
291
|
-
continue;
|
|
292
|
-
}
|
|
293
|
-
if (!response.ok) {
|
|
294
|
-
logger.debug("Failed to fetch WebFinger resource descriptor: {status} {statusText}.", {
|
|
295
|
-
url: url.href,
|
|
296
|
-
status: response.status,
|
|
297
|
-
statusText: response.statusText
|
|
298
|
-
});
|
|
299
|
-
return {
|
|
300
|
-
resource: null,
|
|
301
|
-
result: response.status === 404 || response.status === 410 ? "not_found" : "error",
|
|
302
|
-
statusCode: response.status,
|
|
303
|
-
remoteHost
|
|
304
|
-
};
|
|
305
|
-
}
|
|
306
|
-
try {
|
|
307
|
-
const parsed = JSON.parse(await readBoundedText(response, MAX_BODY_SIZE, url));
|
|
308
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {
|
|
309
|
-
resource: null,
|
|
310
|
-
result: "invalid",
|
|
311
|
-
statusCode: response.status,
|
|
312
|
-
remoteHost
|
|
313
|
-
};
|
|
314
|
-
return {
|
|
315
|
-
resource: parsed,
|
|
316
|
-
result: "found",
|
|
317
|
-
statusCode: response.status,
|
|
318
|
-
remoteHost
|
|
319
|
-
};
|
|
320
|
-
} catch (e) {
|
|
321
|
-
if (e instanceof BodyTooLargeError) return {
|
|
322
|
-
resource: null,
|
|
323
|
-
result: "invalid",
|
|
324
|
-
statusCode: response.status,
|
|
325
|
-
remoteHost
|
|
326
|
-
};
|
|
327
|
-
if (e instanceof SyntaxError) {
|
|
328
|
-
logger.debug("Failed to parse WebFinger resource descriptor as JSON: {error}", { error: e });
|
|
329
|
-
return {
|
|
330
|
-
resource: null,
|
|
331
|
-
result: "invalid",
|
|
332
|
-
statusCode: response.status,
|
|
333
|
-
remoteHost
|
|
334
|
-
};
|
|
335
|
-
}
|
|
336
|
-
throw e;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
405
|
//#endregion
|
|
341
|
-
export { lookupWebFinger };
|
|
406
|
+
export { lookupWebFinger, lookupWebFingerWithDependencies };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fedify/webfinger",
|
|
3
|
-
"version": "2.5.0-dev.
|
|
3
|
+
"version": "2.5.0-dev.2492+4648c04e",
|
|
4
4
|
"homepage": "https://fedify.dev/",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -55,9 +55,9 @@
|
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"@logtape/logtape": "^2.2.0",
|
|
58
|
-
"@opentelemetry/api": "
|
|
58
|
+
"@opentelemetry/api": "~1.9.1",
|
|
59
59
|
"es-toolkit": "1.46.1",
|
|
60
|
-
"@fedify/vocab-runtime": "2.5.0-dev.
|
|
60
|
+
"@fedify/vocab-runtime": "2.5.0-dev.2492+4648c04e"
|
|
61
61
|
},
|
|
62
62
|
"scripts": {
|
|
63
63
|
"build:self": "tsdown",
|