@prosopo/provider 3.2.5 → 3.12.3
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/CHANGELOG.md +578 -0
- package/dist/api/admin/apiRemoveDetectorKeyEndpoint.js +7 -4
- package/dist/api/blacklistRequestInspector.js +26 -20
- package/dist/api/captcha.js +121 -33
- package/dist/api/domainMiddleware.js +8 -8
- package/dist/api/headerCheckMiddleware.js +4 -0
- package/dist/api/ignoreMiddleware.js +4 -1
- package/dist/api/ja4Middleware.js +5 -23
- package/dist/api/public.js +26 -3
- package/dist/api/verify.js +4 -2
- package/dist/cjs/api/admin/apiRemoveDetectorKeyEndpoint.cjs +6 -3
- package/dist/cjs/api/blacklistRequestInspector.cjs +25 -19
- package/dist/cjs/api/captcha.cjs +121 -33
- package/dist/cjs/api/domainMiddleware.cjs +8 -8
- package/dist/cjs/api/headerCheckMiddleware.cjs +4 -0
- package/dist/cjs/api/ignoreMiddleware.cjs +3 -0
- package/dist/cjs/api/ja4Middleware.cjs +4 -22
- package/dist/cjs/api/public.cjs +26 -3
- package/dist/cjs/api/verify.cjs +4 -2
- package/dist/cjs/compositeIpAddress.cjs +53 -0
- package/dist/cjs/index.cjs +7 -0
- package/dist/cjs/pairs.cjs +27 -0
- package/dist/cjs/services/ipComparison.cjs +123 -0
- package/dist/cjs/services/ipInfo.cjs +87 -0
- package/dist/cjs/tasks/captchaManager.cjs +49 -2
- package/dist/cjs/tasks/client/clientTasks.cjs +33 -12
- package/dist/cjs/tasks/detection/decodePayload.cjs +712 -278
- package/dist/cjs/tasks/detection/getBotScore.cjs +14 -3
- package/dist/cjs/tasks/frictionless/frictionlessTasks.cjs +128 -24
- package/dist/cjs/tasks/frictionless/frictionlessTasksUtils.cjs +17 -0
- package/dist/cjs/tasks/imgCaptcha/imgCaptchaTasks.cjs +62 -20
- package/dist/cjs/tasks/powCaptcha/powTasks.cjs +43 -15
- package/dist/cjs/util.cjs +248 -16
- package/dist/cjs/utils/hashUserAgent.cjs +10 -0
- package/dist/compositeIpAddress.js +53 -0
- package/dist/index.js +8 -1
- package/dist/pairs.js +27 -0
- package/dist/services/ipComparison.js +123 -0
- package/dist/services/ipInfo.js +87 -0
- package/dist/tasks/captchaManager.js +49 -2
- package/dist/tasks/client/clientTasks.js +33 -12
- package/dist/tasks/detection/decodePayload.js +712 -278
- package/dist/tasks/detection/getBotScore.js +15 -4
- package/dist/tasks/frictionless/frictionlessTasks.js +128 -24
- package/dist/tasks/frictionless/frictionlessTasksUtils.js +18 -1
- package/dist/tasks/imgCaptcha/imgCaptchaTasks.js +64 -22
- package/dist/tasks/powCaptcha/powTasks.js +44 -16
- package/dist/util.js +249 -17
- package/dist/utils/hashUserAgent.js +10 -0
- package/package.json +28 -25
- package/vite.test.config.ts +3 -2
- package/vite.threads.test.config.ts +33 -0
package/dist/api/captcha.js
CHANGED
|
@@ -4,8 +4,11 @@ import { parseCaptchaAssets } from "@prosopo/datasets";
|
|
|
4
4
|
import { ClientApiPaths, CaptchaRequestBody, CaptchaType, ApiParams, CaptchaSolutionBody, GetPowCaptchaChallengeRequestBody, SubmitPowCaptchaSolutionBody, GetFrictionlessCaptchaChallengeRequestBody } from "@prosopo/types";
|
|
5
5
|
import { getIPAddress, flatten } from "@prosopo/util";
|
|
6
6
|
import express from "express";
|
|
7
|
+
import { getCompositeIpAddress } from "../compositeIpAddress.js";
|
|
7
8
|
import { FrictionlessManager } from "../tasks/frictionless/frictionlessTasks.js";
|
|
9
|
+
import { timestampDecayFunction } from "../tasks/frictionless/frictionlessTasksUtils.js";
|
|
8
10
|
import { Tasks } from "../tasks/tasks.js";
|
|
11
|
+
import { hashUserAgent } from "../utils/hashUserAgent.js";
|
|
9
12
|
import { getRequestUserScope } from "./blacklistRequestInspector.js";
|
|
10
13
|
import { validateSiteKey, validateAddr } from "./validateAddress.js";
|
|
11
14
|
const DEFAULT_FRICTIONLESS_THRESHOLD = 0.5;
|
|
@@ -63,11 +66,13 @@ function prosopoRouter(env) {
|
|
|
63
66
|
dapp,
|
|
64
67
|
userScope
|
|
65
68
|
))[0];
|
|
66
|
-
const { valid, reason, frictionlessTokenId } = await tasks.imgCaptchaManager.isValidRequest(
|
|
69
|
+
const { valid, reason, frictionlessTokenId, solvedImagesCount } = await tasks.imgCaptchaManager.isValidRequest(
|
|
67
70
|
clientRecord,
|
|
68
71
|
CaptchaType.image,
|
|
72
|
+
env,
|
|
69
73
|
sessionId,
|
|
70
|
-
userAccessPolicy
|
|
74
|
+
userAccessPolicy,
|
|
75
|
+
req.ip
|
|
71
76
|
);
|
|
72
77
|
if (!valid) {
|
|
73
78
|
return next(
|
|
@@ -84,7 +89,7 @@ function prosopoRouter(env) {
|
|
|
84
89
|
}
|
|
85
90
|
const captchaConfig = {
|
|
86
91
|
solved: {
|
|
87
|
-
count: userAccessPolicy?.solvedImagesCount || env.config.captchas.solved.count
|
|
92
|
+
count: solvedImagesCount || userAccessPolicy?.solvedImagesCount || env.config.captchas.solved.count
|
|
88
93
|
},
|
|
89
94
|
unsolved: {
|
|
90
95
|
count: userAccessPolicy?.unsolvedImagesCount || env.config.captchas.unsolved.count
|
|
@@ -115,12 +120,23 @@ function prosopoRouter(env) {
|
|
|
115
120
|
}
|
|
116
121
|
}
|
|
117
122
|
};
|
|
123
|
+
req.logger.info(() => ({
|
|
124
|
+
msg: "Image captcha challenge issued",
|
|
125
|
+
data: {
|
|
126
|
+
captchaType: CaptchaType.image,
|
|
127
|
+
requestHash: taskData.requestHash,
|
|
128
|
+
solvedImagesCount: captchaConfig.solved.count,
|
|
129
|
+
user,
|
|
130
|
+
dapp,
|
|
131
|
+
sessionId
|
|
132
|
+
}
|
|
133
|
+
}));
|
|
118
134
|
return res.json(captchaResponse);
|
|
119
135
|
} catch (err) {
|
|
120
136
|
req.logger.error(() => ({
|
|
121
137
|
err,
|
|
122
138
|
data: req.params,
|
|
123
|
-
msg: "Error in
|
|
139
|
+
msg: "Error in image captcha challenge request"
|
|
124
140
|
}));
|
|
125
141
|
return next(
|
|
126
142
|
new ProsopoApiError("API.BAD_REQUEST", {
|
|
@@ -175,7 +191,7 @@ function prosopoRouter(env) {
|
|
|
175
191
|
parsed[ApiParams.signature].user.timestamp,
|
|
176
192
|
Number.parseInt(parsed[ApiParams.timestamp]),
|
|
177
193
|
parsed[ApiParams.signature].provider.requestHash,
|
|
178
|
-
getIPAddress(req.ip || "")
|
|
194
|
+
getIPAddress(req.ip || ""),
|
|
179
195
|
flatten(req.headers),
|
|
180
196
|
req.ja4
|
|
181
197
|
);
|
|
@@ -190,7 +206,7 @@ function prosopoRouter(env) {
|
|
|
190
206
|
req.logger.error(() => ({
|
|
191
207
|
err,
|
|
192
208
|
body: req.body,
|
|
193
|
-
msg: "Error in
|
|
209
|
+
msg: "Error in image captcha solution submission"
|
|
194
210
|
}));
|
|
195
211
|
return next(
|
|
196
212
|
new ProsopoApiError("API.BAD_REQUEST", {
|
|
@@ -246,11 +262,13 @@ function prosopoRouter(env) {
|
|
|
246
262
|
dapp,
|
|
247
263
|
userScope
|
|
248
264
|
))[0];
|
|
249
|
-
const { valid, reason, frictionlessTokenId } = await tasks.powCaptchaManager.isValidRequest(
|
|
265
|
+
const { valid, reason, frictionlessTokenId, powDifficulty } = await tasks.powCaptchaManager.isValidRequest(
|
|
250
266
|
clientSettings,
|
|
251
267
|
CaptchaType.pow,
|
|
268
|
+
env,
|
|
252
269
|
sessionId,
|
|
253
|
-
userAccessPolicy
|
|
270
|
+
userAccessPolicy,
|
|
271
|
+
req.ip
|
|
254
272
|
);
|
|
255
273
|
if (!valid) {
|
|
256
274
|
return next(
|
|
@@ -280,11 +298,12 @@ function prosopoRouter(env) {
|
|
|
280
298
|
})
|
|
281
299
|
);
|
|
282
300
|
}
|
|
301
|
+
const difficulty = powDifficulty || userAccessPolicy?.powDifficulty || clientSettings?.settings?.powDifficulty;
|
|
283
302
|
const challenge = await tasks.powCaptchaManager.getPowCaptchaChallenge(
|
|
284
303
|
user,
|
|
285
304
|
dapp,
|
|
286
305
|
origin,
|
|
287
|
-
|
|
306
|
+
difficulty
|
|
288
307
|
);
|
|
289
308
|
await tasks.db.storePowCaptchaRecord(
|
|
290
309
|
challenge.challenge,
|
|
@@ -295,7 +314,7 @@ function prosopoRouter(env) {
|
|
|
295
314
|
},
|
|
296
315
|
challenge.difficulty,
|
|
297
316
|
challenge.providerSignature,
|
|
298
|
-
|
|
317
|
+
getCompositeIpAddress(req.ip || ""),
|
|
299
318
|
flatten(req.headers),
|
|
300
319
|
req.ja4,
|
|
301
320
|
frictionlessTokenId
|
|
@@ -311,12 +330,23 @@ function prosopoRouter(env) {
|
|
|
311
330
|
}
|
|
312
331
|
}
|
|
313
332
|
};
|
|
333
|
+
req.logger.info(() => ({
|
|
334
|
+
msg: "PoW captcha challenge issued",
|
|
335
|
+
data: {
|
|
336
|
+
captchaType: CaptchaType.pow,
|
|
337
|
+
challenge: challenge.challenge,
|
|
338
|
+
difficulty: challenge.difficulty,
|
|
339
|
+
user,
|
|
340
|
+
dapp,
|
|
341
|
+
session: sessionId
|
|
342
|
+
}
|
|
343
|
+
}));
|
|
314
344
|
return res.json(getPowCaptchaResponse);
|
|
315
345
|
} catch (err) {
|
|
316
346
|
req.logger.error(() => ({
|
|
317
347
|
err,
|
|
318
348
|
body: req.body,
|
|
319
|
-
msg: "Error in PoW captcha
|
|
349
|
+
msg: "Error in PoW captcha challenge request"
|
|
320
350
|
}));
|
|
321
351
|
return next(
|
|
322
352
|
new ProsopoApiError("API.BAD_REQUEST", {
|
|
@@ -348,15 +378,7 @@ function prosopoRouter(env) {
|
|
|
348
378
|
})
|
|
349
379
|
);
|
|
350
380
|
}
|
|
351
|
-
const {
|
|
352
|
-
challenge,
|
|
353
|
-
difficulty,
|
|
354
|
-
signature,
|
|
355
|
-
nonce,
|
|
356
|
-
verifiedTimeout,
|
|
357
|
-
dapp,
|
|
358
|
-
user
|
|
359
|
-
} = parsed;
|
|
381
|
+
const { challenge, signature, nonce, verifiedTimeout, dapp, user } = parsed;
|
|
360
382
|
validateSiteKey(dapp);
|
|
361
383
|
validateAddr(user);
|
|
362
384
|
try {
|
|
@@ -372,7 +394,6 @@ function prosopoRouter(env) {
|
|
|
372
394
|
}
|
|
373
395
|
const verified = await tasks.powCaptchaManager.verifyPowCaptchaSolution(
|
|
374
396
|
challenge,
|
|
375
|
-
difficulty,
|
|
376
397
|
signature.provider.challenge,
|
|
377
398
|
nonce,
|
|
378
399
|
verifiedTimeout,
|
|
@@ -414,17 +435,39 @@ function prosopoRouter(env) {
|
|
|
414
435
|
token: existingToken,
|
|
415
436
|
msg: "Token has already been used"
|
|
416
437
|
}));
|
|
417
|
-
return
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
438
|
+
return next(
|
|
439
|
+
new ProsopoApiError("API.BAD_REQUEST", {
|
|
440
|
+
context: {
|
|
441
|
+
code: 400,
|
|
442
|
+
siteKey: dapp,
|
|
443
|
+
user
|
|
444
|
+
},
|
|
445
|
+
i18n: req.i18n,
|
|
446
|
+
logger: req.logger
|
|
447
|
+
})
|
|
421
448
|
);
|
|
422
449
|
}
|
|
423
450
|
const lScore = tasks.frictionlessManager.checkLangRules(
|
|
424
451
|
req.headers["accept-language"] || ""
|
|
425
452
|
);
|
|
426
|
-
const {
|
|
427
|
-
|
|
453
|
+
const {
|
|
454
|
+
baseBotScore,
|
|
455
|
+
timestamp,
|
|
456
|
+
providerSelectEntropy,
|
|
457
|
+
userId,
|
|
458
|
+
userAgent
|
|
459
|
+
} = await tasks.frictionlessManager.decryptPayload(token);
|
|
460
|
+
req.logger.debug(() => ({
|
|
461
|
+
msg: "Decrypted payload",
|
|
462
|
+
data: {
|
|
463
|
+
baseBotScore,
|
|
464
|
+
timestamp,
|
|
465
|
+
providerSelectEntropy,
|
|
466
|
+
userId,
|
|
467
|
+
userAgent
|
|
468
|
+
}
|
|
469
|
+
}));
|
|
470
|
+
let botScore = baseBotScore + lScore;
|
|
428
471
|
const clientRecord = await tasks.db.getClientRecord(dapp);
|
|
429
472
|
if (!clientRecord) {
|
|
430
473
|
return next(
|
|
@@ -437,7 +480,8 @@ function prosopoRouter(env) {
|
|
|
437
480
|
}
|
|
438
481
|
const { valid, reason } = await tasks.frictionlessManager.isValidRequest(
|
|
439
482
|
clientRecord,
|
|
440
|
-
CaptchaType.frictionless
|
|
483
|
+
CaptchaType.frictionless,
|
|
484
|
+
env
|
|
441
485
|
);
|
|
442
486
|
if (!valid) {
|
|
443
487
|
return next(
|
|
@@ -460,7 +504,9 @@ function prosopoRouter(env) {
|
|
|
460
504
|
scoreComponents: {
|
|
461
505
|
baseScore: baseBotScore,
|
|
462
506
|
...lScore && { lScore }
|
|
463
|
-
}
|
|
507
|
+
},
|
|
508
|
+
providerSelectEntropy,
|
|
509
|
+
ipAddress: getCompositeIpAddress(req.ip || "")
|
|
464
510
|
});
|
|
465
511
|
const userScope = getRequestUserScope(
|
|
466
512
|
flatten(req.headers),
|
|
@@ -473,6 +519,28 @@ function prosopoRouter(env) {
|
|
|
473
519
|
dapp,
|
|
474
520
|
userScope
|
|
475
521
|
))[0];
|
|
522
|
+
const headersUserAgent = req.headers["user-agent"];
|
|
523
|
+
const hashedHeadersUserAgent = headersUserAgent ? hashUserAgent(headersUserAgent) : "";
|
|
524
|
+
const headersProsopoUser = req.headers["prosopo-user"];
|
|
525
|
+
if (hashedHeadersUserAgent !== userAgent || headersProsopoUser !== userId) {
|
|
526
|
+
req.logger.info(() => ({
|
|
527
|
+
msg: "User agent or user id does not match",
|
|
528
|
+
data: {
|
|
529
|
+
headersUserAgent,
|
|
530
|
+
hashedHeadersUserAgent,
|
|
531
|
+
userAgent,
|
|
532
|
+
// This is the hashed user agent from the token
|
|
533
|
+
headersProsopoUser,
|
|
534
|
+
userId
|
|
535
|
+
}
|
|
536
|
+
}));
|
|
537
|
+
return res.json(
|
|
538
|
+
await tasks.frictionlessManager.sendImageCaptcha(
|
|
539
|
+
tokenId,
|
|
540
|
+
timestampDecayFunction(timestamp)
|
|
541
|
+
)
|
|
542
|
+
);
|
|
543
|
+
}
|
|
476
544
|
if (userAccessPolicy) {
|
|
477
545
|
await tasks.frictionlessManager.scoreIncreaseAccessPolicy(
|
|
478
546
|
userAccessPolicy,
|
|
@@ -482,7 +550,10 @@ function prosopoRouter(env) {
|
|
|
482
550
|
);
|
|
483
551
|
if (userAccessPolicy.captchaType === CaptchaType.image) {
|
|
484
552
|
return res.json(
|
|
485
|
-
await tasks.frictionlessManager.sendImageCaptcha(
|
|
553
|
+
await tasks.frictionlessManager.sendImageCaptcha(
|
|
554
|
+
tokenId,
|
|
555
|
+
userAccessPolicy.solvedImagesCount
|
|
556
|
+
)
|
|
486
557
|
);
|
|
487
558
|
}
|
|
488
559
|
if (userAccessPolicy.captchaType === CaptchaType.pow) {
|
|
@@ -499,12 +570,26 @@ function prosopoRouter(env) {
|
|
|
499
570
|
tokenId
|
|
500
571
|
);
|
|
501
572
|
return res.json(
|
|
502
|
-
await tasks.frictionlessManager.sendImageCaptcha(
|
|
573
|
+
await tasks.frictionlessManager.sendImageCaptcha(
|
|
574
|
+
tokenId,
|
|
575
|
+
timestampDecayFunction(timestamp)
|
|
576
|
+
)
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
const hostVerified = await tasks.frictionlessManager.hostVerified(
|
|
580
|
+
providerSelectEntropy
|
|
581
|
+
);
|
|
582
|
+
if (!hostVerified.verified) {
|
|
583
|
+
botScore = await tasks.frictionlessManager.scoreIncreaseUnverifiedHost(
|
|
584
|
+
hostVerified.domain,
|
|
585
|
+
baseBotScore,
|
|
586
|
+
botScore,
|
|
587
|
+
tokenId
|
|
503
588
|
);
|
|
504
589
|
}
|
|
505
590
|
if (Number(botScore) > botThreshold) {
|
|
506
591
|
req.logger.info(() => ({
|
|
507
|
-
|
|
592
|
+
msg: "Bot score is greater than threshold",
|
|
508
593
|
data: {
|
|
509
594
|
botScore,
|
|
510
595
|
botThreshold,
|
|
@@ -512,7 +597,10 @@ function prosopoRouter(env) {
|
|
|
512
597
|
}
|
|
513
598
|
}));
|
|
514
599
|
return res.json(
|
|
515
|
-
await tasks.frictionlessManager.sendImageCaptcha(
|
|
600
|
+
await tasks.frictionlessManager.sendImageCaptcha(
|
|
601
|
+
tokenId,
|
|
602
|
+
env.config.captchas.solved.count
|
|
603
|
+
)
|
|
516
604
|
);
|
|
517
605
|
}
|
|
518
606
|
return res.json(
|
|
@@ -8,26 +8,26 @@ const domainMiddleware = (env) => {
|
|
|
8
8
|
const tasks = new Tasks(env);
|
|
9
9
|
return async (req, res, next) => {
|
|
10
10
|
try {
|
|
11
|
-
const
|
|
12
|
-
if (!
|
|
11
|
+
const siteKey = req.headers["prosopo-site-key"];
|
|
12
|
+
if (!siteKey)
|
|
13
13
|
throw siteKeyNotRegisteredError(
|
|
14
14
|
req.i18n,
|
|
15
15
|
"No sitekey provided",
|
|
16
16
|
req.logger
|
|
17
17
|
);
|
|
18
18
|
try {
|
|
19
|
-
validateAddress(
|
|
19
|
+
validateAddress(siteKey, false, 42);
|
|
20
20
|
} catch (err) {
|
|
21
|
-
throw invalidSiteKeyError(req.i18n,
|
|
21
|
+
throw invalidSiteKeyError(req.i18n, siteKey, req.logger);
|
|
22
22
|
}
|
|
23
|
-
const clientSettings = await tasks.db.getClientRecord(
|
|
23
|
+
const clientSettings = await tasks.db.getClientRecord(siteKey);
|
|
24
24
|
if (!clientSettings)
|
|
25
|
-
throw siteKeyNotRegisteredError(req.i18n,
|
|
25
|
+
throw siteKeyNotRegisteredError(req.i18n, siteKey, req.logger);
|
|
26
26
|
const allowedDomains = clientSettings.settings?.domains;
|
|
27
27
|
if (!allowedDomains)
|
|
28
28
|
throw siteKeyInvalidDomainError(
|
|
29
29
|
req.i18n,
|
|
30
|
-
|
|
30
|
+
siteKey,
|
|
31
31
|
req.hostname,
|
|
32
32
|
req.logger
|
|
33
33
|
);
|
|
@@ -35,7 +35,7 @@ const domainMiddleware = (env) => {
|
|
|
35
35
|
if (!origin)
|
|
36
36
|
throw unauthorizedOriginError(req.i18n, void 0, req.logger);
|
|
37
37
|
for (const domain of allowedDomains) {
|
|
38
|
-
if (tasks.clientTaskManager.
|
|
38
|
+
if (tasks.clientTaskManager.domainPatternMatcher(origin, domain)) {
|
|
39
39
|
next();
|
|
40
40
|
return;
|
|
41
41
|
}
|
|
@@ -17,6 +17,10 @@ const headerCheckMiddleware = (env) => {
|
|
|
17
17
|
validateAddr(user, void 0, req.logger);
|
|
18
18
|
req.user = user;
|
|
19
19
|
req.siteKey = siteKey;
|
|
20
|
+
req.logger = req.logger.with({
|
|
21
|
+
user,
|
|
22
|
+
siteKey
|
|
23
|
+
});
|
|
20
24
|
next();
|
|
21
25
|
} catch (err) {
|
|
22
26
|
return handleErrors(err, req, res, next);
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import { ApiPrefix } from "@prosopo/types";
|
|
1
|
+
import { PublicApiPaths, ApiPrefix } from "@prosopo/types";
|
|
2
2
|
function ignoreMiddleware() {
|
|
3
3
|
return (req, res, next) => {
|
|
4
|
+
if (req.originalUrl.indexOf(PublicApiPaths.Healthz) !== -1) {
|
|
5
|
+
return next();
|
|
6
|
+
}
|
|
4
7
|
if (req.originalUrl.indexOf(ApiPrefix) === -1) {
|
|
5
8
|
res.statusCode = 404;
|
|
6
9
|
res.send("Not Found");
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
1
|
import { Readable } from "node:stream";
|
|
3
2
|
import { handleErrors } from "@prosopo/api-express-router";
|
|
4
3
|
import { getLogger } from "@prosopo/common";
|
|
5
4
|
import { randomAsHex } from "@prosopo/util-crypto";
|
|
6
|
-
import { readTlsClientHello } from "read-tls-client-hello";
|
|
5
|
+
import { readTlsClientHello, calculateJa4FromHelloData } from "read-tls-client-hello";
|
|
7
6
|
const DEFAULT_JA4 = "ja4";
|
|
8
7
|
const getJA4 = async (headers, logger) => {
|
|
9
8
|
logger = logger || getLogger("info", import.meta.url);
|
|
@@ -15,7 +14,6 @@ const getJA4 = async (headers, logger) => {
|
|
|
15
14
|
try {
|
|
16
15
|
const xTlsClientHello = (headers["x-tls-clienthello"] || "").toString();
|
|
17
16
|
const xTlsVersion = (headers["x-tls-version"] || "").toString().toLowerCase();
|
|
18
|
-
const xTlsServerName = (headers["x-tls-server-name"] || "").toString();
|
|
19
17
|
const clientHelloBuffer = Buffer.from(xTlsClientHello, "base64");
|
|
20
18
|
logger.debug(() => ({
|
|
21
19
|
msg: "ClientHello First Bytes:",
|
|
@@ -31,32 +29,13 @@ const getJA4 = async (headers, logger) => {
|
|
|
31
29
|
msg: "Headers TLS Version:",
|
|
32
30
|
data: { xTlsVersion }
|
|
33
31
|
}));
|
|
34
|
-
const tlsVersion = xTlsVersion.replace(/(tls)|\./g, "");
|
|
35
32
|
const readableStream = new Readable({
|
|
36
33
|
read() {
|
|
37
34
|
this.push(clientHelloBuffer);
|
|
38
35
|
}
|
|
39
36
|
});
|
|
40
37
|
const clientHello = await readTlsClientHello(readableStream);
|
|
41
|
-
const
|
|
42
|
-
const [_tlsVersion, cipherSuites, extensions] = clientHello.fingerprintData;
|
|
43
|
-
const transport = "t";
|
|
44
|
-
const sniIndicator = xTlsServerName ? "d" : "i";
|
|
45
|
-
const validCipherSuites = cipherSuites.filter(
|
|
46
|
-
(cs) => (cs & 3855) !== 2570
|
|
47
|
-
);
|
|
48
|
-
const cipherCount = validCipherSuites.length;
|
|
49
|
-
const validExtensions = extensions.filter(
|
|
50
|
-
(ext) => (ext & 3855) !== 2570
|
|
51
|
-
);
|
|
52
|
-
const extensionCount = validExtensions.length;
|
|
53
|
-
const alpn = alpnProtocols?.length ? alpnProtocols[0] : "";
|
|
54
|
-
const alpnLabel = alpn ? `${alpn[0]}${alpn[alpn.length - 1]}` : "00";
|
|
55
|
-
const sortedCiphers = validCipherSuites.map((cs) => cs.toString(16).padStart(4, "0")).sort().join(",");
|
|
56
|
-
const cipherHash = createHash("sha256").update(sortedCiphers).digest("hex").slice(0, 12);
|
|
57
|
-
const decimalString = extensions.sort((a, b) => a - b).map((ext) => ext.toString(10)).join("-");
|
|
58
|
-
const extensionHash = createHash("sha256").update(decimalString).digest("hex").slice(0, 12);
|
|
59
|
-
const ja4PlusFingerprint = `${transport}${tlsVersion}${sniIndicator}${cipherCount}${extensionCount}${alpnLabel}_${cipherHash}_${extensionHash}`;
|
|
38
|
+
const ja4PlusFingerprint = calculateJa4FromHelloData(clientHello);
|
|
60
39
|
return { ja4PlusFingerprint };
|
|
61
40
|
} catch (e) {
|
|
62
41
|
logger.error(() => ({
|
|
@@ -72,6 +51,9 @@ const ja4Middleware = (env) => {
|
|
|
72
51
|
req.logger.debug(() => ({ data: { url: req.url } }));
|
|
73
52
|
const ja4 = await getJA4(req.headers, req.logger);
|
|
74
53
|
req.ja4 = ja4.ja4PlusFingerprint || "";
|
|
54
|
+
req.logger = req.logger.with({
|
|
55
|
+
ja4: req.ja4
|
|
56
|
+
});
|
|
75
57
|
next();
|
|
76
58
|
} catch (err) {
|
|
77
59
|
return handleErrors(err, req, res, next);
|
package/dist/api/public.js
CHANGED
|
@@ -3,16 +3,39 @@ import { ProsopoApiError } from "@prosopo/common";
|
|
|
3
3
|
import { PublicApiPaths } from "@prosopo/types";
|
|
4
4
|
import { version } from "@prosopo/util";
|
|
5
5
|
import express from "express";
|
|
6
|
-
function publicRouter() {
|
|
6
|
+
function publicRouter(env) {
|
|
7
7
|
const router = express.Router();
|
|
8
8
|
router.get(PublicApiPaths.Healthz, (req, res) => {
|
|
9
9
|
res.status(200).send("OK");
|
|
10
10
|
});
|
|
11
11
|
router.get(PublicApiPaths.GetProviderDetails, async (req, res, next) => {
|
|
12
12
|
try {
|
|
13
|
-
|
|
13
|
+
const db = env.getDb();
|
|
14
|
+
const redisConnection = db.getRedisConnection();
|
|
15
|
+
const redisAccessRulesConnection = db.getRedisAccessRulesConnection();
|
|
16
|
+
const response = {
|
|
17
|
+
version,
|
|
18
|
+
message: "Provider online",
|
|
19
|
+
redis: [
|
|
20
|
+
{
|
|
21
|
+
actor: "General",
|
|
22
|
+
isReady: redisConnection.isReady(),
|
|
23
|
+
awaitingTimeSeconds: Math.ceil(
|
|
24
|
+
redisConnection.getAwaitingTimeMs() / 1e3
|
|
25
|
+
)
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
actor: "UAP",
|
|
29
|
+
isReady: redisAccessRulesConnection.isReady(),
|
|
30
|
+
awaitingTimeSeconds: Math.ceil(
|
|
31
|
+
redisAccessRulesConnection.getAwaitingTimeMs() / 1e3
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
};
|
|
36
|
+
return res.json(response);
|
|
14
37
|
} catch (err) {
|
|
15
|
-
|
|
38
|
+
env.logger.error(() => ({
|
|
16
39
|
err,
|
|
17
40
|
data: { reqParams: req.params },
|
|
18
41
|
msg: "Error getting provider details"
|
package/dist/api/verify.js
CHANGED
|
@@ -22,7 +22,7 @@ function prosopoVerifyRouter(env) {
|
|
|
22
22
|
})
|
|
23
23
|
);
|
|
24
24
|
}
|
|
25
|
-
const { dappSignature, token, ip } = parsed;
|
|
25
|
+
const { dappSignature, token, ip, maxVerifiedTime } = parsed;
|
|
26
26
|
try {
|
|
27
27
|
const { user, dapp, timestamp, commitmentId } = decodeProcaptchaOutput(token);
|
|
28
28
|
validateAddress(dapp, false, 42);
|
|
@@ -43,7 +43,8 @@ function prosopoVerifyRouter(env) {
|
|
|
43
43
|
user,
|
|
44
44
|
dapp,
|
|
45
45
|
commitmentId,
|
|
46
|
-
|
|
46
|
+
env,
|
|
47
|
+
maxVerifiedTime,
|
|
47
48
|
ip
|
|
48
49
|
);
|
|
49
50
|
req.logger.debug(() => ({ data: { response } }));
|
|
@@ -111,6 +112,7 @@ function prosopoVerifyRouter(env) {
|
|
|
111
112
|
dapp,
|
|
112
113
|
challenge,
|
|
113
114
|
verifiedTimeout,
|
|
115
|
+
env,
|
|
114
116
|
ip
|
|
115
117
|
);
|
|
116
118
|
const verificationResponse = tasks.powCaptchaManager.getVerificationResponse(
|
|
@@ -10,10 +10,13 @@ class ApiRemoveDetectorKeyEndpoint {
|
|
|
10
10
|
async processRequest(args, logger) {
|
|
11
11
|
logger = logger || common.getLogger("info", module);
|
|
12
12
|
try {
|
|
13
|
-
const { detectorKey } = args;
|
|
13
|
+
const { detectorKey, expirationInSeconds } = args;
|
|
14
14
|
logger = logger || common.getLogger("info", module);
|
|
15
15
|
logger.info(() => ({ msg: "Removing detector key" }));
|
|
16
|
-
await this.clientTaskManager.removeDetectorKey(
|
|
16
|
+
await this.clientTaskManager.removeDetectorKey(
|
|
17
|
+
detectorKey,
|
|
18
|
+
expirationInSeconds
|
|
19
|
+
);
|
|
17
20
|
return {
|
|
18
21
|
status: apiRoute.ApiEndpointResponseStatus.SUCCESS
|
|
19
22
|
};
|
|
@@ -26,7 +29,7 @@ class ApiRemoveDetectorKeyEndpoint {
|
|
|
26
29
|
}
|
|
27
30
|
}
|
|
28
31
|
getRequestArgsSchema() {
|
|
29
|
-
return types.
|
|
32
|
+
return types.RemoveDetectorKeyBodySpec;
|
|
30
33
|
}
|
|
31
34
|
}
|
|
32
35
|
exports.ApiRemoveDetectorKeyEndpoint = ApiRemoveDetectorKeyEndpoint;
|
|
@@ -10,13 +10,12 @@ const getRequestUserScope = (requestHeaders, ja4, ip, user) => {
|
|
|
10
10
|
...ja4 && { ja4Hash: ja4 },
|
|
11
11
|
...userAgent && { userAgent },
|
|
12
12
|
...ip && { ip }
|
|
13
|
+
// TODO more things with headers
|
|
13
14
|
};
|
|
14
15
|
};
|
|
15
|
-
const
|
|
16
|
-
const userScopeKeys = Object.keys(userScope)
|
|
17
|
-
|
|
18
|
-
);
|
|
19
|
-
const prioritisedUserScopes = util.uniqueSubsets(userScopeKeys).map(
|
|
16
|
+
const getPrioritisedUserScopes = (userScope) => {
|
|
17
|
+
const userScopeKeys = Object.keys(userScope);
|
|
18
|
+
return util.uniqueSubsets(userScopeKeys).map(
|
|
20
19
|
(subset) => subset.reduce(
|
|
21
20
|
(acc, key) => {
|
|
22
21
|
acc[key] = userScope[key];
|
|
@@ -24,22 +23,29 @@ const getPrioritisedAccessRule = async (userAccessRulesStorage, userScope, clien
|
|
|
24
23
|
},
|
|
25
24
|
{}
|
|
26
25
|
)
|
|
27
|
-
)
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
const getPrioritisedAccessRule = async (userAccessRulesStorage, userScope, clientId) => {
|
|
29
|
+
const prioritisedUserScopes = getPrioritisedUserScopes(userScope);
|
|
28
30
|
const policyPromises = [];
|
|
29
|
-
|
|
31
|
+
const clientLoop = clientId ? [clientId, void 0] : [void 0];
|
|
32
|
+
for (const clientOrUndefined of clientLoop) {
|
|
30
33
|
for (const scope of prioritisedUserScopes) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
34
|
+
if (Object.values(scope).every((value) => value === void 0)) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const parsedUserScope = userAccessPolicy.userScopeInputSchema.parse(scope);
|
|
38
|
+
const filter = {
|
|
39
|
+
...clientOrUndefined && {
|
|
40
|
+
policyScope: {
|
|
41
|
+
clientId: clientOrUndefined
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
policyScopeMatch: userAccessPolicy.ScopeMatch.Exact,
|
|
45
|
+
userScope: parsedUserScope,
|
|
46
|
+
userScopeMatch: userAccessPolicy.ScopeMatch.Exact
|
|
47
|
+
};
|
|
48
|
+
policyPromises.push(userAccessRulesStorage.findRules(filter, true, true));
|
|
43
49
|
}
|
|
44
50
|
}
|
|
45
51
|
return (await Promise.all(policyPromises)).flat();
|