Adjust for new recaptcha

master
Oleksii Sribnyi 2019-12-03 21:29:46 +01:00
parent 6478569537
commit 2dac52930b
4 changed files with 431 additions and 112 deletions

View File

@ -9,6 +9,7 @@
],
"rules": {
"semi": [2, "always"],
"no-trailing-spaces": [0],
"no-multi-spaces": [1, {
"exceptions": {
"VariableDeclarator": true

View File

@ -251,9 +251,23 @@ function onCloudflareResponse (options, response, body) {
onRequestComplete(options, response, body);
}
function detectRecaptchaVersion (body) {
// New version > Dec 2019
if (/__cf_chl_captcha_tk__=(.*)/i.test(body)) { // Test for ver2 first, as it also has ver2 fields
return 'ver2';
// Old version < Dec 2019
} else if (body.indexOf('why_captcha') !== -1 || /cdn-cgi\/l\/chk_captcha/i.test(body)) {
return 'ver1';
}
return false;
}
function validateResponse (options, response, body) {
// Finding captcha
if (body.indexOf('why_captcha') !== -1 || /cdn-cgi\/l\/chk_captcha/i.test(body)) {
// Old version < Dec 2019
let recaptchaVer = detectRecaptchaVersion(body);
if (recaptchaVer) {
// Convenience boolean
response.isCaptcha = true;
throw new CaptchaError('captcha', options, response);
@ -383,11 +397,13 @@ function onChallenge (options, response, body) {
// Parses the reCAPTCHA form and hands control over to the user
function onCaptcha (options, response, body) {
let recaptchaVer = detectRecaptchaVersion(body);
let isRecaptchaVer2 = recaptchaVer === 'ver2';
const callback = options.callback;
// UDF that has the responsibility of returning control back to cloudscraper
const handler = options.onCaptcha;
// The form data to send back to Cloudflare
const payload = { /* s, g-re-captcha-response */ };
const payload = { /* r|s, g-re-captcha-response */ };
let cause;
let match;
@ -399,7 +415,18 @@ function onCaptcha (options, response, body) {
}
const form = match[1];
let siteKey;
let rayId; // only for ver 2
if (isRecaptchaVer2) {
match = body.match(/\sdata-ray=["']?([^\s"'<>&]+)/);
if (!match) {
cause = 'Unable to find cloudflare ray id';
return callback(new ParserError(cause, options, response));
}
rayId = match[1];
}
match = body.match(/\sdata-sitekey=["']?([^\s"'<>&]+)/);
if (match) {
@ -434,9 +461,24 @@ function onCaptcha (options, response, body) {
response.captcha = {
siteKey,
uri: response.request.uri,
form: payload
form: payload,
version: recaptchaVer
};
if (isRecaptchaVer2) {
response.rayId = rayId;
match = body.match(/id="challenge-form" action="(.+?)" method="(.+?)"/);
if (!match) {
cause = 'Challenge form action and method extraction failed';
return callback(new ParserError(cause, options, response));
}
response.captcha.formMethod = match[2];
match = match[1].match(/\/(.*)/);
response.captcha.formActionUri = match[0];
payload.id = rayId;
}
Object.defineProperty(response.captcha, 'url', {
configurable: true,
enumerable: false,
@ -465,7 +507,7 @@ function onCaptcha (options, response, body) {
}
// Sanity check
if (!payload.s) {
if (!payload.s && !payload.r) {
cause = 'Challenge form is missing secret input';
return callback(new ParserError(cause, options, response));
}
@ -506,19 +548,34 @@ function onCaptcha (options, response, body) {
function onSubmitCaptcha (options, response) {
const callback = options.callback;
const uri = response.request.uri;
const isRecaptchaVer2 = response.captcha.version === 'ver2';
if (!response.captcha.form['g-recaptcha-response']) {
const cause = 'Form submission without g-recaptcha-response';
return callback(new CaptchaError(cause, options, response));
}
options.method = 'GET';
options.qs = response.captcha.form;
if (isRecaptchaVer2) {
options.qs = {
__cf_chl_captcha_tk__: response.captcha.formActionUri.match(/__cf_chl_captcha_tk__=(.*)/)[1]
};
options.form = response.captcha.form;
} else {
options.qs = response.captcha.form;
}
options.method = response.captcha.formMethod || 'GET';
// Prevent reusing the headers object to simplify unit testing.
options.headers = Object.assign({}, options.headers);
// Use the original uri as the referer and to construct the form action.
options.headers.Referer = uri.href;
options.uri = uri.protocol + '//' + uri.host + '/cdn-cgi/l/chk_captcha';
if (isRecaptchaVer2) {
options.uri = uri.protocol + '//' + uri.host + response.captcha.formActionUri;
} else {
options.uri = uri.protocol + '//' + uri.host + '/cdn-cgi/l/chk_captcha';
}
performRequest(options, false);
}

View File

@ -0,0 +1,148 @@
<!DOCTYPE html>
<!--[if lt IE 7]> <html class="no-js ie6 oldie" lang="en-US"> <![endif]-->
<!--[if IE 7]> <html class="no-js ie7 oldie" lang="en-US"> <![endif]-->
<!--[if IE 8]> <html class="no-js ie8 oldie" lang="en-US"> <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js" lang="en-US"> <!--<![endif]-->
<head>
<title>Attention Required! | Cloudflare</title>
<meta name="captcha-bypass" id="captcha-bypass" />
<meta charset="UTF-8" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1" />
<meta name="robots" content="noindex, nofollow" />
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1" />
<link rel="stylesheet" id="cf_styles-css" href="/cdn-cgi/styles/cf.errors.css" type="text/css" media="screen,projection" />
<!--[if lt IE 9]><link rel="stylesheet" id='cf_styles-ie-css' href="/cdn-cgi/styles/cf.errors.ie.css" type="text/css" media="screen,projection" /><![endif]-->
<style type="text/css">body{margin:0;padding:0}</style>
<!--[if gte IE 10]><!--><script type="text/javascript" src="/cdn-cgi/scripts/zepto.min.js"></script><!--<![endif]-->
<!--[if gte IE 10]><!--><script type="text/javascript" src="/cdn-cgi/scripts/cf.common.js"></script><!--<![endif]-->
</head>
<body>
<div id="cf-wrapper">
<div class="cf-alert cf-alert-error cf-cookie-error" id="cookie-alert" data-translate="enable_cookies">Please enable cookies.</div>
<div id="cf-error-details" class="cf-error-details-wrapper">
<div class="cf-wrapper cf-header cf-error-overview">
<h1 data-translate="challenge_headline">One more step</h1>
<h2 class="cf-subheadline"><span data-translate="complete_sec_check">Please complete the security check to access</span> www.cloudflare.com</h2>
</div><!-- /.header -->
<div class="cf-section cf-highlight cf-captcha-container">
<div class="cf-wrapper">
<div class="cf-columns two">
<div class="cf-column">
<div class="cf-highlight-inverse cf-form-stacked">
<form class="challenge-form" id="challenge-form" action="/?__cf_chl_captcha_tk__=e8844bdff35ae5e" method="POST" enctype="application/x-www-form-urlencoded">
<input type="hidden" name="r" value="0bd666f149acf02bbc05bba3b1bb">
<script type="text/javascript" src="/cdn-cgi/scripts/cf.challenge.js" data-type="normal" data-ray="53dfe8147d2a9e73" async data-sitekey="6LfBixYUAAAAABhdHynFUIMA_sa4s-XsJvnjtgB0"></script>
<div class="g-recaptcha"></div>
<noscript id="cf-captcha-bookmark" class="cf-captcha-info">
<div><div style="width: 302px">
<div>
<iframe src="https://www.google.com/recaptcha/api/fallback?k=6LfBixYUAAAAABhdHynFUIMA_sa4s-XsJvnjtgB0" frameborder="0" scrolling="no" style="width: 302px; height:422px; border-style: none;"></iframe>
</div>
<div style="width: 300px; border-style: none; bottom: 12px; left: 25px; margin: 0px; padding: 0px; right: 25px; background: #f9f9f9; border: 1px solid #c1c1c1; border-radius: 3px;">
<textarea id="g-recaptcha-response" name="g-recaptcha-response" class="g-recaptcha-response" style="width: 250px; height: 40px; border: 1px solid #c1c1c1; margin: 10px 25px; padding: 0px; resize: none;"></textarea>
<input type="submit" value="Submit"></input>
</div>
</div></div>
</noscript>
</form>
</div>
</div>
<div class="cf-column">
<div class="cf-screenshot-container">
<span class="cf-no-screenshot"></span>
</div>
</div>
</div><!-- /.columns -->
</div>
</div><!-- /.captcha-container -->
<div class="cf-section cf-wrapper">
<div class="cf-columns two">
<div class="cf-column">
<h2 data-translate="why_captcha_headline">Why do I have to complete a CAPTCHA?</h2>
<p data-translate="why_captcha_detail">Completing the CAPTCHA proves you are a human and gives you temporary access to the web property.</p>
</div>
<div class="cf-column">
<h2 data-translate="resolve_captcha_headline">What can I do to prevent this in the future?</h2>
<p data-translate="resolve_captcha_antivirus">If you are on a personal connection, like at home, you can run an anti-virus scan on your device to make sure it is not infected with malware.</p>
<p data-translate="resolve_captcha_network">If you are at an office or shared network, you can ask the network administrator to run a scan across the network looking for misconfigured or infected devices.</p>
<p data-translate="resolve_captcha_privacy_pass"> Another way to prevent getting this page in the future is to use Privacy Pass. You may need to download version 2.0 now from the <a href="https://chrome.google.com/webstore/detail/privacy-pass/ajhmfdgkijocedmfjonnpjfojldioehi">Chrome Web Store</a>.</p>
</div>
</div>
</div><!-- /.section -->
<div class="cf-error-footer cf-wrapper">
<p>
<span class="cf-footer-item">Cloudflare Ray ID: <strong>53dfe8147d2a9e73</strong></span>
<span class="cf-footer-separator">&bull;</span>
<span class="cf-footer-item"><span>Your IP</span>: 185.4.132.135</span>
<span class="cf-footer-separator">&bull;</span>
<span class="cf-footer-item"><span>Performance &amp; security by</span> <a href="https://www.cloudflare.com/5xx-error-landing?utm_source=error_footer" id="brand_link" target="_blank">Cloudflare</a></span>
</p>
</div><!-- /.error-footer -->
</div><!-- /#cf-error-details -->
</div><!-- /#cf-wrapper -->
<script type="text/javascript">
window._cf_translation = {};
</script>
<script src="https://ajax.cloudflare.com/cdn-cgi/scripts/f8ce4a63/cloudflare-static/pic-chl.js"></script>
<script type="text/javascript">
(function(){
var a = function() {try{return !!window.addEventListener} catch(e) {return !1} },
b = function(b, c) {a() ? document.addEventListener("DOMContentLoaded", b, c) : document.attachEvent("onreadystatechange", b)};
b(function(){
var f = document.getElementById('challenge-form');
if (f) {
var input = document.createElement('input');
input.type = 'hidden';
input.name = 'cv_chal_result';
input.value = window.__CF$cv$chal([0xf6a7adf9fb,0xa4950f3586]);
f.appendChild(input);
try {
if (window.__CF$cv$fp) {
var input = document.createElement('input');
input.type = 'hidden';
input.name = 'cv_chal_fp';
input.value = window.__CF$cv$fp();
f.appendChild(input);
}
} catch (e) { }
}
}, false);
})();
</script>
</body>
</html>

View File

@ -64,111 +64,224 @@ describe('Cloudscraper', function () {
expect(promise).to.be.rejectedWith(errors.CaptchaError).and.notify(done);
});
});
describe('reCAPTCHA (version as on 10.04.2019)', () => {
for (let stage = 0; stage < 4; stage++) {
const desc = {
0: 'should resolve when user calls captcha.submit()',
1: 'should callback with an error if user calls captcha.submit(error)',
2: 'should resolve when the onCaptcha promise resolves',
3: 'should callback with an error if the onCaptcha promise is rejected'
};
// Run this test 4 times
it(desc[stage], function (done) {
const secret = '6b132d85d185a8255f2451d48fe6a8bee7154ea2-1555377580-1800-AQ1azEkeDOnQP5ByOpwUU/RdbKrmMwHYpkaenRvjPXtB0w8Vbjn/Ceg62tfpp/lT799kjDLEMMuDkEMqQ7iO51kniWCQm00BQvDGl+D0h/WvXDWO96YXOUD3qrqUTuzO7QbUOinc8y8kedvOQkr4c0o=';
const siteKey = '6LfBixYUAAAAABhdHynFUIMA_sa4s-XsJvnjtgB0';
const expectedError = new Error('anti-captcha failed!');
helper.router
.get('/test', function (req, res) {
res.sendCaptcha('cf_recaptcha_15_04_2019.html');
})
.get('/cdn-cgi/l/chk_captcha', function (req, res) {
res.send(requestedPage);
});
const onCaptcha = sinon.spy(function (options, response, body) {
expect(options).to.be.an('object');
expect(response).to.be.instanceof(http.IncomingMessage);
expect(body).to.be.a('string');
sinon.assert.match(response, {
isCloudflare: true,
isHTML: true,
isCaptcha: true,
captcha: sinon.match.object
});
sinon.assert.match(response.captcha, {
url: uri, // <-- Deprecated
uri: sinon.match.same(response.request.uri),
form: { s: secret },
siteKey: siteKey,
submit: sinon.match.func
});
// Simulate what the user should do here
response.captcha.form['g-recaptcha-response'] = 'foobar';
switch (stage) {
case 0:
// User green lights form submission
response.captcha.submit();
break;
case 1:
// User reports an error when solving the reCAPTCHA
response.captcha.submit(expectedError);
break;
case 2:
// User green lights form submission by resolving the returned promise
return Promise.resolve();
case 3:
// User reports an error by rejecting the returned promise
return Promise.reject(expectedError);
}
});
const firstParams = helper.extendParams({ onCaptcha, uri });
const secondParams = helper.extendParams({
onCaptcha,
method: 'GET',
uri: helper.resolve('/cdn-cgi/l/chk_captcha'),
headers: {
Referer: uri
},
qs: {
s: secret,
'g-recaptcha-response': 'foobar'
}
});
const options = { onCaptcha, uri };
const promise = cloudscraper.get(options, function (error, response, body) {
switch (stage) {
case 0:
case 2:
expect(error).to.be.null;
expect(onCaptcha).to.be.calledOnce;
expect(Request).to.be.calledTwice;
expect(Request.firstCall).to.be.calledWithExactly(firstParams);
expect(Request.secondCall).to.be.calledWithExactly(secondParams);
expect(body).to.be.equal(requestedPage);
expect(promise).to.eventually.equal(requestedPage).and.notify(done);
break;
case 1:
case 3:
expect(error).to.be.instanceOf(errors.CaptchaError);
expect(error.error).to.be.an('error');
expect(error).to.have.property('errorType', 1);
expect(error.message).to.include(expectedError.message);
expect(promise).to.be.rejectedWith(errors.CaptchaError).and.notify(done);
break;
}
});
});
}
});
for (let stage = 0; stage < 4; stage++) {
const desc = {
0: 'should resolve reCAPTCHA (version as on 10.04.2019) when user calls captcha.submit()',
1: 'should callback with an error if user calls captcha.submit(error)',
2: 'should resolve reCAPTCHA (version as on 10.04.2019) when the onCaptcha promise resolves',
3: 'should callback with an error if the onCaptcha promise is rejected'
describe('reCAPTCHA (version as on 01.12.2019)', () => {
for (let stage = 0; stage < 4; stage++) {
const desc = {
0: 'should resolve when user calls captcha.submit()',
1: 'should callback with an error if user calls captcha.submit(error)',
2: 'should resolve when the onCaptcha promise resolves',
3: 'should callback with an error if the onCaptcha promise is rejected'
};
// Run this test 4 times
it(desc[stage], function (done) {
const secret = '0bd666f149acf02bbc05bba3b1bb';
const siteKey = '6LfBixYUAAAAABhdHynFUIMA_sa4s-XsJvnjtgB0';
const rayId = '53dfe8147d2a9e73';
const expectedError = new Error('anti-captcha failed!');
helper.router
.get('/test', function (req, res) {
res.sendCaptcha('cf_recaptcha_01_12_2019.html');
})
.post('/', function (req, res) {
res.send(requestedPage);
});
const onCaptcha = sinon.spy(function (options, response, body) {
expect(options).to.be.an('object');
expect(response).to.be.instanceof(http.IncomingMessage);
expect(body).to.be.a('string');
sinon.assert.match(response, {
isCloudflare: true,
isHTML: true,
isCaptcha: true,
captcha: sinon.match.object
});
sinon.assert.match(response.captcha, {
url: uri, // <-- Deprecated
uri: sinon.match.same(response.request.uri),
form: { r: secret, id: rayId },
siteKey: siteKey,
submit: sinon.match.func
});
// Simulate what the user should do here
response.captcha.form['g-recaptcha-response'] = 'foobar';
switch (stage) {
case 0:
// User green lights form submission
response.captcha.submit();
break;
case 1:
// User reports an error when solving the reCAPTCHA
response.captcha.submit(expectedError);
break;
case 2:
// User green lights form submission by resolving the returned promise
return Promise.resolve();
case 3:
// User reports an error by rejecting the returned promise
return Promise.reject(expectedError);
}
});
const firstParams = helper.extendParams({ onCaptcha, uri });
const secondParams = helper.extendParams({
onCaptcha,
method: 'POST',
uri: helper.resolve('/?__cf_chl_captcha_tk__=e8844bdff35ae5e'),
qs: { __cf_chl_captcha_tk__: 'e8844bdff35ae5e' },
headers: {
Referer: helper.resolve('/test')
},
form: {
r: secret,
id: rayId,
'g-recaptcha-response': 'foobar'
}
});
const options = { onCaptcha, uri };
const promise = cloudscraper.get(options, function (error, response, body) {
switch (stage) {
case 0:
case 2:
expect(error).to.be.null;
expect(onCaptcha).to.be.calledOnce;
expect(Request).to.be.calledTwice;
expect(Request.firstCall).to.be.calledWithExactly(firstParams);
expect(Request.secondCall).to.be.calledWithExactly(secondParams);
expect(body).to.be.equal(requestedPage);
expect(promise).to.eventually.equal(requestedPage).and.notify(done);
break;
case 1:
case 3:
expect(error).to.be.instanceOf(errors.CaptchaError);
expect(error.error).to.be.an('error');
expect(error).to.have.property('errorType', 1);
expect(error.message).to.include(expectedError.message);
expect(promise).to.be.rejectedWith(errors.CaptchaError).and.notify(done);
break;
}
});
});
};
// Run this test 4 times
it(desc[stage], function (done) {
const secret = '6b132d85d185a8255f2451d48fe6a8bee7154ea2-1555377580-1800-AQ1azEkeDOnQP5ByOpwUU/RdbKrmMwHYpkaenRvjPXtB0w8Vbjn/Ceg62tfpp/lT799kjDLEMMuDkEMqQ7iO51kniWCQm00BQvDGl+D0h/WvXDWO96YXOUD3qrqUTuzO7QbUOinc8y8kedvOQkr4c0o=';
const siteKey = '6LfBixYUAAAAABhdHynFUIMA_sa4s-XsJvnjtgB0';
const expectedError = new Error('anti-captcha failed!');
helper.router
.get('/test', function (req, res) {
res.sendCaptcha('cf_recaptcha_15_04_2019.html');
})
.get('/cdn-cgi/l/chk_captcha', function (req, res) {
res.send(requestedPage);
});
const onCaptcha = sinon.spy(function (options, response, body) {
expect(options).to.be.an('object');
expect(response).to.be.instanceof(http.IncomingMessage);
expect(body).to.be.a('string');
sinon.assert.match(response, {
isCloudflare: true,
isHTML: true,
isCaptcha: true,
captcha: sinon.match.object
});
sinon.assert.match(response.captcha, {
url: uri, // <-- Deprecated
uri: sinon.match.same(response.request.uri),
form: { s: secret },
siteKey: siteKey,
submit: sinon.match.func
});
// Simulate what the user should do here
response.captcha.form['g-recaptcha-response'] = 'foobar';
switch (stage) {
case 0:
// User green lights form submission
response.captcha.submit();
break;
case 1:
// User reports an error when solving the reCAPTCHA
response.captcha.submit(expectedError);
break;
case 2:
// User green lights form submission by resolving the returned promise
return Promise.resolve();
case 3:
// User reports an error by rejecting the returned promise
return Promise.reject(expectedError);
}
});
const firstParams = helper.extendParams({ onCaptcha, uri });
const secondParams = helper.extendParams({
onCaptcha,
method: 'GET',
uri: helper.resolve('/cdn-cgi/l/chk_captcha'),
headers: {
Referer: uri
},
qs: {
s: secret,
'g-recaptcha-response': 'foobar'
}
});
const options = { onCaptcha, uri };
const promise = cloudscraper.get(options, function (error, response, body) {
switch (stage) {
case 0:
case 2:
expect(error).to.be.null;
expect(onCaptcha).to.be.calledOnce;
expect(Request).to.be.calledTwice;
expect(Request.firstCall).to.be.calledWithExactly(firstParams);
expect(Request.secondCall).to.be.calledWithExactly(secondParams);
expect(body).to.be.equal(requestedPage);
expect(promise).to.eventually.equal(requestedPage).and.notify(done);
break;
case 1:
case 3:
expect(error).to.be.instanceOf(errors.CaptchaError);
expect(error.error).to.be.an('error');
expect(error).to.have.property('errorType', 1);
expect(error.message).to.include(expectedError.message);
expect(promise).to.be.rejectedWith(errors.CaptchaError).and.notify(done);
break;
}
});
});
}
});
});