nodebb-plugin-anti-account-sharing 1.0.5 → 1.0.6
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 +16 -23
- package/package.json +1 -1
- package/static/security.js +52 -20
package/library.js
CHANGED
|
@@ -8,23 +8,10 @@ const PLUGIN_ID = 'nodebb-plugin-anti-account-sharing';
|
|
|
8
8
|
|
|
9
9
|
const DEFAULTS = {
|
|
10
10
|
enabled: true,
|
|
11
|
-
|
|
12
|
-
// Fingerprint değişince ne yapalım?
|
|
13
|
-
// "kick" => diğer oturumları kır
|
|
14
|
-
// "block" => isteği 403 ile engelle (giriş yapanı bile)
|
|
15
11
|
enforcement: 'kick',
|
|
16
|
-
|
|
17
|
-
// Aynı kullanıcı için fingerprint değişimini kaç saniye "şüpheli" sayalım?
|
|
18
|
-
// Örn: 300 => son 5 dakikada farklı fingerprint görürse tetikle
|
|
19
12
|
windowSeconds: 300,
|
|
20
|
-
|
|
21
|
-
// Log seviyesi: "debug" | "info" | "warn" | "error"
|
|
22
13
|
logLevel: 'info',
|
|
23
|
-
|
|
24
|
-
// Fingerprint: ip + user-agent (+ accept-language opsiyonel)
|
|
25
14
|
includeAcceptLanguage: false,
|
|
26
|
-
|
|
27
|
-
// Reverse-proxy arkasında doğru ip için
|
|
28
15
|
trustProxy: true,
|
|
29
16
|
};
|
|
30
17
|
|
|
@@ -40,8 +27,6 @@ function toInt(v, dflt) {
|
|
|
40
27
|
}
|
|
41
28
|
|
|
42
29
|
async function getSettings() {
|
|
43
|
-
// ACP -> Settings -> Anti Account Sharing (meta settings) varsa buradan okur
|
|
44
|
-
// Yoksa defaults kullanır.
|
|
45
30
|
let s = {};
|
|
46
31
|
try {
|
|
47
32
|
s = await meta.settings.get(PLUGIN_ID);
|
|
@@ -70,7 +55,6 @@ function sha1(input) {
|
|
|
70
55
|
}
|
|
71
56
|
|
|
72
57
|
function getClientIp(req, trustProxy) {
|
|
73
|
-
// NodeBB/Express: trust proxy açık değilse x-forwarded-for güvenilmez olur.
|
|
74
58
|
if (trustProxy) {
|
|
75
59
|
const xff = (req.headers['x-forwarded-for'] || '').toString();
|
|
76
60
|
if (xff) return xff.split(',')[0].trim();
|
|
@@ -91,17 +75,18 @@ function getFingerprint(req, settings) {
|
|
|
91
75
|
return sha1(raw);
|
|
92
76
|
}
|
|
93
77
|
|
|
78
|
+
function safeJson(x) {
|
|
79
|
+
try { return JSON.stringify(x); } catch (e) { return '[unjsonable]'; }
|
|
80
|
+
}
|
|
81
|
+
|
|
94
82
|
function buildLogger(settings) {
|
|
95
|
-
// winston ile NodeBB loglarına düşer, ayrıca console’a da basar.
|
|
96
83
|
const level = settings?.logLevel || 'info';
|
|
97
84
|
|
|
98
85
|
function log(lvl, msg, metaObj) {
|
|
99
86
|
const line = `[AAS] ${msg}${metaObj ? ` ${safeJson(metaObj)}` : ''}`;
|
|
100
|
-
// NodeBB winston
|
|
101
87
|
if (winston && typeof winston[lvl] === 'function') winston[lvl](line);
|
|
102
88
|
else if (winston && typeof winston.info === 'function') winston.info(line);
|
|
103
89
|
|
|
104
|
-
// console fallback (docker logs)
|
|
105
90
|
if (lvl === 'error') console.error(line);
|
|
106
91
|
else if (lvl === 'warn') console.warn(line);
|
|
107
92
|
else console.log(line);
|
|
@@ -115,11 +100,14 @@ function buildLogger(settings) {
|
|
|
115
100
|
};
|
|
116
101
|
}
|
|
117
102
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
103
|
+
/**
|
|
104
|
+
* ✅ IMPORTANT: Hook'ları burada export et
|
|
105
|
+
* security.js içinde bunlar tanımlı olacak.
|
|
106
|
+
*/
|
|
107
|
+
const Security = require('./security');
|
|
121
108
|
|
|
122
109
|
module.exports = {
|
|
110
|
+
// helpers
|
|
123
111
|
PLUGIN_ID,
|
|
124
112
|
DEFAULTS,
|
|
125
113
|
getSettings,
|
|
@@ -127,4 +115,9 @@ module.exports = {
|
|
|
127
115
|
getClientIp,
|
|
128
116
|
sha1,
|
|
129
117
|
buildLogger,
|
|
130
|
-
|
|
118
|
+
|
|
119
|
+
// hooks (plugin.json bunları arıyor)
|
|
120
|
+
init: Security.init,
|
|
121
|
+
recordSession: Security.recordSession,
|
|
122
|
+
checkSession: Security.checkSession,
|
|
123
|
+
}
|
package/package.json
CHANGED
package/static/security.js
CHANGED
|
@@ -27,7 +27,6 @@ function sessionObjectKeys(sid) {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
async function getUserSessionIds(uid) {
|
|
30
|
-
// NodeBB genelde sorted set tutar
|
|
31
30
|
try {
|
|
32
31
|
const sids = await db.getSortedSetRange(SESS_SET(uid), 0, -1);
|
|
33
32
|
return Array.isArray(sids) ? sids : [];
|
|
@@ -36,8 +35,7 @@ async function getUserSessionIds(uid) {
|
|
|
36
35
|
}
|
|
37
36
|
}
|
|
38
37
|
|
|
39
|
-
async function
|
|
40
|
-
// session obj key’leri silmeyi dener
|
|
38
|
+
async function deleteSessionObjects(sid, logger) {
|
|
41
39
|
for (const k of sessionObjectKeys(sid)) {
|
|
42
40
|
try {
|
|
43
41
|
await db.delete(k);
|
|
@@ -53,6 +51,7 @@ async function kickOtherSessions(uid, keepSid, logger) {
|
|
|
53
51
|
if (!all.length) return { total: 0, removed: 0 };
|
|
54
52
|
|
|
55
53
|
let removed = 0;
|
|
54
|
+
|
|
56
55
|
for (const sid of all) {
|
|
57
56
|
if (!sid) continue;
|
|
58
57
|
if (keepSid && sid === keepSid) continue;
|
|
@@ -65,8 +64,9 @@ async function kickOtherSessions(uid, keepSid, logger) {
|
|
|
65
64
|
logger.warn('Failed removing session from sessions set', { uid, sid, err: e.message });
|
|
66
65
|
}
|
|
67
66
|
|
|
68
|
-
await
|
|
67
|
+
await deleteSessionObjects(sid, logger);
|
|
69
68
|
}
|
|
69
|
+
|
|
70
70
|
return { total: all.length, removed };
|
|
71
71
|
}
|
|
72
72
|
|
|
@@ -76,7 +76,6 @@ function getCurrentSessionId(req) {
|
|
|
76
76
|
|
|
77
77
|
// Cookie’den yakalamayı dene (connect.sid vb.)
|
|
78
78
|
const cookie = (req.headers && req.headers.cookie) ? String(req.headers.cookie) : '';
|
|
79
|
-
// connect.sid=s%3Axxxxx.yyyyy
|
|
80
79
|
const m = cookie.match(/connect\.sid=([^;]+)/i);
|
|
81
80
|
if (m && m[1]) return decodeURIComponent(m[1]).replace(/^s:/, '').split('.')[0];
|
|
82
81
|
return null;
|
|
@@ -100,6 +99,7 @@ async function shouldTrigger(uid, fp, nowSec, settings) {
|
|
|
100
99
|
if (diff <= settings.windowSeconds) {
|
|
101
100
|
return { trigger: true, reason: `fp_changed_within_${settings.windowSeconds}s` };
|
|
102
101
|
}
|
|
102
|
+
|
|
103
103
|
return { trigger: false, reason: `fp_changed_outside_window(${diff}s)` };
|
|
104
104
|
}
|
|
105
105
|
|
|
@@ -124,11 +124,6 @@ function createSecurityMiddleware(settings, logger) {
|
|
|
124
124
|
const uid = parseInt(req.uid, 10);
|
|
125
125
|
if (!Number.isFinite(uid) || uid <= 0) return next();
|
|
126
126
|
|
|
127
|
-
// Bazı route’larda user obj yoksa da problem değil
|
|
128
|
-
// (Bunu loglamak istersen aç)
|
|
129
|
-
// const u = await user.getUserFields(uid, ['username']);
|
|
130
|
-
// const username = u?.username;
|
|
131
|
-
|
|
132
127
|
const fp = getFingerprint(req, settings);
|
|
133
128
|
const nowSec = Math.floor(Date.now() / 1000);
|
|
134
129
|
|
|
@@ -145,20 +140,16 @@ function createSecurityMiddleware(settings, logger) {
|
|
|
145
140
|
});
|
|
146
141
|
|
|
147
142
|
if (settings.enforcement === 'block') {
|
|
148
|
-
// current request’i de engelle
|
|
149
143
|
await updateLast(uid, fp, req, nowSec);
|
|
150
|
-
res.status(403).json({
|
|
144
|
+
return res.status(403).json({
|
|
151
145
|
error: 'account-sharing-detected',
|
|
152
146
|
message: 'Bu hesap farklı cihaz/ağ üzerinden aynı anda kullanılıyor olabilir.',
|
|
153
147
|
});
|
|
154
|
-
return;
|
|
155
148
|
}
|
|
156
149
|
|
|
157
150
|
// kick mode: diğer session’ları kır
|
|
158
151
|
const result = await kickOtherSessions(uid, keepSid, logger);
|
|
159
152
|
logger.info('Kick result', { uid, ...result });
|
|
160
|
-
|
|
161
|
-
// (İsteğe bağlı) burada kullanıcıya bildirim de basılabilir
|
|
162
153
|
}
|
|
163
154
|
|
|
164
155
|
// her request sonunda last’i güncelle
|
|
@@ -174,8 +165,7 @@ function createSecurityMiddleware(settings, logger) {
|
|
|
174
165
|
const Security = {};
|
|
175
166
|
|
|
176
167
|
Security.init = async function (params) {
|
|
177
|
-
// NodeBB hook: static:app.load
|
|
178
|
-
// params: { app, router, middleware, controllers }
|
|
168
|
+
// NodeBB hook: static:app.load
|
|
179
169
|
const { app } = params;
|
|
180
170
|
const settings = await getSettings();
|
|
181
171
|
const logger = buildLogger(settings);
|
|
@@ -192,14 +182,56 @@ Security.init = async function (params) {
|
|
|
192
182
|
return;
|
|
193
183
|
}
|
|
194
184
|
|
|
195
|
-
//
|
|
185
|
+
// Reverse proxy varsa gerçek IP için
|
|
196
186
|
try {
|
|
197
187
|
app.set('trust proxy', !!settings.trustProxy);
|
|
198
188
|
} catch (e) {}
|
|
199
189
|
|
|
200
|
-
//
|
|
201
|
-
|
|
190
|
+
// ✅ DEBUG ENDPOINT
|
|
191
|
+
// Sadece login olmuş kullanıcı görür (istersen admin-only yap)
|
|
192
|
+
app.get('/api/aas/debug', async (req, res) => {
|
|
193
|
+
try {
|
|
194
|
+
const uid = parseInt(req.uid, 10) || 0;
|
|
195
|
+
if (uid <= 0) {
|
|
196
|
+
return res.status(401).json({ ok: false, error: 'not-logged-in' });
|
|
197
|
+
}
|
|
202
198
|
|
|
199
|
+
// ✅ opsiyonel: sadece admin
|
|
200
|
+
// const isAdmin = await user.isAdministrator(uid);
|
|
201
|
+
// if (!isAdmin) return res.status(403).json({ ok: false, error: 'admin-only' });
|
|
202
|
+
|
|
203
|
+
const fp = getFingerprint(req, settings);
|
|
204
|
+
const sid = req.sessionID;
|
|
205
|
+
|
|
206
|
+
const sessions = await db.getSortedSetRange(`uid:${uid}:sessions`, 0, -1);
|
|
207
|
+
|
|
208
|
+
logger.info('DEBUG endpoint hit', {
|
|
209
|
+
uid,
|
|
210
|
+
sid,
|
|
211
|
+
fp,
|
|
212
|
+
sessionsCount: sessions.length,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
return res.json({
|
|
216
|
+
ok: true,
|
|
217
|
+
plugin: PLUGIN_ID,
|
|
218
|
+
uid,
|
|
219
|
+
sessionID: sid,
|
|
220
|
+
fingerprint: fp,
|
|
221
|
+
sessions,
|
|
222
|
+
headers: {
|
|
223
|
+
ua: req.headers['user-agent'],
|
|
224
|
+
ip: req.headers['x-forwarded-for'] || req.ip,
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
} catch (e) {
|
|
228
|
+
logger.error('DEBUG endpoint error', { err: e.message });
|
|
229
|
+
return res.status(500).json({ error: e.message });
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// ✅ middleware attach
|
|
234
|
+
app.use(createSecurityMiddleware(settings, logger));
|
|
203
235
|
logger.info('Security middleware attached', { plugin: PLUGIN_ID });
|
|
204
236
|
};
|
|
205
237
|
|