@prosopo/provider 2.5.5 → 2.7.1
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 +57 -0
- package/dist/api/captcha.d.ts.map +1 -1
- package/dist/api/captcha.js +1 -1
- package/dist/api/captcha.js.map +1 -1
- package/dist/api/ja4Middleware.d.ts +6 -0
- package/dist/api/ja4Middleware.d.ts.map +1 -1
- package/dist/api/ja4Middleware.js +7 -7
- package/dist/api/ja4Middleware.js.map +1 -1
- package/dist/cjs/api/admin/apiAdminRoutesProvider.cjs +32 -0
- package/dist/cjs/api/admin/apiRegisterSiteKeyEndpoint.cjs +25 -0
- package/dist/cjs/api/admin/apiRemoveDetectorKeyEndpoint.cjs +32 -0
- package/dist/cjs/api/admin/apiUpdateDetectorKeyEndpoint.cjs +32 -0
- package/dist/cjs/api/admin/createApiAdminRoutesProvider.cjs +10 -0
- package/dist/cjs/api/authMiddleware.cjs +81 -0
- package/dist/cjs/api/blacklistRequestInspector.cjs +73 -0
- package/dist/cjs/api/block.cjs +24 -0
- package/dist/cjs/api/captcha.cjs +482 -0
- package/dist/cjs/api/domainMiddleware.cjs +89 -0
- package/dist/cjs/api/headerCheckMiddleware.cjs +29 -0
- package/dist/cjs/api/ignoreMiddleware.cjs +14 -0
- package/dist/cjs/api/ja4Middleware.cjs +73 -0
- package/dist/cjs/api/public.cjs +27 -0
- package/dist/cjs/api/requestLoggerMiddleware.cjs +14 -0
- package/dist/cjs/api/robotsMiddleware.cjs +12 -0
- package/dist/cjs/api/validateAddress.cjs +19 -0
- package/dist/cjs/api/verify.cjs +138 -0
- package/dist/cjs/index.cjs +41 -0
- package/dist/cjs/rules/lang.cjs +16 -0
- package/dist/cjs/schedulers/captchaScheduler.cjs +31 -0
- package/dist/cjs/schedulers/getClientList.cjs +29 -0
- package/dist/cjs/tasks/captchaManager.cjs +90 -0
- package/dist/cjs/tasks/client/clientTasks.cjs +281 -0
- package/dist/cjs/tasks/dataset/datasetTasks.cjs +30 -0
- package/dist/cjs/tasks/dataset/datasetTasksUtils.cjs +34 -0
- package/dist/cjs/tasks/detection/decodePayload.cjs +475 -0
- package/dist/cjs/tasks/detection/getBotScore.cjs +13 -0
- package/dist/cjs/tasks/frictionless/frictionlessTasks.cjs +121 -0
- package/dist/cjs/tasks/frictionless/frictionlessTasksUtils.cjs +11 -0
- package/dist/cjs/tasks/imgCaptcha/imgCaptchaTasks.cjs +366 -0
- package/dist/cjs/tasks/imgCaptcha/imgCaptchaTasksUtils.cjs +25 -0
- package/dist/cjs/tasks/index.cjs +4 -0
- package/dist/cjs/tasks/powCaptcha/powTasks.cjs +155 -0
- package/dist/cjs/tasks/powCaptcha/powTasksUtils.cjs +26 -0
- package/dist/cjs/tasks/tasks.cjs +51 -0
- package/dist/cjs/util.cjs +58 -0
- package/dist/schedulers/captchaScheduler.d.ts +1 -1
- package/dist/schedulers/captchaScheduler.d.ts.map +1 -1
- package/dist/schedulers/captchaScheduler.js +1 -7
- package/dist/schedulers/captchaScheduler.js.map +1 -1
- package/dist/schedulers/getClientList.d.ts +1 -1
- package/dist/schedulers/getClientList.d.ts.map +1 -1
- package/dist/schedulers/getClientList.js +1 -7
- package/dist/schedulers/getClientList.js.map +1 -1
- package/dist/tasks/client/clientTasks.d.ts +5 -2
- package/dist/tasks/client/clientTasks.d.ts.map +1 -1
- package/dist/tasks/client/clientTasks.js +55 -9
- package/dist/tasks/client/clientTasks.js.map +1 -1
- package/dist/tasks/imgCaptcha/imgCaptchaTasks.d.ts +1 -1
- package/dist/tasks/imgCaptcha/imgCaptchaTasks.d.ts.map +1 -1
- package/dist/tasks/imgCaptcha/imgCaptchaTasks.js +13 -3
- package/dist/tasks/imgCaptcha/imgCaptchaTasks.js.map +1 -1
- package/dist/tests/unit/api/ja4Middleware.unit.test.d.ts +2 -0
- package/dist/tests/unit/api/ja4Middleware.unit.test.d.ts.map +1 -0
- package/dist/tests/unit/api/ja4Middleware.unit.test.js +57 -0
- package/dist/tests/unit/api/ja4Middleware.unit.test.js.map +1 -0
- package/dist/tests/unit/schedulers/captchaScheduler.unit.test.js +2 -2
- package/dist/tests/unit/schedulers/captchaScheduler.unit.test.js.map +1 -1
- package/dist/tests/unit/tasks/captchaManager.unit.test.js +1 -0
- package/dist/tests/unit/tasks/captchaManager.unit.test.js.map +1 -1
- package/dist/tests/unit/tasks/client/clientTasks.unit.test.js +11 -0
- package/dist/tests/unit/tasks/client/clientTasks.unit.test.js.map +1 -1
- package/dist/tests/unit/tasks/imgCaptcha/imgCaptchaTasks.unit.test.js +2 -2
- package/dist/tests/unit/tasks/imgCaptcha/imgCaptchaTasks.unit.test.js.map +1 -1
- package/package.json +16 -15
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const apiExpressRouter = require("@prosopo/api-express-router");
|
|
4
|
+
const common = require("@prosopo/common");
|
|
5
|
+
const datasets = require("@prosopo/datasets");
|
|
6
|
+
const types = require("@prosopo/types");
|
|
7
|
+
const userAccessPolicy = require("@prosopo/user-access-policy");
|
|
8
|
+
const util$1 = require("@prosopo/util");
|
|
9
|
+
const express = require("express");
|
|
10
|
+
const frictionlessTasks = require("../tasks/frictionless/frictionlessTasks.cjs");
|
|
11
|
+
const tasks = require("../tasks/tasks.cjs");
|
|
12
|
+
const util = require("../util.cjs");
|
|
13
|
+
const validateAddress = require("./validateAddress.cjs");
|
|
14
|
+
const DEFAULT_FRICTIONLESS_THRESHOLD = 0.5;
|
|
15
|
+
function prosopoRouter(env) {
|
|
16
|
+
const router = express.Router();
|
|
17
|
+
const tasks$1 = new tasks.Tasks(env);
|
|
18
|
+
const userAccessRulesStorage = env.getDb().getUserAccessRulesStorage();
|
|
19
|
+
router.post(
|
|
20
|
+
types.ClientApiPaths.GetImageCaptchaChallenge,
|
|
21
|
+
async (req, res, next) => {
|
|
22
|
+
let parsed;
|
|
23
|
+
if (!req.ip) {
|
|
24
|
+
return next(
|
|
25
|
+
new common.ProsopoApiError("API.BAD_REQUEST", {
|
|
26
|
+
context: { code: 400, error: "IP address not found" },
|
|
27
|
+
i18n: req.i18n,
|
|
28
|
+
logger: req.logger
|
|
29
|
+
})
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
const ipAddress = util.getIPAddress(req.ip || "");
|
|
33
|
+
try {
|
|
34
|
+
parsed = types.CaptchaRequestBody.parse(req.body);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
return next(
|
|
37
|
+
new common.ProsopoApiError("CAPTCHA.PARSE_ERROR", {
|
|
38
|
+
context: { code: 400, error: err },
|
|
39
|
+
i18n: req.i18n,
|
|
40
|
+
logger: req.logger
|
|
41
|
+
})
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
const { datasetId, user, dapp, sessionId } = parsed;
|
|
45
|
+
validateAddress.validiateSiteKey(dapp);
|
|
46
|
+
validateAddress.validateAddr(user);
|
|
47
|
+
try {
|
|
48
|
+
const clientRecord = await tasks$1.db.getClientRecord(dapp);
|
|
49
|
+
if (!clientRecord) {
|
|
50
|
+
return next(
|
|
51
|
+
new common.ProsopoApiError("API.SITE_KEY_NOT_REGISTERED", {
|
|
52
|
+
context: { code: 400, siteKey: dapp },
|
|
53
|
+
i18n: req.i18n,
|
|
54
|
+
logger: req.logger
|
|
55
|
+
})
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
const imageCaptchaConfigResolver = userAccessPolicy.createImageCaptchaConfigResolver(
|
|
59
|
+
userAccessRulesStorage,
|
|
60
|
+
req.logger
|
|
61
|
+
);
|
|
62
|
+
const captchaConfig = await imageCaptchaConfigResolver.resolveConfig(
|
|
63
|
+
env.config.captchas,
|
|
64
|
+
ipAddress,
|
|
65
|
+
req.ja4,
|
|
66
|
+
user,
|
|
67
|
+
dapp
|
|
68
|
+
);
|
|
69
|
+
const { valid, reason, frictionlessTokenId } = await tasks$1.imgCaptchaManager.isValidRequest(
|
|
70
|
+
clientRecord,
|
|
71
|
+
types.CaptchaType.image,
|
|
72
|
+
sessionId
|
|
73
|
+
);
|
|
74
|
+
if (!valid) {
|
|
75
|
+
return next(
|
|
76
|
+
new common.ProsopoApiError(reason || "API.BAD_REQUEST", {
|
|
77
|
+
context: {
|
|
78
|
+
code: 400,
|
|
79
|
+
siteKey: dapp,
|
|
80
|
+
user
|
|
81
|
+
},
|
|
82
|
+
i18n: req.i18n,
|
|
83
|
+
logger: req.logger
|
|
84
|
+
})
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
const taskData = await tasks$1.imgCaptchaManager.getRandomCaptchasAndRequestHash(
|
|
88
|
+
datasetId,
|
|
89
|
+
user,
|
|
90
|
+
ipAddress,
|
|
91
|
+
captchaConfig,
|
|
92
|
+
clientRecord.settings.imageThreshold,
|
|
93
|
+
frictionlessTokenId
|
|
94
|
+
);
|
|
95
|
+
const captchaResponse = {
|
|
96
|
+
[types.ApiParams.status]: "ok",
|
|
97
|
+
[types.ApiParams.captchas]: taskData.captchas.map((captcha) => ({
|
|
98
|
+
...captcha,
|
|
99
|
+
target: req.t(`TARGET.${captcha.target}`),
|
|
100
|
+
items: captcha.items.map(
|
|
101
|
+
(item) => datasets.parseCaptchaAssets(item, env.assetsResolver)
|
|
102
|
+
)
|
|
103
|
+
})),
|
|
104
|
+
[types.ApiParams.requestHash]: taskData.requestHash,
|
|
105
|
+
[types.ApiParams.timestamp]: taskData.timestamp.toString(),
|
|
106
|
+
[types.ApiParams.signature]: {
|
|
107
|
+
[types.ApiParams.provider]: {
|
|
108
|
+
[types.ApiParams.requestHash]: taskData.signedRequestHash
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
return res.json(captchaResponse);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
req.logger.error({ err, params: req.params });
|
|
115
|
+
return next(
|
|
116
|
+
new common.ProsopoApiError("API.BAD_REQUEST", {
|
|
117
|
+
context: {
|
|
118
|
+
error: err,
|
|
119
|
+
code: 500,
|
|
120
|
+
params: req.params,
|
|
121
|
+
context: err
|
|
122
|
+
},
|
|
123
|
+
i18n: req.i18n,
|
|
124
|
+
logger: req.logger
|
|
125
|
+
})
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
);
|
|
130
|
+
router.post(
|
|
131
|
+
types.ClientApiPaths.SubmitImageCaptchaSolution,
|
|
132
|
+
async (req, res, next) => {
|
|
133
|
+
let parsed;
|
|
134
|
+
try {
|
|
135
|
+
parsed = types.CaptchaSolutionBody.parse(req.body);
|
|
136
|
+
} catch (err) {
|
|
137
|
+
return next(
|
|
138
|
+
new common.ProsopoApiError("CAPTCHA.PARSE_ERROR", {
|
|
139
|
+
context: { code: 400, error: err, body: req.body },
|
|
140
|
+
i18n: req.i18n,
|
|
141
|
+
logger: req.logger
|
|
142
|
+
})
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
const { user, dapp } = parsed;
|
|
146
|
+
validateAddress.validiateSiteKey(dapp);
|
|
147
|
+
validateAddress.validateAddr(user);
|
|
148
|
+
try {
|
|
149
|
+
const clientRecord = await tasks$1.db.getClientRecord(parsed.dapp);
|
|
150
|
+
if (!clientRecord) {
|
|
151
|
+
return next(
|
|
152
|
+
new common.ProsopoApiError("API.SITE_KEY_NOT_REGISTERED", {
|
|
153
|
+
context: { code: 400, siteKey: dapp },
|
|
154
|
+
i18n: req.i18n,
|
|
155
|
+
logger: req.logger
|
|
156
|
+
})
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
const result = await tasks$1.imgCaptchaManager.dappUserSolution(
|
|
160
|
+
user,
|
|
161
|
+
dapp,
|
|
162
|
+
parsed[types.ApiParams.requestHash],
|
|
163
|
+
parsed[types.ApiParams.captchas],
|
|
164
|
+
parsed[types.ApiParams.signature].user.timestamp,
|
|
165
|
+
Number.parseInt(parsed[types.ApiParams.timestamp]),
|
|
166
|
+
parsed[types.ApiParams.signature].provider.requestHash,
|
|
167
|
+
util.getIPAddress(req.ip || "").bigInt(),
|
|
168
|
+
util$1.flatten(req.headers),
|
|
169
|
+
req.ja4
|
|
170
|
+
);
|
|
171
|
+
const returnValue = {
|
|
172
|
+
status: req.i18n.t(
|
|
173
|
+
result.verified ? "API.CAPTCHA_PASSED" : "API.CAPTCHA_FAILED"
|
|
174
|
+
),
|
|
175
|
+
...result
|
|
176
|
+
};
|
|
177
|
+
return res.json(returnValue);
|
|
178
|
+
} catch (err) {
|
|
179
|
+
req.logger.error({ err, body: req.body });
|
|
180
|
+
return next(
|
|
181
|
+
new common.ProsopoApiError("API.BAD_REQUEST", {
|
|
182
|
+
context: {
|
|
183
|
+
code: 500,
|
|
184
|
+
siteKey: req.body.dapp,
|
|
185
|
+
error: err
|
|
186
|
+
},
|
|
187
|
+
i18n: req.i18n,
|
|
188
|
+
logger: req.logger
|
|
189
|
+
})
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
);
|
|
194
|
+
router.post(types.ClientApiPaths.GetPowCaptchaChallenge, async (req, res, next) => {
|
|
195
|
+
let parsed;
|
|
196
|
+
try {
|
|
197
|
+
parsed = types.GetPowCaptchaChallengeRequestBody.parse(req.body);
|
|
198
|
+
} catch (err) {
|
|
199
|
+
return next(
|
|
200
|
+
new common.ProsopoApiError("CAPTCHA.PARSE_ERROR", {
|
|
201
|
+
context: { code: 400, error: err },
|
|
202
|
+
i18n: req.i18n,
|
|
203
|
+
logger: req.logger
|
|
204
|
+
})
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
const { user, dapp, sessionId } = parsed;
|
|
208
|
+
validateAddress.validiateSiteKey(dapp);
|
|
209
|
+
validateAddress.validateAddr(user);
|
|
210
|
+
try {
|
|
211
|
+
const clientSettings = await tasks$1.db.getClientRecord(dapp);
|
|
212
|
+
if (!clientSettings) {
|
|
213
|
+
return next(
|
|
214
|
+
new common.ProsopoApiError("API.SITE_KEY_NOT_REGISTERED", {
|
|
215
|
+
context: { code: 400, siteKey: dapp },
|
|
216
|
+
i18n: req.i18n,
|
|
217
|
+
logger: req.logger
|
|
218
|
+
})
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
const { valid, reason, frictionlessTokenId } = await tasks$1.powCaptchaManager.isValidRequest(
|
|
222
|
+
clientSettings,
|
|
223
|
+
types.CaptchaType.pow,
|
|
224
|
+
sessionId
|
|
225
|
+
);
|
|
226
|
+
if (!valid) {
|
|
227
|
+
return next(
|
|
228
|
+
new common.ProsopoApiError(reason || "API.BAD_REQUEST", {
|
|
229
|
+
context: {
|
|
230
|
+
code: 400,
|
|
231
|
+
siteKey: dapp,
|
|
232
|
+
user
|
|
233
|
+
},
|
|
234
|
+
i18n: req.i18n,
|
|
235
|
+
logger: req.logger
|
|
236
|
+
})
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
const origin = req.headers.origin;
|
|
240
|
+
if (!origin) {
|
|
241
|
+
return next(
|
|
242
|
+
new common.ProsopoApiError("API.BAD_REQUEST", {
|
|
243
|
+
context: {
|
|
244
|
+
error: "Origin header not found",
|
|
245
|
+
code: 400,
|
|
246
|
+
siteKey: dapp,
|
|
247
|
+
user
|
|
248
|
+
},
|
|
249
|
+
i18n: req.i18n,
|
|
250
|
+
logger: req.logger
|
|
251
|
+
})
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
const challenge = await tasks$1.powCaptchaManager.getPowCaptchaChallenge(
|
|
255
|
+
user,
|
|
256
|
+
dapp,
|
|
257
|
+
origin,
|
|
258
|
+
clientSettings?.settings?.powDifficulty
|
|
259
|
+
);
|
|
260
|
+
await tasks$1.db.storePowCaptchaRecord(
|
|
261
|
+
challenge.challenge,
|
|
262
|
+
{
|
|
263
|
+
requestedAtTimestamp: challenge.requestedAtTimestamp,
|
|
264
|
+
userAccount: user,
|
|
265
|
+
dappAccount: dapp
|
|
266
|
+
},
|
|
267
|
+
challenge.difficulty,
|
|
268
|
+
challenge.providerSignature,
|
|
269
|
+
util.getIPAddress(req.ip || "").bigInt(),
|
|
270
|
+
util$1.flatten(req.headers),
|
|
271
|
+
req.ja4,
|
|
272
|
+
frictionlessTokenId
|
|
273
|
+
);
|
|
274
|
+
const getPowCaptchaResponse = {
|
|
275
|
+
[types.ApiParams.status]: "ok",
|
|
276
|
+
[types.ApiParams.challenge]: challenge.challenge,
|
|
277
|
+
[types.ApiParams.difficulty]: challenge.difficulty,
|
|
278
|
+
[types.ApiParams.timestamp]: challenge.requestedAtTimestamp.toString(),
|
|
279
|
+
[types.ApiParams.signature]: {
|
|
280
|
+
[types.ApiParams.provider]: {
|
|
281
|
+
[types.ApiParams.challenge]: challenge.providerSignature
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
return res.json(getPowCaptchaResponse);
|
|
286
|
+
} catch (err) {
|
|
287
|
+
req.logger.error({ err, body: req.body });
|
|
288
|
+
return next(
|
|
289
|
+
new common.ProsopoApiError("API.BAD_REQUEST", {
|
|
290
|
+
context: {
|
|
291
|
+
code: 500,
|
|
292
|
+
siteKey: req.body.dapp,
|
|
293
|
+
user: req.body.user,
|
|
294
|
+
error: err
|
|
295
|
+
},
|
|
296
|
+
i18n: req.i18n,
|
|
297
|
+
logger: req.logger
|
|
298
|
+
})
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
router.post(
|
|
303
|
+
types.ClientApiPaths.SubmitPowCaptchaSolution,
|
|
304
|
+
async (req, res, next) => {
|
|
305
|
+
let parsed;
|
|
306
|
+
try {
|
|
307
|
+
parsed = types.SubmitPowCaptchaSolutionBody.parse(req.body);
|
|
308
|
+
} catch (err) {
|
|
309
|
+
return next(
|
|
310
|
+
new common.ProsopoApiError("CAPTCHA.PARSE_ERROR", {
|
|
311
|
+
context: { code: 400, error: err, body: req.body },
|
|
312
|
+
i18n: req.i18n,
|
|
313
|
+
logger: req.logger
|
|
314
|
+
})
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
const {
|
|
318
|
+
challenge,
|
|
319
|
+
difficulty,
|
|
320
|
+
signature,
|
|
321
|
+
nonce,
|
|
322
|
+
verifiedTimeout,
|
|
323
|
+
dapp,
|
|
324
|
+
user
|
|
325
|
+
} = parsed;
|
|
326
|
+
validateAddress.validiateSiteKey(dapp);
|
|
327
|
+
validateAddress.validateAddr(user);
|
|
328
|
+
try {
|
|
329
|
+
const clientRecord = await tasks$1.db.getClientRecord(dapp);
|
|
330
|
+
if (!clientRecord) {
|
|
331
|
+
return next(
|
|
332
|
+
new common.ProsopoApiError("API.SITE_KEY_NOT_REGISTERED", {
|
|
333
|
+
context: { code: 400, siteKey: dapp },
|
|
334
|
+
i18n: req.i18n,
|
|
335
|
+
logger: req.logger
|
|
336
|
+
})
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
const verified = await tasks$1.powCaptchaManager.verifyPowCaptchaSolution(
|
|
340
|
+
challenge,
|
|
341
|
+
difficulty,
|
|
342
|
+
signature.provider.challenge,
|
|
343
|
+
nonce,
|
|
344
|
+
verifiedTimeout,
|
|
345
|
+
signature.user.timestamp,
|
|
346
|
+
util.getIPAddress(req.ip || ""),
|
|
347
|
+
util$1.flatten(req.headers)
|
|
348
|
+
);
|
|
349
|
+
const response = { status: "ok", verified };
|
|
350
|
+
return res.json(response);
|
|
351
|
+
} catch (err) {
|
|
352
|
+
req.logger.error({ err, body: req.body });
|
|
353
|
+
return next(
|
|
354
|
+
new common.ProsopoApiError("API.BAD_REQUEST", {
|
|
355
|
+
context: {
|
|
356
|
+
code: 500,
|
|
357
|
+
siteKey: req.body.dapp,
|
|
358
|
+
error: err
|
|
359
|
+
},
|
|
360
|
+
i18n: req.i18n,
|
|
361
|
+
logger: req.logger
|
|
362
|
+
})
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
);
|
|
367
|
+
router.post(
|
|
368
|
+
types.ClientApiPaths.GetFrictionlessCaptchaChallenge,
|
|
369
|
+
async (req, res, next) => {
|
|
370
|
+
try {
|
|
371
|
+
const { token, dapp, user } = types.GetFrictionlessCaptchaChallengeRequestBody.parse(req.body);
|
|
372
|
+
const existingToken = await tasks$1.db.getFrictionlessTokenRecordByToken(token);
|
|
373
|
+
if (existingToken) {
|
|
374
|
+
req.logger.info(`Token ${existingToken} has already been used`);
|
|
375
|
+
return res.json(
|
|
376
|
+
await tasks$1.frictionlessManager.sendImageCaptcha(
|
|
377
|
+
existingToken._id
|
|
378
|
+
)
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
const lScore = tasks$1.frictionlessManager.checkLangRules(
|
|
382
|
+
req.headers["accept-language"] || ""
|
|
383
|
+
);
|
|
384
|
+
const { baseBotScore, timestamp } = await tasks$1.frictionlessManager.decryptPayload(token);
|
|
385
|
+
const botScore = baseBotScore + lScore;
|
|
386
|
+
const clientRecord = await tasks$1.db.getClientRecord(dapp);
|
|
387
|
+
if (!clientRecord) {
|
|
388
|
+
return next(
|
|
389
|
+
new common.ProsopoApiError("API.SITE_KEY_NOT_REGISTERED", {
|
|
390
|
+
context: { code: 400, siteKey: dapp },
|
|
391
|
+
i18n: req.i18n,
|
|
392
|
+
logger: req.logger
|
|
393
|
+
})
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
const { valid, reason } = await tasks$1.frictionlessManager.isValidRequest(
|
|
397
|
+
clientRecord,
|
|
398
|
+
types.CaptchaType.frictionless
|
|
399
|
+
);
|
|
400
|
+
if (!valid) {
|
|
401
|
+
return next(
|
|
402
|
+
new common.ProsopoApiError(reason || "API.BAD_REQUEST", {
|
|
403
|
+
context: {
|
|
404
|
+
code: 400,
|
|
405
|
+
siteKey: dapp,
|
|
406
|
+
user
|
|
407
|
+
},
|
|
408
|
+
i18n: req.i18n,
|
|
409
|
+
logger: req.logger
|
|
410
|
+
})
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
const botThreshold = clientRecord.settings?.frictionlessThreshold || DEFAULT_FRICTIONLESS_THRESHOLD;
|
|
414
|
+
const tokenId = await tasks$1.db.storeFrictionlessTokenRecord({
|
|
415
|
+
token,
|
|
416
|
+
score: botScore,
|
|
417
|
+
threshold: botThreshold,
|
|
418
|
+
scoreComponents: {
|
|
419
|
+
baseScore: baseBotScore,
|
|
420
|
+
...lScore && { lScore }
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
if (frictionlessTasks.FrictionlessManager.timestampTooOld(timestamp)) {
|
|
424
|
+
await tasks$1.frictionlessManager.scoreIncreaseTimestamp(
|
|
425
|
+
timestamp,
|
|
426
|
+
baseBotScore,
|
|
427
|
+
botScore,
|
|
428
|
+
tokenId
|
|
429
|
+
);
|
|
430
|
+
return res.json(
|
|
431
|
+
await tasks$1.frictionlessManager.sendImageCaptcha(tokenId)
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
const ipAddress = util.getIPAddress(req.ip || "");
|
|
435
|
+
const imageCaptchaConfigResolver = userAccessPolicy.createImageCaptchaConfigResolver(
|
|
436
|
+
userAccessRulesStorage,
|
|
437
|
+
req.logger
|
|
438
|
+
);
|
|
439
|
+
const imageCaptchaConfigDefined = await imageCaptchaConfigResolver.isConfigDefined(
|
|
440
|
+
dapp,
|
|
441
|
+
ipAddress,
|
|
442
|
+
req.ja4,
|
|
443
|
+
user
|
|
444
|
+
);
|
|
445
|
+
if (imageCaptchaConfigDefined) {
|
|
446
|
+
await tasks$1.frictionlessManager.scoreIncreaseAccessPolicy(
|
|
447
|
+
imageCaptchaConfigResolver.accessRule,
|
|
448
|
+
baseBotScore,
|
|
449
|
+
botScore,
|
|
450
|
+
tokenId
|
|
451
|
+
);
|
|
452
|
+
return res.json(
|
|
453
|
+
await tasks$1.frictionlessManager.sendImageCaptcha(tokenId)
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
if (Number(botScore) > botThreshold) {
|
|
457
|
+
req.logger.info({
|
|
458
|
+
message: `Bot score ${botScore} is greater than threshold ${botThreshold}`
|
|
459
|
+
});
|
|
460
|
+
return res.json(
|
|
461
|
+
await tasks$1.frictionlessManager.sendImageCaptcha(tokenId)
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
return res.json(
|
|
465
|
+
await tasks$1.frictionlessManager.sendPowCaptcha(tokenId)
|
|
466
|
+
);
|
|
467
|
+
} catch (err) {
|
|
468
|
+
req.logger.error("Error in frictionless captcha challenge:", err);
|
|
469
|
+
return next(
|
|
470
|
+
new common.ProsopoApiError("API.BAD_REQUEST", {
|
|
471
|
+
context: { code: 400, error: err },
|
|
472
|
+
i18n: req.i18n,
|
|
473
|
+
logger: req.logger
|
|
474
|
+
})
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
);
|
|
479
|
+
router.use(apiExpressRouter.handleErrors);
|
|
480
|
+
return router;
|
|
481
|
+
}
|
|
482
|
+
exports.prosopoRouter = prosopoRouter;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const utilCrypto = require("@polkadot/util-crypto");
|
|
4
|
+
const apiExpressRouter = require("@prosopo/api-express-router");
|
|
5
|
+
const common = require("@prosopo/common");
|
|
6
|
+
const zod = require("zod");
|
|
7
|
+
require("../tasks/index.cjs");
|
|
8
|
+
const tasks = require("../tasks/tasks.cjs");
|
|
9
|
+
const domainMiddleware = (env) => {
|
|
10
|
+
const tasks$1 = new tasks.Tasks(env);
|
|
11
|
+
return async (req, res, next) => {
|
|
12
|
+
try {
|
|
13
|
+
const dapp = req.headers["prosopo-site-key"];
|
|
14
|
+
if (!dapp)
|
|
15
|
+
throw siteKeyNotRegisteredError(
|
|
16
|
+
req.i18n,
|
|
17
|
+
"No sitekey provided",
|
|
18
|
+
req.logger
|
|
19
|
+
);
|
|
20
|
+
try {
|
|
21
|
+
utilCrypto.validateAddress(dapp, false, 42);
|
|
22
|
+
} catch (err) {
|
|
23
|
+
throw invalidSiteKeyError(req.i18n, dapp, req.logger);
|
|
24
|
+
}
|
|
25
|
+
const clientSettings = await tasks$1.db.getClientRecord(dapp);
|
|
26
|
+
if (!clientSettings)
|
|
27
|
+
throw siteKeyNotRegisteredError(req.i18n, dapp, req.logger);
|
|
28
|
+
const allowedDomains = clientSettings.settings?.domains;
|
|
29
|
+
if (!allowedDomains)
|
|
30
|
+
throw siteKeyInvalidDomainError(
|
|
31
|
+
req.i18n,
|
|
32
|
+
dapp,
|
|
33
|
+
req.hostname,
|
|
34
|
+
req.logger
|
|
35
|
+
);
|
|
36
|
+
const origin = req.headers.origin;
|
|
37
|
+
if (!origin)
|
|
38
|
+
throw unauthorizedOriginError(req.i18n, void 0, req.logger);
|
|
39
|
+
for (const domain of allowedDomains) {
|
|
40
|
+
if (tasks$1.clientTaskManager.isSubdomainOrExactMatch(origin, domain)) {
|
|
41
|
+
next();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
throw unauthorizedOriginError(req.i18n, origin, req.logger);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
if (err instanceof common.ProsopoApiError || err instanceof zod.ZodError || err instanceof SyntaxError) {
|
|
48
|
+
apiExpressRouter.handleErrors(err, req, res, next);
|
|
49
|
+
} else {
|
|
50
|
+
res.status(401).json({ error: "Unauthorized", message: err });
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
const siteKeyNotRegisteredError = (i18n, dapp, logger) => {
|
|
57
|
+
return new common.ProsopoApiError("API.SITE_KEY_NOT_REGISTERED", {
|
|
58
|
+
context: { code: 400, siteKey: dapp },
|
|
59
|
+
i18n,
|
|
60
|
+
logger
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
const invalidSiteKeyError = (i18n, dapp, logger) => {
|
|
64
|
+
return new common.ProsopoApiError("API.INVALID_SITE_KEY", {
|
|
65
|
+
context: { code: 400, siteKey: dapp },
|
|
66
|
+
i18n,
|
|
67
|
+
logger
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
const unauthorizedOriginError = (i18n, origin, logger) => {
|
|
71
|
+
return new common.ProsopoApiError("API.UNAUTHORIZED_ORIGIN_URL", {
|
|
72
|
+
context: { code: 400, origin },
|
|
73
|
+
i18n,
|
|
74
|
+
logger
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
const siteKeyInvalidDomainError = (i18n, dapp, domain, logger) => {
|
|
78
|
+
return new common.ProsopoApiError("API.UNAUTHORIZED_ORIGIN_URL", {
|
|
79
|
+
context: {
|
|
80
|
+
code: 400,
|
|
81
|
+
message: "No domains are allowed for this site key. Please fix in the Procaptcha Portal",
|
|
82
|
+
siteKey: dapp,
|
|
83
|
+
domain
|
|
84
|
+
},
|
|
85
|
+
i18n,
|
|
86
|
+
logger
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
exports.domainMiddleware = domainMiddleware;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const apiExpressRouter = require("@prosopo/api-express-router");
|
|
4
|
+
const validateAddress = require("./validateAddress.cjs");
|
|
5
|
+
const headerCheckMiddleware = (env) => {
|
|
6
|
+
return async (req, res, next) => {
|
|
7
|
+
try {
|
|
8
|
+
const user = req.headers["prosopo-user"];
|
|
9
|
+
const siteKey = req.headers["prosopo-site-key"];
|
|
10
|
+
if (!user) {
|
|
11
|
+
unauthorised(res);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (!siteKey) {
|
|
15
|
+
unauthorised(res);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
validateAddress.validiateSiteKey(siteKey, req.logger);
|
|
19
|
+
validateAddress.validateAddr(user, void 0, req.logger);
|
|
20
|
+
req.user = user;
|
|
21
|
+
req.siteKey = siteKey;
|
|
22
|
+
next();
|
|
23
|
+
} catch (err) {
|
|
24
|
+
return apiExpressRouter.handleErrors(err, req, res, next);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
const unauthorised = (res) => res.status(401).json({ error: "Unauthorized", message: "Unauthorized" });
|
|
29
|
+
exports.headerCheckMiddleware = headerCheckMiddleware;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const types = require("@prosopo/types");
|
|
4
|
+
function ignoreMiddleware() {
|
|
5
|
+
return (req, res, next) => {
|
|
6
|
+
if (req.originalUrl.indexOf(types.ApiPrefix) === -1) {
|
|
7
|
+
res.statusCode = 404;
|
|
8
|
+
res.send("Not Found");
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
next();
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
exports.ignoreMiddleware = ignoreMiddleware;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const node_crypto = require("node:crypto");
|
|
4
|
+
const node_stream = require("node:stream");
|
|
5
|
+
const apiExpressRouter = require("@prosopo/api-express-router");
|
|
6
|
+
const common = require("@prosopo/common");
|
|
7
|
+
const readTlsClientHello = require("read-tls-client-hello");
|
|
8
|
+
const DEFAULT_JA4 = "ja4";
|
|
9
|
+
const getJA4 = async (headers, logger) => {
|
|
10
|
+
logger = logger || common.getLoggerDefault();
|
|
11
|
+
if (process.env.NODE_ENV === "development") {
|
|
12
|
+
return { ja4PlusFingerprint: DEFAULT_JA4 };
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
const xTlsClientHello = (headers["x-tls-clienthello"] || "").toString();
|
|
16
|
+
const xTlsVersion = (headers["x-tls-version"] || "").toString().toLowerCase();
|
|
17
|
+
const xTlsServerName = (headers["x-tls-server-name"] || "").toString();
|
|
18
|
+
const clientHelloBuffer = Buffer.from(xTlsClientHello, "base64");
|
|
19
|
+
logger.debug(
|
|
20
|
+
"ClientHello First Bytes:",
|
|
21
|
+
clientHelloBuffer.subarray(0, 5).toString("hex")
|
|
22
|
+
);
|
|
23
|
+
if (clientHelloBuffer[5] !== 1) {
|
|
24
|
+
logger.warn("Invalid ClientHello message: First byte is not 0x01");
|
|
25
|
+
return { ja4PlusFingerprint: DEFAULT_JA4 };
|
|
26
|
+
}
|
|
27
|
+
logger.debug("Headers TLS Version:", xTlsVersion);
|
|
28
|
+
const tlsVersion = xTlsVersion.replace(/(tls)|\./g, "");
|
|
29
|
+
const readableStream = new node_stream.Readable({
|
|
30
|
+
read() {
|
|
31
|
+
this.push(clientHelloBuffer);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
const clientHello = await readTlsClientHello.readTlsClientHello(readableStream);
|
|
35
|
+
const { alpnProtocols } = clientHello;
|
|
36
|
+
const [_tlsVersion, cipherSuites, extensions] = clientHello.fingerprintData;
|
|
37
|
+
const transport = "t";
|
|
38
|
+
const sniIndicator = xTlsServerName ? "d" : "i";
|
|
39
|
+
const validCipherSuites = cipherSuites.filter(
|
|
40
|
+
(cs) => (cs & 3855) !== 2570
|
|
41
|
+
);
|
|
42
|
+
const cipherCount = validCipherSuites.length;
|
|
43
|
+
const validExtensions = extensions.filter(
|
|
44
|
+
(ext) => (ext & 3855) !== 2570
|
|
45
|
+
);
|
|
46
|
+
const extensionCount = validExtensions.length;
|
|
47
|
+
const alpn = alpnProtocols?.length ? alpnProtocols[0] : "";
|
|
48
|
+
const alpnLabel = alpn ? `${alpn[0]}${alpn[alpn.length - 1]}` : "00";
|
|
49
|
+
const sortedCiphers = validCipherSuites.map((cs) => cs.toString(16).padStart(4, "0")).sort().join(",");
|
|
50
|
+
const cipherHash = node_crypto.createHash("sha256").update(sortedCiphers).digest("hex").slice(0, 12);
|
|
51
|
+
const decimalString = extensions.sort((a, b) => a - b).map((ext) => ext.toString(10)).join("-");
|
|
52
|
+
const extensionHash = node_crypto.createHash("sha256").update(decimalString).digest("hex").slice(0, 12);
|
|
53
|
+
const ja4PlusFingerprint = `${transport}${tlsVersion}${sniIndicator}${cipherCount}${extensionCount}${alpnLabel}_${cipherHash}_${extensionHash}`;
|
|
54
|
+
return { ja4PlusFingerprint };
|
|
55
|
+
} catch (e) {
|
|
56
|
+
logger.error("Error generating JA4+ fingerprint:", e);
|
|
57
|
+
return { ja4PlusFingerprint: DEFAULT_JA4 };
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const ja4Middleware = (env) => {
|
|
61
|
+
return async (req, res, next) => {
|
|
62
|
+
try {
|
|
63
|
+
const ja4 = await getJA4(req.headers, req.logger);
|
|
64
|
+
req.ja4 = ja4.ja4PlusFingerprint || "";
|
|
65
|
+
next();
|
|
66
|
+
} catch (err) {
|
|
67
|
+
return apiExpressRouter.handleErrors(err, req, res, next);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
exports.DEFAULT_JA4 = DEFAULT_JA4;
|
|
72
|
+
exports.getJA4 = getJA4;
|
|
73
|
+
exports.ja4Middleware = ja4Middleware;
|