Add multiple captcha options for form submission

-Supported captchas include Google reCaptcha v3 (score-based), Google reCaptcha v2 (checkbox and invisible) and hCaptcha ("Always Challenge" mode)
-Captcha options are global to the explorer even though the only form submission page is the "Claim Address" feature which takes full advantage of the new captcha options
This commit is contained in:
Joe Uhren
2024-03-20 18:20:03 -06:00
parent 5d960ceea7
commit cf9dce3449
5 changed files with 345 additions and 67 deletions
+125 -32
View File
@@ -97,44 +97,136 @@ app.use('/', routes);
// post method to claim an address using verifymessage functionality
app.post('/claim', function(req, res) {
// check if the bad-words filter is enabled
if (settings.claim_address_page.enable_bad_word_filter == true) {
// initialize the bad-words filter
var bad_word_lib = require('bad-words');
var bad_word_filter = new bad_word_lib();
// validate captcha if applicable
validate_captcha(settings.claim_address_page.enable_captcha, req.body, function(captcha_error) {
// check if there was a problem with captcha
if (captcha_error) {
// show the captcha error
res.json({'status': 'failed', 'error': true, 'message': 'The captcha validation failed'});
} else {
// check if the bad-words filter is enabled
if (settings.claim_address_page.enable_bad_word_filter == true) {
// initialize the bad-words filter
var bad_word_lib = require('bad-words');
var bad_word_filter = new bad_word_lib();
// clean the message (Display name) of bad words
var message = (req.body.message == null || req.body.message == '' ? '' : bad_word_filter.clean(req.body.message));
} else {
// Do not use the bad word filter
var message = (req.body.message == null || req.body.message == '' ? '' : req.body.message);
}
// clean the message (Display name) of bad words
var message = (req.body.message == null || req.body.message == '' ? '' : bad_word_filter.clean(req.body.message));
} else {
// do not use the bad word filter
var message = (req.body.message == null || req.body.message == '' ? '' : req.body.message);
}
// check if the message was filtered
if (message == req.body.message) {
// call the verifymessage api
lib.verify_message(req.body.address, req.body.signature, req.body.message, function(body) {
if (body == false)
res.json({'status': 'failed', 'error': true, 'message': 'Invalid signature'});
else if (body == true) {
db.update_claim_name(req.body.address, req.body.message, function(val) {
// check if the update was successful
if (val == '')
res.json({'status': 'success'});
else if (val == 'no_address')
res.json({'status': 'failed', 'error': true, 'message': 'Wallet address ' + req.body.address + ' is not valid or does not have any transactions'});
else
// check if the message was filtered
if (message == req.body.message) {
// call the verifymessage api
lib.verify_message(req.body.address, req.body.signature, req.body.message, function(body) {
if (body == false)
res.json({'status': 'failed', 'error': true, 'message': 'Invalid signature'});
else if (body == true) {
db.update_claim_name(req.body.address, req.body.message, function(val) {
// check if the update was successful
if (val == '')
res.json({'status': 'success'});
else if (val == 'no_address')
res.json({'status': 'failed', 'error': true, 'message': 'Wallet address ' + req.body.address + ' is not valid or does not have any transactions'});
else
res.json({'status': 'failed', 'error': true, 'message': 'Wallet address or signature is invalid'});
});
} else
res.json({'status': 'failed', 'error': true, 'message': 'Wallet address or signature is invalid'});
});
} else
res.json({'status': 'failed', 'error': true, 'message': 'Wallet address or signature is invalid'});
});
} else {
// message was filtered which would change the signature
res.json({'status': 'failed', 'error': true, 'message': 'Display name contains bad words and cannot be saved: ' + message});
}
} else {
// message was filtered which would change the signature
res.json({'status': 'failed', 'error': true, 'message': 'Display name contains bad words and cannot be saved: ' + message});
}
}
});
});
function validate_captcha(captcha_enabled, data, cb) {
// check if captcha is enabled for the requested feature
if (captcha_enabled == true) {
// determine the captcha type
if (settings.captcha.google_recaptcha3.enabled == true) {
if (data.google_recaptcha3 != null) {
const request = require('postman-request');
request({uri: 'https://www.google.com/recaptcha/api/siteverify?secret=' + settings.captcha.google_recaptcha3.secret_key + '&response=' + data.google_recaptcha3, json: true}, function (error, response, body) {
if (error) {
// an error occurred while trying to validate the captcha
return cb(true);
} else if (body == null || body == '' || typeof body !== 'object') {
// return data is invalid
return cb(true);
} else if (body.score == null || body.score < settings.captcha.google_recaptcha3.pass_score) {
// captcha challenge failed
return cb(true);
} else {
// captcha challenge passed
return cb(false);
}
});
} else {
// a captcha response wasn't received
return cb(true);
}
} else if (settings.captcha.google_recaptcha2.enabled == true) {
if (data.google_recaptcha2 != null) {
const request = require('postman-request');
request({uri: 'https://www.google.com/recaptcha/api/siteverify?secret=' + settings.captcha.google_recaptcha2.secret_key + '&response=' + data.google_recaptcha2, json: true}, function (error, response, body) {
if (error) {
// an error occurred while trying to validate the captcha
return cb(true);
} else if (body == null || body == '' || typeof body !== 'object') {
// return data is invalid
return cb(true);
} else if (body.success == null || body.success == false) {
// captcha challenge failed
return cb(true);
} else {
// captcha challenge passed
return cb(false);
}
});
} else {
// a captcha response wasn't received
return cb(true);
}
} else if (settings.captcha.hcaptcha.enabled == true) {
if (data.hcaptcha != null) {
const request = require('postman-request');
request({uri: 'https://hcaptcha.com/siteverify?secret=' + settings.captcha.hcaptcha.secret_key + '&response=' + data.hcaptcha, json: true}, function (error, response, body) {
if (error) {
// an error occurred while trying to validate the captcha
return cb(true);
} else if (body == null || body == '' || typeof body !== 'object') {
// return data is invalid
return cb(true);
} else if (body.success == null || body.success == false) {
// captcha challenge failed
return cb(true);
} else {
// captcha challenge passed
return cb(false);
}
});
} else {
// a captcha response wasn't received
return cb(true);
}
} else {
// no captcha options are enabled
return cb(false);
}
} else {
// captcha is not enabled for this feature
return cb(false);
}
}
// extended apis
app.use('/ext/getmoneysupply', function(req, res) {
// check if the getmoneysupply api is enabled
@@ -859,6 +951,7 @@ app.set('markets_page', settings.markets_page);
app.set('api_page', settings.api_page);
app.set('claim_address_page', settings.claim_address_page);
app.set('orphans_page', settings.orphans_page);
app.set('captcha', settings.captcha);
app.set('labels', settings.labels);
app.set('default_coingecko_ids', settings.default_coingecko_ids);
app.set('api_cmds', settings.api_cmds);
+49 -1
View File
@@ -1212,7 +1212,10 @@ exports.claim_address_page = {
"show_header_menu": true,
// enable_bad_word_filter: Enable/disable the "bad word" filter for claimed addresses, so that trying to claim an address with a bad word like "ash0le" will fail
// This feature uses the default blacklist from the "bad-words" plugin from here: https://www.npmjs.com/package/bad-words
"enable_bad_word_filter": true
"enable_bad_word_filter": true,
// enable_captcha: Enable/disable using captcha security when filling out and submitting the claim address form
// NOTE: you must also configure and enable one of the options in the main "captcha" settings for this option to function correctly
"enable_captcha": false
};
// orphans_page: a collection of settings that pertain to the orphans page
@@ -1273,6 +1276,51 @@ exports.sync = {
"supply": "GETINFO"
};
// captcha: a collection of settings that pertain to the captcha security used by different elements of the explorer
// NOTE: only 1 captcha option can be enabled and used at any given time. If you enable 2 or more options the explorer will use the first available enabled option it finds
exports.captcha = {
// google_recaptcha3: a collection of settings that pertain to using Google reCAPTCHA v3. Signup to get your site and secret keys here: https://www.google.com/recaptcha/admin
"google_recaptcha3": {
// enabled: Enable/disable the use of Google reCAPTCHA v3 (true/false)
// If set to false, Google reCAPTCHA v3 will be completely disabled as a security feature
"enabled": false,
// pass_score: A numeric score between 0.0 and 1.0 used to deteremine if a particular captcha request passed or failed
// Google reCAPTCHA v3 returns a score value for every captcha request. 1.0 is very likely a good interaction whereas 0.0 is very likely a bot
// Google recommends using 0.5 by default but you may increase the passing score if you are receiving too many automated bot submissions or lower the score if legitimate users are having troubles passing the captcha challenge
"pass_score": 0.5,
// site_key: Enter the SITE KEY value from your Google reCAPTCHA v3 settings here
"site_key": "",
// secret_key: Enter the SECRET KEY value from your Google reCAPTCHA v3 settings here
"secret_key": ""
},
// google_recaptcha2: a collection of settings that pertain to using Google reCAPTCHA v2. Signup to get your site and secret keys here: https://www.google.com/recaptcha/admin
"google_recaptcha2": {
// enabled: Enable/disable the use of Google reCAPTCHA v2 (true/false)
// If set to false, Google reCAPTCHA v2 will be completely disabled as a security feature
"enabled": false,
// captcha_type: Determine the type of captcha to use for security validation
// Valid options:
// checkbox: The "I'm not a robot" Checkbox requires the user to click a checkbox indicating the user is not a robot. This will either pass the user immediately (with No CAPTCHA) or challenge them to validate whether or not they are human
// invisible: The invisible reCAPTCHA badge does not require the user to click on a checkbox. By default only the most suspicious traffic will be prompted to solve a captcha
"captcha_type": "checkbox",
// site_key: Enter the SITE KEY value from your Google reCAPTCHA v2 settings here
"site_key": "",
// secret_key: Enter the SECRET KEY value from your Google reCAPTCHA v2 settings here
"secret_key": ""
},
// hcaptcha: a collection of settings that pertain to using hCaptcha. Signup to get your site and secret keys here: https://dashboard.hcaptcha.com/signup
// NOTE: Only the free "Always Challenge" mode is currently supported
"hcaptcha": {
// enabled: Enable/disable the use of hCaptcha (true/false)
// If set to false, hCaptcha will be completely disabled as a security feature
"enabled": false,
// site_key: Enter the SITE KEY value from your hCaptcha settings here
"site_key": "",
// secret_key: Enter the SECRET KEY value from your hCaptcha settings here
"secret_key": ""
}
};
// labels: a collection of settings that pertain to the list of customized wallet address labels
// Adding entries to this section will display a custom label beside each affected wallet address when displayed in the explorer
// NOTE: You can add as many address labels as desired
+49 -1
View File
@@ -1296,7 +1296,10 @@
"show_header_menu": true,
// enable_bad_word_filter: Enable/disable the "bad word" filter for claimed addresses, so that trying to claim an address with a bad word like "ash0le" will fail
// This feature uses the default blacklist from the "bad-words" plugin from here: https://www.npmjs.com/package/bad-words
"enable_bad_word_filter": true
"enable_bad_word_filter": true,
// enable_captcha: Enable/disable using captcha security when filling out and submitting the claim address form
// NOTE: you must also configure and enable one of the options in the main "captcha" settings for this option to function correctly
"enable_captcha": true
},
// orphans_page: a collection of settings that pertain to the orphans page
@@ -1357,6 +1360,51 @@
"supply": "GETINFO"
},
// captcha: a collection of settings that pertain to the captcha security used by different elements of the explorer
// NOTE: only 1 captcha option can be enabled and used at any given time. If you enable 2 or more options the explorer will use the first available enabled option it finds
"captcha": {
// google_recaptcha3: a collection of settings that pertain to using Google reCAPTCHA v3. Signup to get your site and secret keys here: https://www.google.com/recaptcha/admin
"google_recaptcha3": {
// enabled: Enable/disable the use of Google reCAPTCHA v3 (true/false)
// If set to false, Google reCAPTCHA v3 will be completely disabled as a security feature
"enabled": false,
// pass_score: A numeric score between 0.0 and 1.0 used to deteremine if a particular captcha request passed or failed
// Google reCAPTCHA v3 returns a score value for every captcha request. 1.0 is very likely a good interaction whereas 0.0 is very likely a bot
// Google recommends using 0.5 by default but you may increase the passing score if you are receiving too many automated bot submissions or lower the score if legitimate users are having troubles passing the captcha challenge
"pass_score": 0.5,
// site_key: Enter the SITE KEY value from your Google reCAPTCHA v3 settings here
"site_key": "",
// secret_key: Enter the SECRET KEY value from your Google reCAPTCHA v3 settings here
"secret_key": ""
},
// google_recaptcha2: a collection of settings that pertain to using Google reCAPTCHA v2. Signup to get your site and secret keys here: https://www.google.com/recaptcha/admin
"google_recaptcha2": {
// enabled: Enable/disable the use of Google reCAPTCHA v2 (true/false)
// If set to false, Google reCAPTCHA v2 will be completely disabled as a security feature
"enabled": false,
// captcha_type: Determine the type of captcha to use for security validation
// Valid options:
// checkbox: The "I'm not a robot" Checkbox requires the user to click a checkbox indicating the user is not a robot. This will either pass the user immediately (with No CAPTCHA) or challenge them to validate whether or not they are human
// invisible: The invisible reCAPTCHA badge does not require the user to click on a checkbox. By default only the most suspicious traffic will be prompted to solve a captcha
"captcha_type": "checkbox",
// site_key: Enter the SITE KEY value from your Google reCAPTCHA v2 settings here
"site_key": "",
// secret_key: Enter the SECRET KEY value from your Google reCAPTCHA v2 settings here
"secret_key": ""
},
// hcaptcha: a collection of settings that pertain to using hCaptcha. Signup to get your site and secret keys here: https://dashboard.hcaptcha.com/signup
// NOTE: Only the free "Always Challenge" mode is currently supported
"hcaptcha": {
// enabled: Enable/disable the use of hCaptcha (true/false)
// If set to false, hCaptcha will be completely disabled as a security feature
"enabled": false,
// site_key: Enter the SITE KEY value from your hCaptcha settings here
"site_key": "",
// secret_key: Enter the SECRET KEY value from your hCaptcha settings here
"secret_key": ""
}
},
// labels: a collection of settings that pertain to the list of customized wallet address labels
// Adding entries to this section will display a custom label beside each affected wallet address when displayed in the explorer
// NOTE: You can add as many address labels as desired
+107 -33
View File
@@ -1,27 +1,15 @@
extends layout
block content
- var selected_captcha_object = null
- var selected_captcha_name = ''
if settings.claim_address_page.enable_captcha == true
each captcha, name in settings.captcha
if selected_captcha_object == null && captcha != null && captcha.enabled == true
- selected_captcha_object = captcha
- selected_captcha_name = name
script.
$(document).ready(function () {
function generateAlertHTML(alertClass, allowDismiss, headerText, msgText) {
return '<div class="alert alert-' + alertClass + (allowDismiss ? ' alert-dismissible fade show' : '') + '" role="alert">' +
(allowDismiss ? '<button type="button" class="btn-close" data-bs-dismiss="alert"></button>' : '') +
'<div' + (msgText == null || msgText == '' ? '' : ' class="cardSpacer"') + '>' +
'<span class="fa-solid ' + (alertClass == 'success' ? 'fa-circle-check' : (alertClass == 'danger' ? 'fa-circle-exclamation' : (alertClass == 'info' ? 'fa-circle-info' : 'fa-triangle-exclamation'))) + '" style="margin-right:5px"></span>' +
'<strong>' + headerText + '</strong>' +
'</div>' +
(msgText == null || msgText == '' ? '' : '<span>' + msgText + '</span>') +
'</div>';
}
function displayAsText(str) {
return str.replace(/</g, '&#60;').replace(/>/g, '&#62;');
}
function showClaimAlert(claimClass, warnMsg, removedClaim) {
if ($('#claimAlert').length == 0)
$('<div id="claimAlert"></div>').insertBefore('#claimForm');
$('#claimAlert').html(generateAlertHTML(claimClass, true, (claimClass == 'success' ? (removedClaim ? 'Address claim removed successfully' : 'Address claimed successfully') : (claimClass == 'danger' ? 'Failed to claim address' : 'Required field missing')), (claimClass == 'success' ? '<strong>' + $('input#address').val() + '</strong> will now be referred to as <strong>' + (removedClaim ? $('input#address').val() : displayAsText($('#message').val())) + '</strong> throughout the website' : warnMsg) + '.'));
fixFooterHeightAndPosition();
}
$('#claimInstructions').on('show.bs.collapse', function () {
$('#showClaimInstructions').html('<i class="fa-solid fa-angle-down" style="margin-right:5px;"></i><span>Hide claim instructions</span>');
}).on('hide.bs.collapse', function () {
@@ -36,7 +24,8 @@ block content
var address = $('input#address').val();
var message = $('input#message').val();
var signature = $('input#signature').val();
var url = '/claim';
var recaptcha2 = ($('#g-recaptcha-response').length > 0 ? $('#g-recaptcha-response').val() : '');
var hcaptcha = ($('textarea[name^="h-captcha-response"]').length > 0 ? $('textarea[name^="h-captcha-response"]').val() : '');
if (address == null || address.trim().length == 0) {
showClaimAlert('warning', 'Please enter the wallet address you wish to claim', false);
@@ -44,19 +33,35 @@ block content
} else if (signature == null || signature.trim().length == 0) {
showClaimAlert('warning', 'Please enter the signature value from your wallet software', false);
$('input#signature').focus();
} else if (
(
'#{selected_captcha_name}' == 'google_recaptcha2' &&
'#{settings.captcha.google_recaptcha2.captcha_type}' == 'checkbox' &&
$('#g-recaptcha-response').length > 0 &&
recaptcha2 == ''
) ||
(
$('textarea[name^="h-captcha-response"]').length > 0 &&
hcaptcha == ''
)
) {
showClaimAlert('warning', 'The captcha validation has not been set', false);
} else {
$.ajax({
type: 'post',
url: url,
data: {
'address': address,
'message': message,
'signature': signature
},
success: function (data) {
showClaimAlert((data.status == 'success' ? 'success' : 'danger'), data.message, (data.status == 'success' && message == ''));
}
});
if ('#{selected_captcha_name}' == 'google_recaptcha2' && '#{settings.captcha.google_recaptcha2.captcha_type}' == 'invisible') {
grecaptcha.execute();
} else if ('#{selected_captcha_name}' == 'google_recaptcha3') {
grecaptcha.ready(function() {
grecaptcha.execute('#{(selected_captcha_name != '' && settings.captcha[selected_captcha_name].site_key != null ? settings.captcha[selected_captcha_name].site_key : '')}', {action: 'submit'}).then(function(token) {
submitForm(token);
});
});
} else if (recaptcha2 != '') {
submitForm(recaptcha2);
} else if (hcaptcha != '') {
submitForm(hcaptcha);
} else {
submitForm('');
}
}
});
if ('!{hash}' != 'null' && '!{hash}' != '') {
@@ -66,6 +71,60 @@ block content
if (#{settings.shared_pages.page_header.page_title_image.enable_animation} == true && #{settings.claim_address_page.page_header.show_img} == true)
startRotateElement('img#header-img');
});
function displayAsText(str) {
return str.replace(/</g, '&#60;').replace(/>/g, '&#62;');
}
function showClaimAlert(claimClass, warnMsg, removedClaim) {
if ($('#claimAlert').length == 0)
$('<div id="claimAlert"></div>').insertBefore('#claimForm');
$('#claimAlert').html(generateAlertHTML(claimClass, true, (claimClass == 'success' ? (removedClaim ? 'Address claim removed successfully' : 'Address claimed successfully') : (claimClass == 'danger' ? 'Failed to claim address' : 'Required field missing')), (claimClass == 'success' ? '<strong>' + $('input#address').val() + '</strong> will now be referred to as <strong>' + (removedClaim ? $('input#address').val() : displayAsText($('#message').val())) + '</strong> throughout the website' : warnMsg) + '.'));
fixFooterHeightAndPosition();
}
function generateAlertHTML(alertClass, allowDismiss, headerText, msgText) {
return '<div class="alert alert-' + alertClass + (allowDismiss ? ' alert-dismissible fade show' : '') + '" role="alert">' +
(allowDismiss ? '<button type="button" class="btn-close" data-bs-dismiss="alert"></button>' : '') +
'<div' + (msgText == null || msgText == '' ? '' : ' class="cardSpacer"') + '>' +
'<span class="fa-solid ' + (alertClass == 'success' ? 'fa-circle-check' : (alertClass == 'danger' ? 'fa-circle-exclamation' : (alertClass == 'info' ? 'fa-circle-info' : 'fa-triangle-exclamation'))) + '" style="margin-right:5px"></span>' +
'<strong>' + headerText + '</strong>' +
'</div>' +
(msgText == null || msgText == '' ? '' : '<span>' + msgText + '</span>') +
'</div>';
}
function submitForm(captchaData) {
const url = '/claim';
let postData = {
'address': $('input#address').val(),
'message': $('input#message').val(),
'signature': $('input#signature').val()
};
if ('#{selected_captcha_name}' != '' && captchaData != '')
postData['#{selected_captcha_name}'] = captchaData;
$.ajax({
type: 'post',
url: url,
data: postData
})
.done(function(data) {
showClaimAlert((data.status == 'success' ? 'success' : 'danger'), data.message, (data.status == 'success' && message == ''));
if (
'#{selected_captcha_name}' == 'google_recaptcha2' &&
'#{settings.captcha.google_recaptcha2.captcha_type}' == 'checkbox'
) {
// clear out the captcha to allow the form to be submitted again
grecaptcha.reset();
}
});
}
function onSubmit(token) {
submitForm(token);
// ensure the onSubmit event can fire again without needing to reload the page in the event that the server returns an error and the form must be submitted again
grecaptcha.reset();
}
.col-xs-12.col-md-12
if settings.claim_address_page.page_header.show_img == true || settings.claim_address_page.page_header.show_title == true || settings.claim_address_page.page_header.show_description == true
#page-header-container(style='align-items:' + (settings.claim_address_page.page_header.show_img == true && settings.claim_address_page.page_header.show_title == true && settings.claim_address_page.page_header.show_description == true ? 'flex-start' : 'center'))
@@ -141,4 +200,19 @@ block content
fieldset.entryField
label.form-label.mt-3(for='signature') Signature
input#signature.form-control.mb-3(type='text', placeholder='Signature', maxlength='100')
button.btn.btn-success(type='submit') Claim
if settings.claim_address_page.enable_captcha == true && selected_captcha_object != null
case selected_captcha_name
when 'google_recaptcha2'
if settings.captcha.google_recaptcha2.captcha_type == 'invisible'
div#recaptcha.g-recaptcha(data-sitekey=settings.captcha[selected_captcha_name].site_key, data-callback='onSubmit' data-size='invisible')
else
.form-group
fieldset.entryField
div(class='g-recaptcha mb-3', data-sitekey=settings.captcha[selected_captcha_name].site_key)
when 'hcaptcha'
.form-group
fieldset.entryField
div.h-captcha(data-sitekey=settings.captcha[selected_captcha_name].site_key)
.form-group
fieldset.entryField
button.btn.btn-success(type='submit') Claim
+15
View File
@@ -107,6 +107,21 @@ html(lang='en')
- showNethashChart = true
if settings.claim_address_page.show_difficulty_chart == true
- showDifficultyChart = true
if settings.claim_address_page.enable_captcha == true
- var selected_captcha_object = null
- var selected_captcha_name = ''
each captcha, name in settings.captcha
if selected_captcha_object == null && captcha != null && captcha.enabled == true
- selected_captcha_object = captcha
- selected_captcha_name = name
if selected_captcha_object != null
case selected_captcha_name
when 'google_recaptcha3'
script(type='text/javascript', src='https://www.google.com/recaptcha/api.js?render=' + settings.captcha.google_recaptcha3.site_key, async, defer)
when 'google_recaptcha2'
script(type='text/javascript', src='https://www.google.com/recaptcha/api.js', async, defer)
when 'hcaptcha'
script(type='text/javascript', src='https://js.hcaptcha.com/1/api.js', async, defer)
when 'orphans'
if settings.orphans_page.show_panels == true
- showPanels = true