@arnaudw38/nodebb-plugin-spam-be-gone 1.0.9 → 1.0.11
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/library.js +68 -7
- package/package.json +2 -3
- package/public/js/scripts.js +160 -3
package/library.js
CHANGED
|
@@ -4,7 +4,6 @@ const util = require('util');
|
|
|
4
4
|
const https = require('https');
|
|
5
5
|
const querystring = require('querystring');
|
|
6
6
|
const Honeypot = require('project-honeypot');
|
|
7
|
-
const stopforumspam = require('stopforumspam');
|
|
8
7
|
|
|
9
8
|
const winston = require.main.require('winston');
|
|
10
9
|
const nconf = require.main.require('nconf');
|
|
@@ -50,6 +49,71 @@ function getTurnstileConfigFromSettings(settings) {
|
|
|
50
49
|
};
|
|
51
50
|
}
|
|
52
51
|
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
function sfsRequest(path, method = 'GET', payload = null) {
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
const body = payload ? querystring.stringify(payload) : null;
|
|
57
|
+
const options = {
|
|
58
|
+
hostname: 'api.stopforumspam.org',
|
|
59
|
+
path,
|
|
60
|
+
method,
|
|
61
|
+
headers: {
|
|
62
|
+
'Accept': 'application/json',
|
|
63
|
+
'User-Agent': pluginData.id,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
if (body) {
|
|
67
|
+
options.headers['Content-Type'] = 'application/x-www-form-urlencoded';
|
|
68
|
+
options.headers['Content-Length'] = Buffer.byteLength(body);
|
|
69
|
+
}
|
|
70
|
+
const req = https.request(options, (res) => {
|
|
71
|
+
let responseData = '';
|
|
72
|
+
res.on('data', (chunk) => { responseData += chunk; });
|
|
73
|
+
res.on('end', () => {
|
|
74
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
75
|
+
return reject(new Error(`StopForumSpam request failed (${res.statusCode})`));
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
resolve(JSON.parse(responseData || '{}'));
|
|
79
|
+
} catch (err) {
|
|
80
|
+
reject(new Error('Invalid StopForumSpam response'));
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
req.on('error', reject);
|
|
85
|
+
if (body) {
|
|
86
|
+
req.write(body);
|
|
87
|
+
}
|
|
88
|
+
req.end();
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function sfsIsSpammer({ ip, email, username }) {
|
|
93
|
+
const params = { f: 'json' };
|
|
94
|
+
if (ip) { params.ip = ip; }
|
|
95
|
+
if (email) { params.email = email; }
|
|
96
|
+
if (username) { params.username = username; }
|
|
97
|
+
return await sfsRequest(`/api?${querystring.stringify(params)}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function sfsSubmit({ ip, email, username }, evidence) {
|
|
101
|
+
if (!pluginSettings.stopforumspamApiKey) {
|
|
102
|
+
throw new Error('[[spam-be-gone:sfs-api-key-not-set]]');
|
|
103
|
+
}
|
|
104
|
+
const payload = {
|
|
105
|
+
api_key: pluginSettings.stopforumspamApiKey,
|
|
106
|
+
ip_addr: ip || '',
|
|
107
|
+
email: email || '',
|
|
108
|
+
username: username || '',
|
|
109
|
+
evidence: evidence || '',
|
|
110
|
+
};
|
|
111
|
+
const result = await sfsRequest('/add', 'POST', payload);
|
|
112
|
+
if (result && (result.success === 1 || result.success === true)) {
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
throw new Error((result && (result.error || result.message)) || 'StopForumSpam submit failed');
|
|
116
|
+
}
|
|
53
117
|
Plugin.middleware.isAdminOrGlobalMod = function (req, res, next) {
|
|
54
118
|
User.isAdminOrGlobalMod(req.uid, (err, isAdminOrGlobalMod) => {
|
|
55
119
|
if (err) {
|
|
@@ -100,9 +164,6 @@ Plugin.load = async function (params) {
|
|
|
100
164
|
if (!settings.akismetMinReputationHam) {
|
|
101
165
|
settings.akismetMinReputationHam = 10;
|
|
102
166
|
}
|
|
103
|
-
if (settings.stopforumspamApiKey) {
|
|
104
|
-
stopforumspam.Key(settings.stopforumspamApiKey);
|
|
105
|
-
}
|
|
106
167
|
|
|
107
168
|
pluginSettings = settings;
|
|
108
169
|
|
|
@@ -137,7 +198,7 @@ Plugin.report = async function (req, res, next) {
|
|
|
137
198
|
if (isAdmin) {
|
|
138
199
|
return res.status(403).send({ message: '[[spam-be-gone:cant-report-admin]]' });
|
|
139
200
|
}
|
|
140
|
-
await
|
|
201
|
+
await sfsSubmit({ ip: ips[0], email: fields.email, username: fields.username }, `Manual submission from user: ${req.uid} to user: ${fields.uid} via ${pluginData.id}`);
|
|
141
202
|
res.status(200).json({ message: '[[spam-be-gone:user-reported]]' });
|
|
142
203
|
} catch (err) {
|
|
143
204
|
winston.error(`[plugins/${pluginData.nbbId}][report-error] ${err.message}`);
|
|
@@ -152,7 +213,7 @@ Plugin.reportFromQueue = async (req, res) => {
|
|
|
152
213
|
}
|
|
153
214
|
const submitData = { ip: data.ip, email: data.email, username: data.username };
|
|
154
215
|
try {
|
|
155
|
-
await
|
|
216
|
+
await sfsSubmit(submitData, `Manual submission from user: ${req.uid} to user: ${data.username} via ${pluginData.id}`);
|
|
156
217
|
res.status(200).json({ message: '[[spam-be-gone:user-reported]]' });
|
|
157
218
|
} catch (err) {
|
|
158
219
|
winston.error(`[plugins/${pluginData.nbbId}][report-error] ${err.message}\n${JSON.stringify(submitData, null, 4)}`);
|
|
@@ -283,7 +344,7 @@ Plugin.getRegistrationQueue = async function (data) {
|
|
|
283
344
|
async function augmentWitSpamData(user) {
|
|
284
345
|
try {
|
|
285
346
|
user.ip = user.ip.replace('::ffff:', '');
|
|
286
|
-
let body = await
|
|
347
|
+
let body = await sfsIsSpammer({ ip: user.ip, email: user.email, username: user.username });
|
|
287
348
|
if (!body) {
|
|
288
349
|
body = { success: 1, username: { frequency: 0, appears: 0 }, email: { frequency: 0, appears: 0 }, ip: { frequency: 0, appears: 0, asn: null } };
|
|
289
350
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arnaudw38/nodebb-plugin-spam-be-gone",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.11",
|
|
4
4
|
"description": "Anti-spam plugin for NodeBB 4.x using Akismet, StopForumSpam, ProjectHoneyPot, and Cloudflare Turnstile (Turnstile-only fork)",
|
|
5
5
|
"main": "library.js",
|
|
6
6
|
"scripts": {},
|
|
@@ -33,8 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"async": "^3.2.0",
|
|
36
|
-
"project-honeypot": "~0.0.0"
|
|
37
|
-
"stopforumspam": "^1.3.8"
|
|
36
|
+
"project-honeypot": "~0.0.0"
|
|
38
37
|
},
|
|
39
38
|
"nbbpm": {
|
|
40
39
|
"compatibility": "^4.0.0"
|
package/public/js/scripts.js
CHANGED
|
@@ -5,11 +5,22 @@
|
|
|
5
5
|
$(function () {
|
|
6
6
|
var pluginName = 'spam-be-gone';
|
|
7
7
|
var turnstileScriptUrl = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit';
|
|
8
|
+
var loginTurnstile = {
|
|
9
|
+
widgetId: null,
|
|
10
|
+
observer: null,
|
|
11
|
+
watchTimer: null,
|
|
12
|
+
isWatching: false,
|
|
13
|
+
};
|
|
8
14
|
|
|
9
15
|
function getTurnstileArgs() {
|
|
10
16
|
return ajaxify.data && ajaxify.data.turnstileArgs;
|
|
11
17
|
}
|
|
12
18
|
|
|
19
|
+
function isLoginPage() {
|
|
20
|
+
return !!(ajaxify.data && ajaxify.data.template && ajaxify.data.template.name === 'login') ||
|
|
21
|
+
(window.location.pathname && window.location.pathname.indexOf('/login') !== -1);
|
|
22
|
+
}
|
|
23
|
+
|
|
13
24
|
function injectScriptOnce(src) {
|
|
14
25
|
if (document.querySelector('script[src*="turnstile/v0/api.js"]')) {
|
|
15
26
|
return Promise.resolve();
|
|
@@ -25,9 +36,140 @@ $(function () {
|
|
|
25
36
|
});
|
|
26
37
|
}
|
|
27
38
|
|
|
28
|
-
function
|
|
39
|
+
function hasLoginErrorVisible() {
|
|
40
|
+
if (!isLoginPage()) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
var selectors = [
|
|
44
|
+
'.alert.alert-danger',
|
|
45
|
+
'.alert-danger',
|
|
46
|
+
'[component="alerts"] .alert-danger',
|
|
47
|
+
'[component="login"] .alert-danger',
|
|
48
|
+
'[component="login/login"] .alert-danger',
|
|
49
|
+
];
|
|
50
|
+
return selectors.some(function (sel) {
|
|
51
|
+
var nodes = document.querySelectorAll(sel);
|
|
52
|
+
return Array.prototype.some.call(nodes, function (node) {
|
|
53
|
+
return !!(node && node.offsetParent !== null && node.textContent && node.textContent.trim());
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function clearLoginErrorWatch() {
|
|
59
|
+
if (loginTurnstile.observer) {
|
|
60
|
+
loginTurnstile.observer.disconnect();
|
|
61
|
+
loginTurnstile.observer = null;
|
|
62
|
+
}
|
|
63
|
+
if (loginTurnstile.watchTimer) {
|
|
64
|
+
clearTimeout(loginTurnstile.watchTimer);
|
|
65
|
+
loginTurnstile.watchTimer = null;
|
|
66
|
+
}
|
|
67
|
+
loginTurnstile.isWatching = false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function resetLoginTurnstile() {
|
|
71
|
+
if (!isLoginPage() || typeof turnstile === 'undefined' || !turnstile || typeof turnstile.reset !== 'function') {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
29
74
|
var args = getTurnstileArgs();
|
|
30
|
-
|
|
75
|
+
var target = args && args.targetId ? document.getElementById(args.targetId) : null;
|
|
76
|
+
if (!target) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
if (loginTurnstile.widgetId !== null && loginTurnstile.widgetId !== undefined) {
|
|
81
|
+
turnstile.reset(loginTurnstile.widgetId);
|
|
82
|
+
} else {
|
|
83
|
+
turnstile.reset(target);
|
|
84
|
+
}
|
|
85
|
+
} catch (err) {
|
|
86
|
+
// ignore reset errors to avoid blocking login UX
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function watchForLoginFailureAndReset() {
|
|
91
|
+
if (!isLoginPage() || loginTurnstile.isWatching) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (typeof turnstile === 'undefined' || !turnstile) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
loginTurnstile.isWatching = true;
|
|
99
|
+
|
|
100
|
+
// Fast path in case an error is already present
|
|
101
|
+
if (hasLoginErrorVisible()) {
|
|
102
|
+
resetLoginTurnstile();
|
|
103
|
+
clearLoginErrorWatch();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
var body = document.body;
|
|
108
|
+
if (body && typeof MutationObserver !== 'undefined') {
|
|
109
|
+
loginTurnstile.observer = new MutationObserver(function () {
|
|
110
|
+
if (hasLoginErrorVisible()) {
|
|
111
|
+
resetLoginTurnstile();
|
|
112
|
+
clearLoginErrorWatch();
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
loginTurnstile.observer.observe(body, { childList: true, subtree: true, attributes: true });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Auto-cleanup if success navigation happens or no error is shown
|
|
119
|
+
loginTurnstile.watchTimer = window.setTimeout(function () {
|
|
120
|
+
clearLoginErrorWatch();
|
|
121
|
+
}, 5000);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function bindLoginRetryResetHandlers() {
|
|
125
|
+
document.removeEventListener('click', onLoginIntentCapture, true);
|
|
126
|
+
document.removeEventListener('keydown', onLoginKeydownCapture, true);
|
|
127
|
+
window.removeEventListener('beforeunload', clearLoginErrorWatch);
|
|
128
|
+
window.removeEventListener('pagehide', clearLoginErrorWatch);
|
|
129
|
+
|
|
130
|
+
document.addEventListener('click', onLoginIntentCapture, true);
|
|
131
|
+
document.addEventListener('keydown', onLoginKeydownCapture, true);
|
|
132
|
+
window.addEventListener('beforeunload', clearLoginErrorWatch);
|
|
133
|
+
window.addEventListener('pagehide', clearLoginErrorWatch);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function onLoginIntentCapture(ev) {
|
|
137
|
+
if (!isLoginPage()) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
var el = ev.target && ev.target.closest ? ev.target.closest('button, input[type="submit"], a') : null;
|
|
141
|
+
if (!el) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
var text = ((el.textContent || '') + ' ' + (el.value || '')).toLowerCase();
|
|
145
|
+
var component = (el.getAttribute && (el.getAttribute('component') || '')) || '';
|
|
146
|
+
var action = (el.getAttribute && (el.getAttribute('data-action') || '')) || '';
|
|
147
|
+
var type = (el.getAttribute && (el.getAttribute('type') || '')) || '';
|
|
148
|
+
var maybeLogin = component.indexOf('login') !== -1 || action.toLowerCase().indexOf('login') !== -1 || text.indexOf('log in') !== -1 || text.indexOf('login') !== -1 || type === 'submit';
|
|
149
|
+
if (!maybeLogin) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
window.setTimeout(watchForLoginFailureAndReset, 0);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function onLoginKeydownCapture(ev) {
|
|
156
|
+
if (!isLoginPage() || ev.key !== 'Enter') {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
var input = ev.target;
|
|
160
|
+
if (!input || input.tagName !== 'INPUT') {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
var type = (input.type || '').toLowerCase();
|
|
164
|
+
var name = (input.name || '').toLowerCase();
|
|
165
|
+
if (type === 'password' || name === 'password' || name === 'username' || name === 'email') {
|
|
166
|
+
window.setTimeout(watchForLoginFailureAndReset, 0);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function renderTurnstileIfNeeded(isLoginTpl) {
|
|
171
|
+
var args = getTurnstileArgs();
|
|
172
|
+
if (!args || (isLoginTpl && !args.addLoginTurnstile)) {
|
|
31
173
|
return;
|
|
32
174
|
}
|
|
33
175
|
|
|
@@ -40,7 +182,7 @@ $(function () {
|
|
|
40
182
|
if (!target || target.dataset.turnstileRendered === '1') {
|
|
41
183
|
return;
|
|
42
184
|
}
|
|
43
|
-
turnstile.render('#' + args.targetId, {
|
|
185
|
+
var widgetId = turnstile.render('#' + args.targetId, {
|
|
44
186
|
sitekey: args.siteKey,
|
|
45
187
|
theme: args.theme || 'auto',
|
|
46
188
|
size: args.size || 'normal',
|
|
@@ -55,8 +197,22 @@ $(function () {
|
|
|
55
197
|
'error-callback': function () {
|
|
56
198
|
require(['alerts'], function (alerts) { alerts.error('[[spam-be-gone:captcha-not-verified]]'); });
|
|
57
199
|
},
|
|
200
|
+
'expired-callback': function () {
|
|
201
|
+
if (isLoginTpl) {
|
|
202
|
+
clearLoginErrorWatch();
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
'timeout-callback': function () {
|
|
206
|
+
if (isLoginTpl) {
|
|
207
|
+
clearLoginErrorWatch();
|
|
208
|
+
}
|
|
209
|
+
},
|
|
58
210
|
});
|
|
59
211
|
target.dataset.turnstileRendered = '1';
|
|
212
|
+
if (isLoginTpl) {
|
|
213
|
+
loginTurnstile.widgetId = widgetId;
|
|
214
|
+
bindLoginRetryResetHandlers();
|
|
215
|
+
}
|
|
60
216
|
})
|
|
61
217
|
.catch(function () {
|
|
62
218
|
require(['alerts'], function (alerts) { alerts.error('Failed to load Cloudflare Turnstile'); });
|
|
@@ -94,6 +250,7 @@ $(function () {
|
|
|
94
250
|
}
|
|
95
251
|
|
|
96
252
|
$(window).on('action:ajaxify.end', function (evt, data) {
|
|
253
|
+
clearLoginErrorWatch();
|
|
97
254
|
switch (data.tpl_url) {
|
|
98
255
|
case 'register':
|
|
99
256
|
renderTurnstileIfNeeded(false);
|