@live-change/security-service 0.2.5
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/ban.js +188 -0
- package/definition.js +8 -0
- package/event.js +254 -0
- package/index.js +9 -0
- package/package.json +30 -0
- package/secured.js +50 -0
- package/utils.js +85 -0
package/ban.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
const app = require("@live-change/framework").app()
|
|
2
|
+
const definition = require('./definition.js')
|
|
3
|
+
const { getClientKeysStrings } = require('./utils.js')
|
|
4
|
+
const lcp = require('@live-change/pattern')
|
|
5
|
+
|
|
6
|
+
const banProperties = {
|
|
7
|
+
actions: {
|
|
8
|
+
type: Array,
|
|
9
|
+
of: {
|
|
10
|
+
type: String
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
keys: {
|
|
14
|
+
type: Array,
|
|
15
|
+
of: {
|
|
16
|
+
type: Object,
|
|
17
|
+
properties: {
|
|
18
|
+
key: {
|
|
19
|
+
type: {
|
|
20
|
+
String
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
value: {
|
|
24
|
+
type: {
|
|
25
|
+
String
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
expire: {
|
|
32
|
+
type: Date
|
|
33
|
+
},
|
|
34
|
+
type: {
|
|
35
|
+
type: String,
|
|
36
|
+
options: ['captcha', 'block', 'delay']
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const Ban = definition.model({
|
|
41
|
+
name: "Ban",
|
|
42
|
+
properties: {
|
|
43
|
+
...banProperties
|
|
44
|
+
},
|
|
45
|
+
indexes: {
|
|
46
|
+
bans: {
|
|
47
|
+
property: 'keys',
|
|
48
|
+
multi: true
|
|
49
|
+
},
|
|
50
|
+
actionBans: {
|
|
51
|
+
function: async function(input, output) {
|
|
52
|
+
const values = (ban) => {
|
|
53
|
+
const v = ban.keys.length
|
|
54
|
+
const w = ban.actions.length
|
|
55
|
+
let res = new Array(v * w)
|
|
56
|
+
for(let i = 0; i < v; i++) {
|
|
57
|
+
for(let j = 0; j < w; j++) {
|
|
58
|
+
const key = ban.keys[i]
|
|
59
|
+
res[i * v + j] = `${ban.actions[j]}:${key.key}:${key.value}`
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return res
|
|
63
|
+
}
|
|
64
|
+
await input.table("securityService_Ban").onChange((obj, oldObj) => {
|
|
65
|
+
if(obj && oldObj) {
|
|
66
|
+
let pointers = obj && new Set(values(obj))
|
|
67
|
+
let oldPointers = oldObj && new Set(values(oldObj))
|
|
68
|
+
for(let pointer of pointers) {
|
|
69
|
+
if(!!oldPointers.has(pointer)) output.change({ id: pointer+'_'+obj.id, to: obj.id }, null)
|
|
70
|
+
}
|
|
71
|
+
for(let pointer of oldPointers) {
|
|
72
|
+
if(!!pointers.has(pointer)) output.change(null, { id: pointer+'_'+obj.id, to: obj.id })
|
|
73
|
+
}
|
|
74
|
+
} else if(obj) {
|
|
75
|
+
values(obj).forEach(v => output.change({ id: v+'_'+obj.id, to: obj.id }, null))
|
|
76
|
+
} else if(oldObj) {
|
|
77
|
+
values(oldObj).forEach(v => output.change(null, { id: v+'_'+obj.id, to: obj.id }))
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
definition.event({
|
|
86
|
+
name: "banCreated",
|
|
87
|
+
async execute({ ban, data }) {
|
|
88
|
+
Ban.create({ id: ban, ...data })
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
definition.event({
|
|
93
|
+
name: "banRemoved",
|
|
94
|
+
async execute({ ban }) {
|
|
95
|
+
Ban.delete(ban)
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
definition.view({
|
|
100
|
+
name: "myBans",
|
|
101
|
+
properties: {},
|
|
102
|
+
daoPath(params, { client, service }) {
|
|
103
|
+
const keys = getClientKeysStrings(client)
|
|
104
|
+
return multiKeyIndexQuery(keys, 'Ban_bans')
|
|
105
|
+
},
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
definition.view({
|
|
109
|
+
name: "myActionBans",
|
|
110
|
+
properties: {
|
|
111
|
+
action: {
|
|
112
|
+
type: String
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
daoPath({ action }, { client, service }) {
|
|
116
|
+
const keys = getClientKeysStrings(client, action + ':')
|
|
117
|
+
return multiKeyIndexQuery(keys, 'Ban_actionBans')
|
|
118
|
+
},
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
definition.trigger({
|
|
122
|
+
name: "securityActionBan",
|
|
123
|
+
properties: {
|
|
124
|
+
ban: {
|
|
125
|
+
type: Object,
|
|
126
|
+
properties: {
|
|
127
|
+
actions: banProperties.actions,
|
|
128
|
+
expire: {
|
|
129
|
+
type: String
|
|
130
|
+
},
|
|
131
|
+
type: banProperties.type
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
keys: {
|
|
135
|
+
type: Object
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
async execute({ keys, event, ban: { actions, expire, type } }, { service }, emit) {
|
|
139
|
+
const ban = app.generateUid()
|
|
140
|
+
|
|
141
|
+
console.log("SECURITY BAN!", arguments[0])
|
|
142
|
+
|
|
143
|
+
const banKeys = []
|
|
144
|
+
for(const key of keys) {
|
|
145
|
+
//keys[key] = event.keys[key]
|
|
146
|
+
banKeys.push({ key, value: event.keys[key] })
|
|
147
|
+
}
|
|
148
|
+
console.log("ACTION KEYS", event.keys, '=>', banKeys)
|
|
149
|
+
|
|
150
|
+
const banExpire = expire && new Date(new Date().getTime() + lcp.parseDuration(expire))
|
|
151
|
+
|
|
152
|
+
console.log("BAN KEYS", banKeys)
|
|
153
|
+
console.log("BAN EXPIRE", banExpire)
|
|
154
|
+
|
|
155
|
+
// service.trigger({
|
|
156
|
+
// type: 'createTimer',
|
|
157
|
+
// timer: {
|
|
158
|
+
// timestamp: banExpire.getTime() + 1000,
|
|
159
|
+
// service: 'security',
|
|
160
|
+
// trigger: {
|
|
161
|
+
// type: 'removeExpiredBan',
|
|
162
|
+
// ban
|
|
163
|
+
// }
|
|
164
|
+
// }
|
|
165
|
+
// })
|
|
166
|
+
|
|
167
|
+
// emit({
|
|
168
|
+
// type: "banCreated",
|
|
169
|
+
// ban,
|
|
170
|
+
// data: { actions, banKeys, expire, type }
|
|
171
|
+
// })
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
definition.trigger({
|
|
176
|
+
name: "removeExpiredBan",
|
|
177
|
+
properties: {
|
|
178
|
+
...banProperties
|
|
179
|
+
},
|
|
180
|
+
async execute({ ban }, {client, service}, emit) {
|
|
181
|
+
emit({
|
|
182
|
+
type: "banRemoved",
|
|
183
|
+
ban
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
module.exports = { Ban }
|
package/definition.js
ADDED
package/event.js
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
const crypto = require('crypto')
|
|
2
|
+
const app = require("@live-change/framework").app()
|
|
3
|
+
const definition = require('./definition.js')
|
|
4
|
+
const { getClientKeysObject } = require('./utils.js')
|
|
5
|
+
|
|
6
|
+
const lcp = require('@live-change/pattern')
|
|
7
|
+
const lcpDb = require('@live-change/pattern-db')
|
|
8
|
+
const { request } = require('http')
|
|
9
|
+
|
|
10
|
+
const securityPatterns = definition.config.patterns
|
|
11
|
+
const relationsStore = lcpDb.relationsStore(app.dao, app.databaseName, 'securityService_relations')
|
|
12
|
+
lcp.prepareModelForLive(securityPatterns)
|
|
13
|
+
//console.log("SECURITY PATTERNS", securityPatterns)
|
|
14
|
+
|
|
15
|
+
const securityCounters = definition.config.counters
|
|
16
|
+
|
|
17
|
+
definition.beforeStart(service => {
|
|
18
|
+
relationsStore.createTable()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const eventProperties = {
|
|
22
|
+
type: {
|
|
23
|
+
type: String
|
|
24
|
+
},
|
|
25
|
+
keys: {
|
|
26
|
+
type: Array,
|
|
27
|
+
of: {
|
|
28
|
+
type: Object,
|
|
29
|
+
properties: {
|
|
30
|
+
key: {
|
|
31
|
+
type: {
|
|
32
|
+
String
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
value: {
|
|
36
|
+
type: {
|
|
37
|
+
String
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
timestamp: {
|
|
44
|
+
type: Date
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const Event = definition.model({
|
|
49
|
+
name: "Event",
|
|
50
|
+
properties: {
|
|
51
|
+
...eventProperties
|
|
52
|
+
},
|
|
53
|
+
indexes: {
|
|
54
|
+
byTypeAndTimestamp: {
|
|
55
|
+
property: ['type', 'timestamp']
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
definition.event({
|
|
61
|
+
name: "securityEvent",
|
|
62
|
+
async execute({ event, eventType, keys, newRelations, canceledRelations }) {
|
|
63
|
+
await Event.create({ id: event, type: eventType, keys, timestamp: new Date() })
|
|
64
|
+
const promises = []
|
|
65
|
+
for(const relation of newRelations) {
|
|
66
|
+
promises.push(relationsStore.saveRelation(relation))
|
|
67
|
+
}
|
|
68
|
+
for(const relation of canceledRelations) {
|
|
69
|
+
promises.push(relationsStore.removeRelation(relation))
|
|
70
|
+
}
|
|
71
|
+
await Promise.all(promises)
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
async function processSecurityPatterns(event, time, service) {
|
|
77
|
+
const changes = await lcp.processEvent({...event, time}, securityPatterns, relationsStore.getRelations)
|
|
78
|
+
console.log("PROCESSED EVENT PATTERNS", event, '=>', changes)
|
|
79
|
+
const { newRelations, canceledRelations, actions } = changes
|
|
80
|
+
|
|
81
|
+
for (const relation of newRelations) {
|
|
82
|
+
if (relation.prev) {
|
|
83
|
+
for (const prev of relation.prev) {
|
|
84
|
+
if (prev.relation) prev.relation.prev = null
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let promises = []
|
|
90
|
+
for (const relation of newRelations) {
|
|
91
|
+
if (relation.type == 'eq') {
|
|
92
|
+
// will be handled by event
|
|
93
|
+
} else if (relation.type == 'timeout') {
|
|
94
|
+
const id = crypto.createHash('md5').update(event.id + relation.relation).digest('hex')
|
|
95
|
+
promises.push(service.trigger({
|
|
96
|
+
type: "createTimer",
|
|
97
|
+
timer: {
|
|
98
|
+
id,
|
|
99
|
+
timestamp: relation.time,
|
|
100
|
+
service: 'security',
|
|
101
|
+
trigger: {
|
|
102
|
+
type: 'handleTimeout',
|
|
103
|
+
timeout: relation
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}))
|
|
107
|
+
} else {
|
|
108
|
+
throw new Error(`Relation type ${JSON.stringify(relation.type)} not supported`)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
await Promise.all(promises)
|
|
113
|
+
promises = []
|
|
114
|
+
|
|
115
|
+
for (const relation of canceledRelations) {
|
|
116
|
+
const relationModel = securityPatterns.relations[relation.relation]
|
|
117
|
+
if (relationModel.eq) {
|
|
118
|
+
// will be handled by event
|
|
119
|
+
} else if (relationModel.wait) {
|
|
120
|
+
const id = crypto.createHash('md5').update(event.id + relation.relation).digest('hex')
|
|
121
|
+
promises.push(service.trigger({
|
|
122
|
+
type: "cancelTimerIfExists",
|
|
123
|
+
timer: id
|
|
124
|
+
}))
|
|
125
|
+
} else {
|
|
126
|
+
throw new Error(`Relation ${JSON.stringify(relation.relation)} not supported`)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
await Promise.all(promises)
|
|
131
|
+
return { newRelations, canceledRelations, actions }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function processSecurityCounters(event, timestamp, service) {
|
|
135
|
+
const now = Date.now()
|
|
136
|
+
const actions = []
|
|
137
|
+
const counters = securityCounters.filter(counter => counter.match.includes(event.type))
|
|
138
|
+
if(counters.length == 0) return
|
|
139
|
+
const counterEventsRequests = []
|
|
140
|
+
for(const counter of counters) {
|
|
141
|
+
const duration = lcp.parseDuration(counter.duration)
|
|
142
|
+
for(const eventType of counter.match) {
|
|
143
|
+
const request = counterEventsRequests.find(req => req.type == eventType)
|
|
144
|
+
if(request) {
|
|
145
|
+
request.max = Math.max(request.max, counter.max)
|
|
146
|
+
request.duration = Math.max(request.duration, duration)
|
|
147
|
+
} else {
|
|
148
|
+
counterEventsRequests.push({
|
|
149
|
+
type: eventType,
|
|
150
|
+
max: counter.max,
|
|
151
|
+
duration
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
console.log("COUNTER EVENTS REQUESTS", counterEventsRequests)
|
|
157
|
+
const counterEvents = await Promise.all(counterEventsRequests.map(async request => ({
|
|
158
|
+
type: request.type,
|
|
159
|
+
events: await Event.indexRangeGet('byTypeAndTimestamp',{
|
|
160
|
+
gt: `"${request.type}":"${(new Date(now - request.duration - 1000)).toISOString()}"`,
|
|
161
|
+
lt: `"${request.type}":\xFF`,
|
|
162
|
+
reverse: true,
|
|
163
|
+
limit: request.max
|
|
164
|
+
})
|
|
165
|
+
})))
|
|
166
|
+
console.log("COUNTER EVENTS", counterEvents)
|
|
167
|
+
for(const counter of counters) {
|
|
168
|
+
const duration = lcp.parseDuration(counter.duration)
|
|
169
|
+
const fromTime = (new Date(now - duration)).toISOString()
|
|
170
|
+
let count = 0
|
|
171
|
+
for(const events of counterEvents) {
|
|
172
|
+
if(!counter.match.includes(events.type)) continue
|
|
173
|
+
for(const event of events.events) {
|
|
174
|
+
if(event.timestamp > fromTime) {
|
|
175
|
+
count ++
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if(count + 1 > counter.max) { // +1 for the new event, not added yet
|
|
180
|
+
console.log("COUNTER FIRE", counter)
|
|
181
|
+
actions.push(...counter.actions)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return { actions }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
definition.trigger({
|
|
188
|
+
name: "securityEvent",
|
|
189
|
+
properties: {
|
|
190
|
+
event: {
|
|
191
|
+
type: Object,
|
|
192
|
+
properties: {
|
|
193
|
+
type: {
|
|
194
|
+
type: String
|
|
195
|
+
},
|
|
196
|
+
keys: {
|
|
197
|
+
type: Object
|
|
198
|
+
},
|
|
199
|
+
properties: {
|
|
200
|
+
type: Object
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
client: {
|
|
205
|
+
type: Object
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
async execute({ event, client, timestamp }, { service }, emit) {
|
|
209
|
+
console.log("SECURITY EVENT TRIGGERED", arguments[0])
|
|
210
|
+
event.id = event.id || app.generateUid()
|
|
211
|
+
event.time = event.time || timestamp
|
|
212
|
+
const time = (typeof event.time == 'number') ? event.time : new Date(event.time).getTime()
|
|
213
|
+
|
|
214
|
+
if(client) {
|
|
215
|
+
event.keys = { ...getClientKeysObject(client), ...event.keys }
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
console.log("PROCESS EVENT", event)
|
|
219
|
+
let [
|
|
220
|
+
{ newRelations, canceledRelations, actions: patternsActions },
|
|
221
|
+
{ actions: countersActions }
|
|
222
|
+
] = await Promise.all([
|
|
223
|
+
processSecurityPatterns(event, time, service),
|
|
224
|
+
processSecurityCounters(event, time, service)
|
|
225
|
+
])
|
|
226
|
+
|
|
227
|
+
let promises = []
|
|
228
|
+
const actions = patternsActions.concat(countersActions)
|
|
229
|
+
|
|
230
|
+
for(const action of actions) {
|
|
231
|
+
const actionTypeUpperCase = action.type[0].toUpperCase() + action.type.slice(1)
|
|
232
|
+
console.log("ACTION", JSON.stringify(action, null, ' '))
|
|
233
|
+
promises.push(service.trigger({
|
|
234
|
+
...action,
|
|
235
|
+
event,
|
|
236
|
+
type: 'securityAction' + actionTypeUpperCase
|
|
237
|
+
}))
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
await Promise.all(promises)
|
|
241
|
+
|
|
242
|
+
emit({
|
|
243
|
+
type: "securityEvent",
|
|
244
|
+
event: event.id,
|
|
245
|
+
eventType: event.type,
|
|
246
|
+
keys: event.keys,
|
|
247
|
+
actions,
|
|
248
|
+
newRelations: newRelations.filter(rel => rel.type == 'eq'),
|
|
249
|
+
canceledRelations: canceledRelations.filter(rel => rel.type == 'eq')
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
}
|
|
253
|
+
})
|
|
254
|
+
|
package/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@live-change/security-service",
|
|
3
|
+
"version": "0.2.5",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "NODE_ENV=test tape tests/*"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/live-change/live-change-services.git"
|
|
12
|
+
},
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/live-change/live-change-services/issues"
|
|
16
|
+
},
|
|
17
|
+
"homepage": "https://github.com/live-change/live-change-services",
|
|
18
|
+
"author": {
|
|
19
|
+
"email": "michal@laszczewski.pl",
|
|
20
|
+
"name": "Michał Łaszczewski",
|
|
21
|
+
"url": "https://www.viamage.com/"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@live-change/framework": "^0.5.7",
|
|
25
|
+
"@live-change/pattern": "^0.2.1",
|
|
26
|
+
"@live-change/pattern-db": "^0.2.2",
|
|
27
|
+
"nodemailer": "^6.7.2"
|
|
28
|
+
},
|
|
29
|
+
"gitHead": "7691357c7569a18373668691eb6a81a9e161194d"
|
|
30
|
+
}
|
package/secured.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const definition = require('./definition.js')
|
|
2
|
+
const { getClientKeysObject, getClientKeysStrings, multiKeyIndexQuery } = require('./utils.js')
|
|
3
|
+
|
|
4
|
+
definition.processor(function(service, app) {
|
|
5
|
+
|
|
6
|
+
for(let actionName in service.actions) {
|
|
7
|
+
const action = service.actions[actionName]
|
|
8
|
+
if(!action.secured) continue
|
|
9
|
+
const config = action.secured
|
|
10
|
+
|
|
11
|
+
console.log("SECURED", service.name, action.name)
|
|
12
|
+
|
|
13
|
+
const oldExec = action.execute
|
|
14
|
+
action.execute = async (...args) => {
|
|
15
|
+
const [ properties, context, emit ] = args
|
|
16
|
+
const { client } = context
|
|
17
|
+
oldExec.apply(action, args)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/// TODO: detect bans, block actions
|
|
21
|
+
/// TODO: detect associated events
|
|
22
|
+
/// TODO: report security violation if succeded
|
|
23
|
+
/// TODO: report security violation if failed - another event
|
|
24
|
+
/// TODO: additional validation based on ban type(captcha)
|
|
25
|
+
/// TODO: additional delay based on ban type
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
for(let triggerName in service.actions) {
|
|
29
|
+
const trigger = service.actions[triggerName]
|
|
30
|
+
if(!trigger.secured) continue
|
|
31
|
+
const config = trigger.secured
|
|
32
|
+
|
|
33
|
+
console.log("SECURED TRIGGER", service.name, trigger.name)
|
|
34
|
+
|
|
35
|
+
const oldExec = trigger.execute
|
|
36
|
+
trigger.execute = async (...args) => {
|
|
37
|
+
const [ properties, context, emit ] = args
|
|
38
|
+
const { client, ...otherProperties } = properties
|
|
39
|
+
oldExec.apply(trigger, args)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// TODO: detect bans, block triggers
|
|
43
|
+
/// TODO: detect associated events
|
|
44
|
+
/// TODO: report security violation if succeded
|
|
45
|
+
/// TODO: report security violation if failed - another event
|
|
46
|
+
/// TODO: additional validation based on ban type(captcha)
|
|
47
|
+
/// TODO: additional delay based on ban type
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
})
|
package/utils.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const definition = require('./definition.js')
|
|
2
|
+
|
|
3
|
+
const clientKeys = definition.config.clientKeys
|
|
4
|
+
|
|
5
|
+
function multiKeyIndexQuery(keys, indexName) {
|
|
6
|
+
return ['database', 'query', database, `(${
|
|
7
|
+
async (input, output, { keys, indexName, tableName }) => {
|
|
8
|
+
const objectStates = new Map()
|
|
9
|
+
async function mapper(res) {
|
|
10
|
+
input.table(tableName).object(res.to).get()
|
|
11
|
+
}
|
|
12
|
+
async function onIndexChange(obj, oldObj) {
|
|
13
|
+
if(obj && !oldObj) {
|
|
14
|
+
const data = await mapper(obj)
|
|
15
|
+
if(data) output.change(data, null)
|
|
16
|
+
}
|
|
17
|
+
if(obj && obj.to) {
|
|
18
|
+
let objectState = objectStates.get(obj.to)
|
|
19
|
+
if(!objectState) {
|
|
20
|
+
objectState = { data: undefined, refs: 1 }
|
|
21
|
+
objectState.reader = input.table(tableName).object(obj.to)
|
|
22
|
+
const ind = obj
|
|
23
|
+
objectState.observer = await objectState.reader.onChange(async obj => {
|
|
24
|
+
const data = obj
|
|
25
|
+
const oldData = objectState.data
|
|
26
|
+
output.change(data, oldData)
|
|
27
|
+
if(data) {
|
|
28
|
+
objectState.data = obj
|
|
29
|
+
} else if(oldObj) {
|
|
30
|
+
objectState.data = null
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
objectStates.set(ind.to, objectState)
|
|
34
|
+
} else if(!oldObj || oldObj.to != obj.to) {
|
|
35
|
+
objectState.refs ++
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if(oldObj && oldObj.to && (!obj || obj.to != oldObj.to)) {
|
|
39
|
+
let objectState = objectStates.get(oldObj.to)
|
|
40
|
+
if(objectState) {
|
|
41
|
+
objectState.refs --
|
|
42
|
+
if(objectState.refs <= 0) {
|
|
43
|
+
objectState.reader.unobserve(objectState.observer)
|
|
44
|
+
objectStates.delete(oldObj.to)
|
|
45
|
+
output.change(null, objectState.data)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
await Promise.all(keys.map(async (encodedKey) => {
|
|
51
|
+
const range = {
|
|
52
|
+
gte: encodedKey + '_',
|
|
53
|
+
lte: encodedKey + "_\xFF\xFF\xFF\xFF"
|
|
54
|
+
}
|
|
55
|
+
await (await input.index(indexName)).range(range).onChange(onIndexChange)
|
|
56
|
+
}))
|
|
57
|
+
}
|
|
58
|
+
})`, { keys, indexName, tableName: Ban.tableName }]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getClientKeysStrings(client, prefix = '') {
|
|
62
|
+
if(clientKeys) {
|
|
63
|
+
return clientKeys(client).map(k => prefix + k.key + ':' + k.value)
|
|
64
|
+
} else {
|
|
65
|
+
const keys = []
|
|
66
|
+
for(let key in client) {
|
|
67
|
+
keys.push(prefix + key + ':' + client[key])
|
|
68
|
+
}
|
|
69
|
+
return keys
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getClientKeysObject(client, prefix = '') {
|
|
74
|
+
if(clientKeys) {
|
|
75
|
+
const obj = {}
|
|
76
|
+
for(const { key, value } of clientKeys(client)) {
|
|
77
|
+
obj[key] = value
|
|
78
|
+
}
|
|
79
|
+
return obj
|
|
80
|
+
} else {
|
|
81
|
+
return client
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = { multiKeyIndexQuery, getClientKeysStrings, getClientKeysObject }
|