Skip to content

Commit eed4ba4

Browse files
committed
feat(pir): captcha providers
1 parent 931740f commit eed4ba4

35 files changed

+788
-24
lines changed

injected/integration-test/test-pages/broker-protection/actions/get-captcha.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"state": {
33
"action": {
44
"actionType": "getCaptchaInfo",
5+
"captchaType": "recaptcha2",
56
"selector": ".g-recaptcha",
67
"id": "8324"
78
}

injected/integration-test/test-pages/broker-protection/actions/solve-captcha.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"state": {
33
"action": {
44
"actionType": "solveCaptcha",
5+
"captchaType": "recaptcha2",
56
"id": "83241"
67
},
78
"data": {

injected/src/features/broker-protection/actions/captcha.js renamed to injected/src/features/broker-protection/actions/captcha-deprecated.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
import { captchaCallback } from './captcha-callback.js';
2-
import { getElement } from '../utils.js';
2+
import { getElement } from '../utils';
33
import { ErrorResponse, SuccessResponse } from '../types.js';
44

55
/**
66
* Gets the captcha information to send to the backend
77
*
8-
* @param action
8+
* @param {import('../types.js').PirAction} action
99
* @param {Document | HTMLElement} root
1010
* @return {import('../types.js').ActionResponse}
1111
*/
1212
export function getCaptchaInfo(action, root = document) {
1313
const pageUrl = window.location.href;
14+
if (!action.selector) {
15+
return new ErrorResponse({ actionID: action.id, message: 'missing selector' });
16+
}
17+
1418
const captchaDiv = getElement(root, action.selector);
1519

1620
// if 'captchaDiv' was missing, cannot continue

injected/src/features/broker-protection/actions/click.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { getElements } from '../utils.js';
1+
import { getElements } from '../utils';
22
import { ErrorResponse, SuccessResponse } from '../types.js';
33
import { extractProfiles } from './extract.js';
44
import { processTemplateStringWithUserData } from './build-url-transforms.js';

injected/src/features/broker-protection/actions/expectation.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { getElement } from '../utils.js';
1+
import { getElement } from '../utils';
22
import { ErrorResponse, SuccessResponse } from '../types.js';
33

44
/**

injected/src/features/broker-protection/actions/extract.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { cleanArray, getElement, getElementMatches, getElements, sortAddressesByStateAndCity } from '../utils.js'; // Assuming you have imported the address comparison function
1+
import { cleanArray, getElement, getElementMatches, getElements, sortAddressesByStateAndCity } from '../utils'; // Assuming you have imported the address comparison function
22
import { ErrorResponse, ProfileResult, SuccessResponse } from '../types.js';
33
import { isSameAge } from '../comparisons/is-same-age.js';
44
import { isSameName } from '../comparisons/is-same-name.js';

injected/src/features/broker-protection/actions/fill-form.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { getElement, generateRandomInt } from '../utils.js';
1+
import { getElement, generateRandomInt } from '../utils';
22
import { ErrorResponse, SuccessResponse } from '../types.js';
33
import { generatePhoneNumber, generateZipCode, generateStreetAddress } from './generators.js';
44

injected/src/features/broker-protection/actions/generators.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { generateRandomInt } from '../utils.js';
1+
import { generateRandomInt } from '../utils';
22

33
export function generatePhoneNumber() {
44
/**
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { extract } from './extract.js';
2+
import { fillForm } from './fill-form.js';
3+
import { click } from './click.js';
4+
import { expectation } from './expectation.js';
5+
import { navigate } from './navigate';
6+
import { buildUrl } from './build-url';
7+
import * as captchaHandlers from '../captcha-services';
8+
import * as deprecatedCaptchaHandlers from './captcha-deprecated';
9+
10+
/**
11+
* Returns the captcha handlers based on the useNewActionHandlers flag
12+
* @param {Object} params
13+
* @param {boolean} params.useNewActionHandlers
14+
*/
15+
export const resolveActionHandlers = ({ useNewActionHandlers }) => {
16+
return {
17+
extract,
18+
fillForm,
19+
click,
20+
expectation,
21+
...(useNewActionHandlers
22+
? {
23+
navigate,
24+
...captchaHandlers,
25+
}
26+
: {
27+
navigate: buildUrl,
28+
...deprecatedCaptchaHandlers,
29+
}),
30+
};
31+
};
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { getSupportingCodeToInject } from '../captcha-services';
2+
import { ErrorResponse, SuccessResponse } from '../types';
3+
import { buildUrl } from './build-url';
4+
5+
/**
6+
* This builds the proper URL given the URL template and userData.
7+
* Also, if the action requires a captcha handler, it will inject the necessary code.
8+
*
9+
* @param {import('../types.js').PirAction} action
10+
* @param {Record<string, any>} userData
11+
* @return {import('../types.js').ActionResponse}
12+
*/
13+
export const navigate = (action, userData) => {
14+
const { id: actionID, actionType, injectCaptchaHandler } = action;
15+
try {
16+
const urlResult = buildUrl(action, userData);
17+
if (urlResult instanceof ErrorResponse) {
18+
return urlResult;
19+
}
20+
21+
const codeToInject = injectCaptchaHandler ? getSupportingCodeToInject(injectCaptchaHandler) : null;
22+
const response = {
23+
...urlResult.success.response,
24+
...(codeToInject && { code: codeToInject }),
25+
};
26+
27+
return new SuccessResponse({ actionID, actionType, response });
28+
} catch (e) {
29+
return new ErrorResponse({ actionID, message: `[navigate] ${e.message}` });
30+
}
31+
};
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { getElement, removeUrlQueryParams } from '../utils';
2+
import { ErrorResponse, SuccessResponse } from '../types';
3+
import { getCaptchaProvider, getCaptchaSolveProvider } from './get-captcha-provider';
4+
import { captchaFactory } from './providers';
5+
6+
/**
7+
* Returns the supporting code to inject for the given captcha type
8+
*
9+
* @param {import('../types.js').PirAction['captchaType']} captchaType
10+
* @return string
11+
* @throws {Error}
12+
*/
13+
export const getSupportingCodeToInject = (captchaType) => {
14+
const captchaProvider = captchaFactory.getProviderByType(captchaType);
15+
if (!captchaProvider) {
16+
throw new Error(`[injectCaptchaHandler] could not find captchaProvider with type ${captchaType}`);
17+
}
18+
19+
return captchaProvider.getSupportingCodeToInject();
20+
};
21+
22+
/**
23+
* Gets the captcha information to send to the backend
24+
*
25+
* @param {import('../types.js').PirAction} action
26+
* @param {Document | HTMLElement} root
27+
* @return {import('../types.js').ActionResponse}
28+
*/
29+
export const getCaptchaInfo = (action, root = document) => {
30+
const { id: actionID, selector, actionType, captchaType } = action;
31+
try {
32+
if (!selector) {
33+
throw new Error('missing selector');
34+
}
35+
36+
const captchaContainer = getElement(root, selector);
37+
if (!captchaContainer) {
38+
throw new Error(`could not find captchaContainer with selector ${selector}`);
39+
}
40+
41+
const captchaProvider = getCaptchaProvider(captchaContainer, captchaType);
42+
const captchaIdentifier = captchaProvider.getCaptchaIdentifier(captchaContainer);
43+
if (!captchaIdentifier) {
44+
throw new Error(`could not extract captcha identifier from ${captchaType} captcha`);
45+
}
46+
47+
const response = {
48+
url: removeUrlQueryParams(window.location.href), // query params (which may include PII)
49+
siteKey: captchaIdentifier,
50+
type: captchaProvider.getType(),
51+
};
52+
return new SuccessResponse({ actionID, actionType, response });
53+
} catch (e) {
54+
return new ErrorResponse({ actionID, message: `[getCaptchaInfo] ${e.message}` });
55+
}
56+
};
57+
58+
/**
59+
* Takes the solved captcha token and injects it into the page to solve the captcha
60+
*
61+
* @param {import('../types.js').PirAction} action
62+
* @param {string} token
63+
* @param {Document} root
64+
* @return {import('../types.js').ActionResponse}
65+
*/
66+
export const solveCaptcha = (action, token, root = document) => {
67+
const { id: actionID, actionType, captchaType } = action;
68+
try {
69+
if (!captchaType) {
70+
throw new Error('missing captchaType');
71+
}
72+
73+
const captchaSolveProvider = getCaptchaSolveProvider(root, captchaType);
74+
if (!captchaSolveProvider.canSolve(root)) {
75+
throw new Error(`[solveCaptcha] cannot solve captcha with type ${captchaType}`);
76+
}
77+
78+
captchaSolveProvider.injectToken(root, token);
79+
return new SuccessResponse({
80+
actionID,
81+
actionType,
82+
response: { callback: { eval: captchaSolveProvider.getSolveCallback(token) } },
83+
});
84+
} catch (e) {
85+
return new ErrorResponse({ actionID, message: `[solveCaptcha] ${e.message}` });
86+
}
87+
};
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* Factory for captcha providers
3+
*/
4+
export class CaptchaFactory {
5+
constructor() {
6+
this.providers = new Map();
7+
}
8+
9+
/**
10+
* Register a captcha provider
11+
* @param {typeof import('./providers/provider.interface').CaptchaProvider} ProviderClass
12+
*/
13+
registerProvider(ProviderClass) {
14+
const provider = new ProviderClass();
15+
this.providers.set(provider.getType(), provider);
16+
}
17+
18+
/**
19+
* Get a provider by type
20+
* @param {string} type - The provider type
21+
* @returns {import('./providers/provider.interface').CaptchaProvider|null}
22+
*/
23+
getProviderByType(type) {
24+
return this.providers.get(type) || null;
25+
}
26+
27+
/**
28+
* Detect the captcha provider based on the element
29+
* @param {HTMLElement} element - The element to check
30+
* @returns {import('./providers/provider.interface').CaptchaProvider|null}
31+
*/
32+
detectProvider(element) {
33+
return this._getAllProviders().find((provider) => provider.isSupportedForElement(element)) || null;
34+
}
35+
36+
detectSolveProvider(root) {
37+
return this._getAllProviders().find((provider) => provider.canSolve(root)) || null;
38+
}
39+
40+
/**
41+
* Get all registered providers
42+
* @private
43+
* @returns {Array<import('./providers/provider.interface').CaptchaProvider>}
44+
*/
45+
_getAllProviders() {
46+
return Array.from(this.providers.values());
47+
}
48+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { captchaFactory } from './providers';
2+
3+
/**
4+
* Gets the captcha provider for the getCaptchaInfo action
5+
*
6+
* @param {HTMLElement} captchaDiv
7+
* @param {string} captchaType
8+
* @return {import('./providers/provider.interface.js').CaptchaProvider}
9+
* @throws {Error}
10+
*/
11+
export const getCaptchaProvider = (captchaDiv, captchaType) => {
12+
const captchaProvider = captchaFactory.getProviderByType(captchaType);
13+
if (!captchaProvider) {
14+
throw new Error(`[getCaptchaProvider] could not find captchaProvider with type ${captchaType}`);
15+
}
16+
17+
if (captchaProvider.isSupportedForElement(captchaDiv)) {
18+
return captchaProvider;
19+
}
20+
21+
// TODO fire a pixel
22+
// if the captcha provider type is different from the expected type, log a warning
23+
console.warn(
24+
`[getCaptchaProvider] mismatch between expected captha type ${captchaType} and detected type ${captchaProvider.getType()}`,
25+
);
26+
const detectedProvider = captchaFactory.detectProvider(captchaDiv);
27+
if (!detectedProvider) {
28+
throw new Error(`[getCaptchaProvider] could not detect captcha provider for ${captchaType} captcha and element ${captchaDiv}`);
29+
}
30+
31+
return detectedProvider;
32+
};
33+
34+
/**
35+
* Gets the captcha provider for the solveCaptcha action
36+
* @param {Document} root
37+
* @param {string} captchaType
38+
* @return {import('./providers/provider.interface.js').CaptchaProvider}
39+
* @throws {Error}
40+
*/
41+
export const getCaptchaSolveProvider = (root, captchaType) => {
42+
const captchaProvider = captchaFactory.getProviderByType(captchaType);
43+
if (!captchaProvider) {
44+
throw new Error(`[getCaptchaSolveProvider] could not find captchaProvider with type ${captchaType}`);
45+
}
46+
47+
if (captchaProvider.canSolve(root)) {
48+
return captchaProvider;
49+
}
50+
51+
// TODO fire a pixel
52+
// if the captcha provider type is different from the expected type, log a warning
53+
console.warn(
54+
`[getCaptchaSolveProvider] mismatch between expected captha type ${captchaType} and detected type ${captchaProvider.getType()}`,
55+
);
56+
const detectedProvider = captchaFactory.detectSolveProvider(root);
57+
if (!detectedProvider) {
58+
throw new Error(`[getCaptchaSolveProvider] could not detect captcha provider for ${captchaType} captcha and element ${root}`);
59+
}
60+
61+
return detectedProvider;
62+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { getSupportingCodeToInject, getCaptchaInfo, solveCaptcha } from './captcha.service';
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { CaptchaProvider } from './provider.interface';
2+
3+
export class CloudFlareTurnstileProvider extends CaptchaProvider {
4+
getType() {
5+
return 'cloudflareTurnstile';
6+
}
7+
8+
/**
9+
* @param {HTMLElement} captchaContainerElement
10+
*/
11+
isSupportedForElement(captchaContainerElement) {
12+
// TODO: Implement
13+
return false;
14+
}
15+
16+
/**
17+
* @param {HTMLElement} captchaContainerElement - The element containing the captcha
18+
*/
19+
getCaptchaIdentifier(captchaContainerElement) {
20+
// TODO: Implement
21+
return null;
22+
}
23+
24+
getSupportingCodeToInject() {
25+
// TODO: Implement
26+
return null;
27+
}
28+
29+
/**
30+
* @param {Document} root - The document root to search for captchas
31+
*/
32+
canSolve(root) {
33+
// TODO: Implement
34+
return false;
35+
}
36+
37+
/**
38+
* @param {Document} root - The document root containing the captcha
39+
* @param {string} token - The solved captcha token
40+
*/
41+
injectToken(root, token) {
42+
// TODO: Implement
43+
return false;
44+
}
45+
46+
/**
47+
* @param {string} token - The solved captcha token
48+
*/
49+
getSolveCallback(token) {
50+
return null;
51+
}
52+
}

0 commit comments

Comments
 (0)