@tilua-types/bridge 1.0.0 → 2.1.0
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/README.md +86 -22
- package/generate.mjs +93 -70
- package/index.d.tilua +31 -7
- package/lowering.mjs +228 -187
- package/package.json +3 -3
- package/runtime/bridge.luau +830 -103
package/runtime/bridge.luau
CHANGED
|
@@ -13,8 +13,15 @@
|
|
|
13
13
|
-- Client to server, each packet is AES-GCM under a key the server hands each
|
|
14
14
|
-- player once, with a sequence number inside, so a packet cannot be forged,
|
|
15
15
|
-- changed or sent twice. Server to client is plain: nothing but the server can
|
|
16
|
-
-- fire at a client.
|
|
16
|
+
-- fire at a client. `bridge.invoke` is sealed and numbered the same way, apart,
|
|
17
|
+
-- through a RemoteFunction; the answer comes back plain, to that client alone.
|
|
18
|
+
--
|
|
19
|
+
-- What is sent in one frame goes in one packet, at the next Heartbeat: each
|
|
20
|
+
-- message is `[name, arguments]`, its arguments a JSON array of their own, so a
|
|
21
|
+
-- handler's limits are checked before they are decoded. Instances travel as
|
|
22
|
+
-- their path from `game` and are looked up again where they arrive.
|
|
17
23
|
|
|
24
|
+
local DataModel = game
|
|
18
25
|
local RunService = game:GetService("RunService")
|
|
19
26
|
local Players = game:GetService("Players")
|
|
20
27
|
local HttpService = game:GetService("HttpService")
|
|
@@ -31,6 +38,11 @@ local UNHANDLED_AFTER = 10
|
|
|
31
38
|
local NO_SERVER_AFTER = 15
|
|
32
39
|
-- How long a breach waits for a file that registers `bridge.onBreach` to load.
|
|
33
40
|
local BREACH_GRACE = 5
|
|
41
|
+
-- How deep a table, how long an Instance's path and how many arguments a message
|
|
42
|
+
-- may have: past them a sender is refused and a packet is out of shape.
|
|
43
|
+
local MAX_DEPTH = 64
|
|
44
|
+
local MAX_PATH = 100
|
|
45
|
+
local MAX_ARGS = 255
|
|
34
46
|
|
|
35
47
|
local ONE_VM = "bridge: this module ran a second time, which means an Actor: the bridge works in one Luau VM, so call it from outside Actors"
|
|
36
48
|
|
|
@@ -64,13 +76,20 @@ local breachHandlers = {}
|
|
|
64
76
|
local banning = {}
|
|
65
77
|
|
|
66
78
|
-- With no handler: for good, and the player's alt accounts too. A ban that fails
|
|
67
|
-
-- is a kick. Studio
|
|
68
|
-
|
|
79
|
+
-- is a kick. In Studio it is a kick that says why, so a test cannot ban whoever
|
|
80
|
+
-- runs it.
|
|
81
|
+
local function punish(player, reason)
|
|
69
82
|
warn(("[bridge] %s: %s"):format(player.Name, reason))
|
|
70
|
-
if banning[player]
|
|
83
|
+
if banning[player] then
|
|
71
84
|
return
|
|
72
85
|
end
|
|
73
86
|
banning[player] = true
|
|
87
|
+
if RunService:IsStudio() then
|
|
88
|
+
if player.Parent then
|
|
89
|
+
player:Kick("[bridge] " .. reason)
|
|
90
|
+
end
|
|
91
|
+
return
|
|
92
|
+
end
|
|
74
93
|
task.spawn(function()
|
|
75
94
|
local ok, failure = pcall(Players.BanAsync, Players, {
|
|
76
95
|
UserIds = { player.UserId },
|
|
@@ -95,7 +114,8 @@ local function runBreachHandlers(player, reason)
|
|
|
95
114
|
end
|
|
96
115
|
end
|
|
97
116
|
|
|
98
|
-
-- On the server: whoever the game registered with `bridge.onBreach`, or
|
|
117
|
+
-- On the server: whoever the game registered with `bridge.onBreach`, or the
|
|
118
|
+
-- punishment.
|
|
99
119
|
local function serverBreach(player, reason)
|
|
100
120
|
if #breachHandlers > 0 then
|
|
101
121
|
runBreachHandlers(player, reason)
|
|
@@ -105,60 +125,481 @@ local function serverBreach(player, reason)
|
|
|
105
125
|
if #breachHandlers > 0 then
|
|
106
126
|
runBreachHandlers(player, reason)
|
|
107
127
|
elseif player then
|
|
108
|
-
|
|
128
|
+
punish(player, reason)
|
|
109
129
|
end
|
|
110
130
|
end)
|
|
111
131
|
end
|
|
112
132
|
|
|
133
|
+
--------------------------------------------------------------------------------
|
|
134
|
+
-- Arguments
|
|
135
|
+
--------------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
-- JSON carries strings, numbers, booleans and tables of them. The rest is said by
|
|
138
|
+
-- a table with one key that starts with \1: an Instance by its path of names from
|
|
139
|
+
-- `game`, a Vector3 or CFrame or the like by its components, a `nil` argument by
|
|
140
|
+
-- a mark, so the array has no holes. A key of the
|
|
141
|
+
-- game's own that starts with \1 gets another in front, and loses it on arrival.
|
|
142
|
+
local TAG = "\1"
|
|
143
|
+
local INSTANCE = TAG .. "i"
|
|
144
|
+
local NIL = TAG .. "n"
|
|
145
|
+
|
|
146
|
+
local function pathOf(instance)
|
|
147
|
+
local names = {}
|
|
148
|
+
local at = instance
|
|
149
|
+
while at ~= DataModel do
|
|
150
|
+
if at == nil or #names >= MAX_PATH then
|
|
151
|
+
return nil
|
|
152
|
+
end
|
|
153
|
+
table.insert(names, 1, at.Name)
|
|
154
|
+
at = at.Parent
|
|
155
|
+
end
|
|
156
|
+
return names
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
-- The first child of each name, from `game`: nil where there is none, as there is
|
|
160
|
+
-- for what the other side has and this one was never given
|
|
161
|
+
local function instanceAt(path)
|
|
162
|
+
if type(path) ~= "table" or #path > MAX_PATH then
|
|
163
|
+
return nil
|
|
164
|
+
end
|
|
165
|
+
local at = DataModel
|
|
166
|
+
for _, name in ipairs(path) do
|
|
167
|
+
if type(name) ~= "string" then
|
|
168
|
+
return nil
|
|
169
|
+
end
|
|
170
|
+
at = at:FindFirstChild(name)
|
|
171
|
+
if not at then
|
|
172
|
+
return nil
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
return at
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
-- Roblox's value types, as `{"\1d": [typeof, [components]]}`: each says what its
|
|
179
|
+
-- components are and makes it again from them. What arrives is checked part by
|
|
180
|
+
-- part — a number where a number goes, as many as there should be — and a value
|
|
181
|
+
-- that does not make one is nil.
|
|
182
|
+
local DATATYPE = TAG .. "d"
|
|
183
|
+
local MAX_KEYPOINTS = 20
|
|
184
|
+
|
|
185
|
+
local function numbers(parts, count)
|
|
186
|
+
if #parts ~= count then
|
|
187
|
+
error("wrong count", 0)
|
|
188
|
+
end
|
|
189
|
+
for i = 1, count do
|
|
190
|
+
if type(parts[i]) ~= "number" then
|
|
191
|
+
error("not a number", 0)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
return table.unpack(parts, 1, count)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
-- A sequence's keypoints, each `stride` numbers
|
|
198
|
+
local function keypoints(parts, stride, make)
|
|
199
|
+
if #parts == 0 or #parts % stride ~= 0 or #parts > stride * MAX_KEYPOINTS then
|
|
200
|
+
error("wrong count", 0)
|
|
201
|
+
end
|
|
202
|
+
local points = {}
|
|
203
|
+
for i = 1, #parts, stride do
|
|
204
|
+
table.insert(points, make(numbers(table.move(parts, i, i + stride - 1, 1, {}), stride)))
|
|
205
|
+
end
|
|
206
|
+
return points
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
local DATATYPES = {
|
|
210
|
+
Vector3 = {
|
|
211
|
+
pack = function(v)
|
|
212
|
+
return { v.X, v.Y, v.Z }
|
|
213
|
+
end,
|
|
214
|
+
unpack = function(p)
|
|
215
|
+
return Vector3.new(numbers(p, 3))
|
|
216
|
+
end,
|
|
217
|
+
},
|
|
218
|
+
Vector2 = {
|
|
219
|
+
pack = function(v)
|
|
220
|
+
return { v.X, v.Y }
|
|
221
|
+
end,
|
|
222
|
+
unpack = function(p)
|
|
223
|
+
return Vector2.new(numbers(p, 2))
|
|
224
|
+
end,
|
|
225
|
+
},
|
|
226
|
+
Vector3int16 = {
|
|
227
|
+
pack = function(v)
|
|
228
|
+
return { v.X, v.Y, v.Z }
|
|
229
|
+
end,
|
|
230
|
+
unpack = function(p)
|
|
231
|
+
return Vector3int16.new(numbers(p, 3))
|
|
232
|
+
end,
|
|
233
|
+
},
|
|
234
|
+
Vector2int16 = {
|
|
235
|
+
pack = function(v)
|
|
236
|
+
return { v.X, v.Y }
|
|
237
|
+
end,
|
|
238
|
+
unpack = function(p)
|
|
239
|
+
return Vector2int16.new(numbers(p, 2))
|
|
240
|
+
end,
|
|
241
|
+
},
|
|
242
|
+
CFrame = {
|
|
243
|
+
pack = function(v)
|
|
244
|
+
return { v:GetComponents() }
|
|
245
|
+
end,
|
|
246
|
+
unpack = function(p)
|
|
247
|
+
return CFrame.new(numbers(p, 12))
|
|
248
|
+
end,
|
|
249
|
+
},
|
|
250
|
+
Color3 = {
|
|
251
|
+
pack = function(v)
|
|
252
|
+
return { v.R, v.G, v.B }
|
|
253
|
+
end,
|
|
254
|
+
unpack = function(p)
|
|
255
|
+
return Color3.new(numbers(p, 3))
|
|
256
|
+
end,
|
|
257
|
+
},
|
|
258
|
+
BrickColor = {
|
|
259
|
+
pack = function(v)
|
|
260
|
+
return { v.Number }
|
|
261
|
+
end,
|
|
262
|
+
unpack = function(p)
|
|
263
|
+
return BrickColor.new(numbers(p, 1))
|
|
264
|
+
end,
|
|
265
|
+
},
|
|
266
|
+
UDim = {
|
|
267
|
+
pack = function(v)
|
|
268
|
+
return { v.Scale, v.Offset }
|
|
269
|
+
end,
|
|
270
|
+
unpack = function(p)
|
|
271
|
+
return UDim.new(numbers(p, 2))
|
|
272
|
+
end,
|
|
273
|
+
},
|
|
274
|
+
UDim2 = {
|
|
275
|
+
pack = function(v)
|
|
276
|
+
return { v.X.Scale, v.X.Offset, v.Y.Scale, v.Y.Offset }
|
|
277
|
+
end,
|
|
278
|
+
unpack = function(p)
|
|
279
|
+
return UDim2.new(numbers(p, 4))
|
|
280
|
+
end,
|
|
281
|
+
},
|
|
282
|
+
Rect = {
|
|
283
|
+
pack = function(v)
|
|
284
|
+
return { v.Min.X, v.Min.Y, v.Max.X, v.Max.Y }
|
|
285
|
+
end,
|
|
286
|
+
unpack = function(p)
|
|
287
|
+
return Rect.new(numbers(p, 4))
|
|
288
|
+
end,
|
|
289
|
+
},
|
|
290
|
+
NumberRange = {
|
|
291
|
+
pack = function(v)
|
|
292
|
+
return { v.Min, v.Max }
|
|
293
|
+
end,
|
|
294
|
+
unpack = function(p)
|
|
295
|
+
return NumberRange.new(numbers(p, 2))
|
|
296
|
+
end,
|
|
297
|
+
},
|
|
298
|
+
NumberSequence = {
|
|
299
|
+
pack = function(v)
|
|
300
|
+
local parts = {}
|
|
301
|
+
for _, point in v.Keypoints do
|
|
302
|
+
table.insert(parts, point.Time)
|
|
303
|
+
table.insert(parts, point.Value)
|
|
304
|
+
table.insert(parts, point.Envelope)
|
|
305
|
+
end
|
|
306
|
+
return parts
|
|
307
|
+
end,
|
|
308
|
+
unpack = function(p)
|
|
309
|
+
return NumberSequence.new(keypoints(p, 3, NumberSequenceKeypoint.new))
|
|
310
|
+
end,
|
|
311
|
+
},
|
|
312
|
+
ColorSequence = {
|
|
313
|
+
pack = function(v)
|
|
314
|
+
local parts = {}
|
|
315
|
+
for _, point in v.Keypoints do
|
|
316
|
+
table.insert(parts, point.Time)
|
|
317
|
+
table.insert(parts, point.Value.R)
|
|
318
|
+
table.insert(parts, point.Value.G)
|
|
319
|
+
table.insert(parts, point.Value.B)
|
|
320
|
+
end
|
|
321
|
+
return parts
|
|
322
|
+
end,
|
|
323
|
+
unpack = function(p)
|
|
324
|
+
return ColorSequence.new(keypoints(p, 4, function(time, r, g, b)
|
|
325
|
+
return ColorSequenceKeypoint.new(time, Color3.new(r, g, b))
|
|
326
|
+
end))
|
|
327
|
+
end,
|
|
328
|
+
},
|
|
329
|
+
Ray = {
|
|
330
|
+
pack = function(v)
|
|
331
|
+
return { v.Origin.X, v.Origin.Y, v.Origin.Z, v.Direction.X, v.Direction.Y, v.Direction.Z }
|
|
332
|
+
end,
|
|
333
|
+
unpack = function(p)
|
|
334
|
+
local ox, oy, oz, dx, dy, dz = numbers(p, 6)
|
|
335
|
+
return Ray.new(Vector3.new(ox, oy, oz), Vector3.new(dx, dy, dz))
|
|
336
|
+
end,
|
|
337
|
+
},
|
|
338
|
+
Region3 = {
|
|
339
|
+
pack = function(v)
|
|
340
|
+
local min, max = v.CFrame.Position - v.Size / 2, v.CFrame.Position + v.Size / 2
|
|
341
|
+
return { min.X, min.Y, min.Z, max.X, max.Y, max.Z }
|
|
342
|
+
end,
|
|
343
|
+
unpack = function(p)
|
|
344
|
+
local x0, y0, z0, x1, y1, z1 = numbers(p, 6)
|
|
345
|
+
return Region3.new(Vector3.new(x0, y0, z0), Vector3.new(x1, y1, z1))
|
|
346
|
+
end,
|
|
347
|
+
},
|
|
348
|
+
PhysicalProperties = {
|
|
349
|
+
pack = function(v)
|
|
350
|
+
return { v.Density, v.Friction, v.Elasticity, v.FrictionWeight, v.ElasticityWeight }
|
|
351
|
+
end,
|
|
352
|
+
unpack = function(p)
|
|
353
|
+
return PhysicalProperties.new(numbers(p, 5))
|
|
354
|
+
end,
|
|
355
|
+
},
|
|
356
|
+
DateTime = {
|
|
357
|
+
pack = function(v)
|
|
358
|
+
return { v.UnixTimestampMillis }
|
|
359
|
+
end,
|
|
360
|
+
unpack = function(p)
|
|
361
|
+
return DateTime.fromUnixTimestampMillis(numbers(p, 1))
|
|
362
|
+
end,
|
|
363
|
+
},
|
|
364
|
+
-- By its enum's name and its own: Enum.Material.Plastic is ["Material", "Plastic"]
|
|
365
|
+
EnumItem = {
|
|
366
|
+
pack = function(v)
|
|
367
|
+
return { tostring(v.EnumType), v.Name }
|
|
368
|
+
end,
|
|
369
|
+
unpack = function(p)
|
|
370
|
+
if #p ~= 2 or type(p[1]) ~= "string" or type(p[2]) ~= "string" then
|
|
371
|
+
error("not an enum item", 0)
|
|
372
|
+
end
|
|
373
|
+
return Enum[p[1]][p[2]]
|
|
374
|
+
end,
|
|
375
|
+
},
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
local function datatypeOf(value)
|
|
379
|
+
local tagged = value[DATATYPE]
|
|
380
|
+
if type(tagged) ~= "table" or type(tagged[1]) ~= "string" or type(tagged[2]) ~= "table" then
|
|
381
|
+
return nil
|
|
382
|
+
end
|
|
383
|
+
local datatype = DATATYPES[tagged[1]]
|
|
384
|
+
if not datatype then
|
|
385
|
+
return nil
|
|
386
|
+
end
|
|
387
|
+
local ok, made = pcall(datatype.unpack, tagged[2])
|
|
388
|
+
return if ok and typeof(made) == tagged[1] then made else nil
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
local function packValue(value, depth)
|
|
392
|
+
local kind = typeof(value)
|
|
393
|
+
if kind == "Instance" then
|
|
394
|
+
local path = pathOf(value)
|
|
395
|
+
return if path then { [INSTANCE] = path } else { [NIL] = true }
|
|
396
|
+
end
|
|
397
|
+
local datatype = DATATYPES[kind]
|
|
398
|
+
if datatype then
|
|
399
|
+
return { [DATATYPE] = { kind, datatype.pack(value) } }
|
|
400
|
+
end
|
|
401
|
+
if type(value) ~= "table" then
|
|
402
|
+
return value
|
|
403
|
+
end
|
|
404
|
+
if depth >= MAX_DEPTH then
|
|
405
|
+
error(("bridge: a table nested deeper than %d, or inside itself"):format(MAX_DEPTH), 0)
|
|
406
|
+
end
|
|
407
|
+
local out = {}
|
|
408
|
+
for key, item in value do
|
|
409
|
+
if type(key) == "string" and string.sub(key, 1, 1) == TAG then
|
|
410
|
+
key = TAG .. key
|
|
411
|
+
end
|
|
412
|
+
out[key] = packValue(item, depth + 1)
|
|
413
|
+
end
|
|
414
|
+
return out
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
local function unpackValue(value, depth)
|
|
418
|
+
if type(value) ~= "table" then
|
|
419
|
+
return value
|
|
420
|
+
end
|
|
421
|
+
if depth >= MAX_DEPTH then
|
|
422
|
+
error("too deep", 0)
|
|
423
|
+
end
|
|
424
|
+
local only = next(value)
|
|
425
|
+
if only ~= nil and next(value, only) == nil then
|
|
426
|
+
if only == INSTANCE then
|
|
427
|
+
return instanceAt(value[INSTANCE])
|
|
428
|
+
elseif only == DATATYPE then
|
|
429
|
+
return datatypeOf(value)
|
|
430
|
+
elseif only == NIL then
|
|
431
|
+
return nil
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
local out = {}
|
|
435
|
+
for key, item in value do
|
|
436
|
+
if type(key) == "string" and string.sub(key, 1, 2) == TAG .. TAG then
|
|
437
|
+
key = string.sub(key, 2)
|
|
438
|
+
end
|
|
439
|
+
out[key] = unpackValue(item, depth + 1)
|
|
440
|
+
end
|
|
441
|
+
return out
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
-- Arguments, as a JSON array with no holes: `args` as table.pack makes it
|
|
445
|
+
local function encodeArgs(args)
|
|
446
|
+
local packed = table.create(args.n)
|
|
447
|
+
for i = 1, args.n do
|
|
448
|
+
local value = packValue(args[i], 0)
|
|
449
|
+
packed[i] = if value == nil then { [NIL] = true } else value
|
|
450
|
+
end
|
|
451
|
+
return HttpService:JSONEncode(packed)
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
-- One message, as it travels: `[name, "[...arguments]"]`
|
|
455
|
+
local function encodeMessage(what, name, ...)
|
|
456
|
+
if type(name) ~= "string" then
|
|
457
|
+
error(("bridge.%s: the name is a string, not %s"):format(what, typeof(name)), 3)
|
|
458
|
+
end
|
|
459
|
+
local args = table.pack(...)
|
|
460
|
+
if args.n > MAX_ARGS then
|
|
461
|
+
error(("bridge.%s: '%s' has %d arguments, over %d"):format(what, name, args.n, MAX_ARGS), 3)
|
|
462
|
+
end
|
|
463
|
+
return HttpService:JSONEncode({ name, encodeArgs(args) })
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
-- What a handler is called with, from a message's argument text; an error if it
|
|
467
|
+
-- is not one the bridge made
|
|
468
|
+
local function decodeArgs(text)
|
|
469
|
+
local raw = HttpService:JSONDecode(text)
|
|
470
|
+
if type(raw) ~= "table" or #raw > MAX_ARGS then
|
|
471
|
+
error("not an argument list", 0)
|
|
472
|
+
end
|
|
473
|
+
local args = { n = #raw }
|
|
474
|
+
for i = 1, args.n do
|
|
475
|
+
args[i] = unpackValue(raw[i], 0)
|
|
476
|
+
end
|
|
477
|
+
return args
|
|
478
|
+
end
|
|
479
|
+
|
|
113
480
|
--------------------------------------------------------------------------------
|
|
114
481
|
-- Handlers
|
|
115
482
|
--------------------------------------------------------------------------------
|
|
116
483
|
|
|
117
484
|
-- Every handler of a name runs. What comes before there is one is held for it, as
|
|
118
485
|
-- a remote holds events until something connects: the module connects as it
|
|
119
|
-
-- loads, likely before the file with the handler has.
|
|
120
|
-
|
|
121
|
-
|
|
486
|
+
-- loads, likely before the file with the handler has. `malformed` is what a
|
|
487
|
+
-- message's arguments the bridge did not make are, on that side.
|
|
488
|
+
local function newInbox(malformed)
|
|
489
|
+
return { handlers = {}, waiting = {}, held = 0, noted = {}, full = false, malformed = malformed }
|
|
122
490
|
end
|
|
123
491
|
|
|
124
|
-
|
|
492
|
+
-- A handler's own limits, from the third argument of onServer/onClient
|
|
493
|
+
local function newEntry(what, name, handler, options)
|
|
494
|
+
if type(name) ~= "string" then
|
|
495
|
+
error(("bridge.%s: the name is a string, not %s"):format(what, typeof(name)), 0)
|
|
496
|
+
end
|
|
497
|
+
if options ~= nil and type(options) ~= "table" then
|
|
498
|
+
error(("bridge.%s('%s'): the options are a table, not %s"):format(what, name, typeof(options)), 0)
|
|
499
|
+
end
|
|
500
|
+
local entry = { handler = handler, rateLimit = 0, maxPacket = 0, buckets = setmetatable({}, { __mode = "k" }), warned = false }
|
|
501
|
+
for _, key in { "rateLimit", "maxPacket" } do
|
|
502
|
+
local value = options and options[key]
|
|
503
|
+
if value ~= nil then
|
|
504
|
+
if type(value) ~= "number" or value < 0 or value ~= value then
|
|
505
|
+
error(("bridge.%s('%s'): %s is a number from 0, not %s"):format(what, name, key, tostring(value)), 0)
|
|
506
|
+
end
|
|
507
|
+
entry[key] = value
|
|
508
|
+
end
|
|
509
|
+
end
|
|
510
|
+
return entry
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
-- Within the handler's limits: no larger than its maxPacket, and no more of them
|
|
514
|
+
-- from this sender this second than its rateLimit. The rest is dropped.
|
|
515
|
+
local function admits(entry, name, message)
|
|
516
|
+
if entry.maxPacket > 0 and #message.text > entry.maxPacket then
|
|
517
|
+
if not entry.warned then
|
|
518
|
+
entry.warned = true
|
|
519
|
+
warn(("[bridge] a '%s' of %d bytes is over its handler's maxPacket of %d, so it and those after it are dropped"):format(name, #message.text, entry.maxPacket))
|
|
520
|
+
end
|
|
521
|
+
return false
|
|
522
|
+
end
|
|
523
|
+
if entry.rateLimit > 0 then
|
|
524
|
+
local key = message.from or false
|
|
525
|
+
local now = os.clock()
|
|
526
|
+
local bucket = entry.buckets[key]
|
|
527
|
+
if not bucket or now - bucket.window >= 1 then
|
|
528
|
+
bucket = { window = now, count = 0 }
|
|
529
|
+
entry.buckets[key] = bucket
|
|
530
|
+
end
|
|
531
|
+
bucket.count += 1
|
|
532
|
+
if bucket.count > entry.rateLimit then
|
|
533
|
+
return false
|
|
534
|
+
end
|
|
535
|
+
end
|
|
536
|
+
return true
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
-- Decoded once, for the first handler that admits it
|
|
540
|
+
local function run(inbox, entry, message)
|
|
541
|
+
if not admits(entry, message.name, message) then
|
|
542
|
+
return
|
|
543
|
+
end
|
|
544
|
+
if message.args == nil then
|
|
545
|
+
local ok, args = pcall(decodeArgs, message.text)
|
|
546
|
+
message.args = ok and args
|
|
547
|
+
if not ok then
|
|
548
|
+
inbox.malformed(message.from, message.name)
|
|
549
|
+
end
|
|
550
|
+
end
|
|
551
|
+
local args = message.args
|
|
552
|
+
if not args then
|
|
553
|
+
return
|
|
554
|
+
end
|
|
555
|
+
if message.from ~= nil then
|
|
556
|
+
task.spawn(entry.handler, message.from, table.unpack(args, 1, args.n))
|
|
557
|
+
else
|
|
558
|
+
task.spawn(entry.handler, table.unpack(args, 1, args.n))
|
|
559
|
+
end
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
-- `from` is the player on the server, nil on the client
|
|
563
|
+
local function deliver(inbox, from, name, text)
|
|
564
|
+
local message = { from = from, name = name, text = text }
|
|
125
565
|
local handlers = inbox.handlers[name]
|
|
126
566
|
if handlers then
|
|
127
|
-
for _,
|
|
128
|
-
|
|
567
|
+
for _, entry in handlers do
|
|
568
|
+
run(inbox, entry, message)
|
|
129
569
|
end
|
|
130
570
|
return
|
|
131
571
|
end
|
|
132
572
|
if inbox.held >= HOLD_LIMIT then
|
|
133
573
|
if not inbox.full then
|
|
134
574
|
inbox.full = true
|
|
135
|
-
warn(("[bridge] %d messages wait for handlers, so '%s' and those after it are dropped"):format(HOLD_LIMIT,
|
|
575
|
+
warn(("[bridge] %d messages wait for handlers, so '%s' and those after it are dropped"):format(HOLD_LIMIT, name))
|
|
136
576
|
end
|
|
137
577
|
return
|
|
138
578
|
end
|
|
139
579
|
inbox.held += 1
|
|
140
|
-
table.insert(inbox.waiting,
|
|
580
|
+
table.insert(inbox.waiting, message)
|
|
141
581
|
if not inbox.noted[name] then
|
|
142
582
|
inbox.noted[name] = true
|
|
143
583
|
task.delay(UNHANDLED_AFTER, function()
|
|
144
584
|
if not inbox.handlers[name] then
|
|
145
|
-
warn(("[bridge] nothing handles '%s'"):format(
|
|
585
|
+
warn(("[bridge] nothing handles '%s'"):format(name))
|
|
146
586
|
end
|
|
147
587
|
end)
|
|
148
588
|
end
|
|
149
589
|
end
|
|
150
590
|
|
|
151
|
-
local function listen(inbox, name, handler)
|
|
591
|
+
local function listen(inbox, what, name, handler, options)
|
|
592
|
+
local entry = newEntry(what, name, handler, options)
|
|
152
593
|
local handlers = inbox.handlers[name]
|
|
153
594
|
if handlers then
|
|
154
|
-
table.insert(handlers,
|
|
595
|
+
table.insert(handlers, entry)
|
|
155
596
|
return
|
|
156
597
|
end
|
|
157
|
-
inbox.handlers[name] = {
|
|
598
|
+
inbox.handlers[name] = { entry }
|
|
158
599
|
local still = {}
|
|
159
600
|
for _, message in inbox.waiting do
|
|
160
601
|
if message.name == name then
|
|
161
|
-
|
|
602
|
+
run(inbox, entry, message)
|
|
162
603
|
else
|
|
163
604
|
table.insert(still, message)
|
|
164
605
|
end
|
|
@@ -172,21 +613,229 @@ end
|
|
|
172
613
|
-- The server's side
|
|
173
614
|
--------------------------------------------------------------------------------
|
|
174
615
|
|
|
175
|
-
local serverInbox = newInbox()
|
|
176
|
-
|
|
177
|
-
|
|
616
|
+
local serverInbox = newInbox(function(player)
|
|
617
|
+
serverBreach(player, "a packet out of shape")
|
|
618
|
+
end)
|
|
619
|
+
local clientInbox = newInbox(function(_, name)
|
|
620
|
+
warn(("[bridge] the server sent a '%s' the bridge could not read"):format(name))
|
|
621
|
+
end)
|
|
622
|
+
local packetRemote, keyRemote, invokeRemote
|
|
623
|
+
|
|
624
|
+
-- What answers `bridge.invoke`: one handler a name. An invoke that comes before
|
|
625
|
+
-- it waits for it, as long as a message would wait to be warned about.
|
|
626
|
+
local invokeHandlers = {}
|
|
627
|
+
local invokesWaiting = 0
|
|
628
|
+
|
|
629
|
+
-- What the server sends this frame, in order: `to` a player, or false for all
|
|
630
|
+
local serverOutbox = {}
|
|
631
|
+
|
|
632
|
+
local function flushServer()
|
|
633
|
+
if #serverOutbox == 0 then
|
|
634
|
+
return
|
|
635
|
+
end
|
|
636
|
+
local queued = serverOutbox
|
|
637
|
+
serverOutbox = {}
|
|
638
|
+
local everyone = true
|
|
639
|
+
for _, item in queued do
|
|
640
|
+
if item.to then
|
|
641
|
+
everyone = false
|
|
642
|
+
break
|
|
643
|
+
end
|
|
644
|
+
end
|
|
645
|
+
if everyone then
|
|
646
|
+
local texts = table.create(#queued)
|
|
647
|
+
for i, item in queued do
|
|
648
|
+
texts[i] = item.text
|
|
649
|
+
end
|
|
650
|
+
packetRemote:FireAllClients("[" .. table.concat(texts, ",") .. "]")
|
|
651
|
+
return
|
|
652
|
+
end
|
|
653
|
+
-- One packet a player, still in the order they were sent
|
|
654
|
+
for _, player in Players:GetPlayers() do
|
|
655
|
+
local texts = {}
|
|
656
|
+
for _, item in queued do
|
|
657
|
+
if not item.to or item.to == player then
|
|
658
|
+
table.insert(texts, item.text)
|
|
659
|
+
end
|
|
660
|
+
end
|
|
661
|
+
if #texts > 0 then
|
|
662
|
+
packetRemote:FireClient(player, "[" .. table.concat(texts, ",") .. "]")
|
|
663
|
+
end
|
|
664
|
+
end
|
|
665
|
+
end
|
|
178
666
|
|
|
179
667
|
local sessions = {}
|
|
180
668
|
|
|
181
669
|
local function sessionOf(player)
|
|
182
670
|
local session = sessions[player]
|
|
183
671
|
if not session then
|
|
184
|
-
|
|
672
|
+
-- Packets and invokes are numbered apart: an invoke waits for its answer
|
|
673
|
+
-- while the frame's packets go out, so neither can say which came first
|
|
674
|
+
session = {
|
|
675
|
+
key = crypto.randomBytes(32),
|
|
676
|
+
given = false,
|
|
677
|
+
last = 0,
|
|
678
|
+
lastInvoke = 0,
|
|
679
|
+
honeyEvent = 0,
|
|
680
|
+
honeyFunction = 0,
|
|
681
|
+
window = 0,
|
|
682
|
+
count = 0,
|
|
683
|
+
}
|
|
185
684
|
sessions[player] = session
|
|
186
685
|
end
|
|
187
686
|
return session
|
|
188
687
|
end
|
|
189
688
|
|
|
689
|
+
-- The text a blob sealed under this session's key holds, or nil
|
|
690
|
+
local function unseal(session, blob)
|
|
691
|
+
local ok, raw = pcall(crypto.base64.decode, blob)
|
|
692
|
+
local sealed = ok and #raw >= 28 and {
|
|
693
|
+
iv = string.sub(raw, 1, 12),
|
|
694
|
+
ciphertext = string.sub(raw, 13, -17),
|
|
695
|
+
tag = string.sub(raw, -16),
|
|
696
|
+
}
|
|
697
|
+
return sealed and crypto.aesGcm.decrypt(session.key, sealed) or nil
|
|
698
|
+
end
|
|
699
|
+
|
|
700
|
+
-- Two of the decoys are honeypots: the game's own client fires one beside every
|
|
701
|
+
-- packet and invokes the other beside every invoke, sealed and numbered on a
|
|
702
|
+
-- count of each, so a spy sees them carry traffic like the real ones. What comes
|
|
703
|
+
-- in with the next number is that and is let be; anything else — a blob it did
|
|
704
|
+
-- not seal, a number out of turn, a replay — is someone firing what they saw.
|
|
705
|
+
local HONEYPOT_MAX = 512
|
|
706
|
+
|
|
707
|
+
local function honeypot(player, blob, field, touched)
|
|
708
|
+
local session = sessions[player]
|
|
709
|
+
if not session or not session.given or type(blob) ~= "string" or #blob > HONEYPOT_MAX then
|
|
710
|
+
serverBreach(player, touched)
|
|
711
|
+
return
|
|
712
|
+
end
|
|
713
|
+
local text = unseal(session, blob)
|
|
714
|
+
if not text then
|
|
715
|
+
serverBreach(player, touched)
|
|
716
|
+
return
|
|
717
|
+
end
|
|
718
|
+
local decoded, packet = pcall(HttpService.JSONDecode, HttpService, text)
|
|
719
|
+
if not decoded or type(packet) ~= "table" or packet.h ~= session[field] + 1 then
|
|
720
|
+
serverBreach(player, "replayed a honeypot")
|
|
721
|
+
return
|
|
722
|
+
end
|
|
723
|
+
session[field] = packet.h
|
|
724
|
+
end
|
|
725
|
+
|
|
726
|
+
-- A client's sealed packet, opened: the JSON in it, or nil when it is a breach or
|
|
727
|
+
-- over the rate. `last` is the session's field its number has to be past.
|
|
728
|
+
-- `shaped` says whether what is in it is what the remote takes.
|
|
729
|
+
local function openPacket(player, blob, last, shaped)
|
|
730
|
+
local session = sessions[player]
|
|
731
|
+
if not session or not session.given or type(blob) ~= "string" then
|
|
732
|
+
serverBreach(player, "a packet without a session")
|
|
733
|
+
return nil
|
|
734
|
+
end
|
|
735
|
+
-- Both before any of the crypto, which is what a flood would cost. Past the
|
|
736
|
+
-- rate a packet is dropped, not a breach: a game can burst.
|
|
737
|
+
if RATE_LIMIT > 0 then
|
|
738
|
+
local now = os.clock()
|
|
739
|
+
if now - session.window >= 1 then
|
|
740
|
+
session.window = now
|
|
741
|
+
session.count = 0
|
|
742
|
+
end
|
|
743
|
+
session.count += 1
|
|
744
|
+
if session.count > RATE_LIMIT then
|
|
745
|
+
return nil
|
|
746
|
+
end
|
|
747
|
+
end
|
|
748
|
+
if MAX_PACKET > 0 and #blob > MAX_PACKET then
|
|
749
|
+
serverBreach(player, "a packet over bridgeMaxPacket")
|
|
750
|
+
return nil
|
|
751
|
+
end
|
|
752
|
+
local text = unseal(session, blob)
|
|
753
|
+
if not text then
|
|
754
|
+
serverBreach(player, "a packet the bridge did not seal")
|
|
755
|
+
return nil
|
|
756
|
+
end
|
|
757
|
+
local decoded, packet = pcall(HttpService.JSONDecode, HttpService, text)
|
|
758
|
+
if not decoded or type(packet) ~= "table" or type(packet.s) ~= "number" or not shaped(packet) then
|
|
759
|
+
serverBreach(player, "a packet out of shape")
|
|
760
|
+
return nil
|
|
761
|
+
end
|
|
762
|
+
if packet.s <= session[last] then
|
|
763
|
+
serverBreach(player, "a packet sent twice")
|
|
764
|
+
return nil
|
|
765
|
+
end
|
|
766
|
+
session[last] = packet.s
|
|
767
|
+
return packet
|
|
768
|
+
end
|
|
769
|
+
|
|
770
|
+
local function isMessage(message)
|
|
771
|
+
return type(message) == "table" and type(message[1]) == "string" and type(message[2]) == "string"
|
|
772
|
+
end
|
|
773
|
+
|
|
774
|
+
local function isEventPacket(packet)
|
|
775
|
+
if type(packet.b) == "string" then
|
|
776
|
+
return true
|
|
777
|
+
end
|
|
778
|
+
if type(packet.m) ~= "table" then
|
|
779
|
+
return false
|
|
780
|
+
end
|
|
781
|
+
for _, message in packet.m do
|
|
782
|
+
if not isMessage(message) then
|
|
783
|
+
return false
|
|
784
|
+
end
|
|
785
|
+
end
|
|
786
|
+
return true
|
|
787
|
+
end
|
|
788
|
+
|
|
789
|
+
local function isInvokePacket(packet)
|
|
790
|
+
return isMessage(packet.i)
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
-- An invoke's answer: true and the result as an argument list, or false and why
|
|
794
|
+
-- not. Why is the bridge's own words: a handler's error stays on the server.
|
|
795
|
+
local function answerInvoke(player, blob)
|
|
796
|
+
local packet = openPacket(player, blob, "lastInvoke", isInvokePacket)
|
|
797
|
+
if not packet then
|
|
798
|
+
return nil
|
|
799
|
+
end
|
|
800
|
+
local name, text = packet.i[1], packet.i[2]
|
|
801
|
+
local entry = invokeHandlers[name]
|
|
802
|
+
if not entry then
|
|
803
|
+
if invokesWaiting >= HOLD_LIMIT then
|
|
804
|
+
return false, "found no handler, with too many invokes waiting"
|
|
805
|
+
end
|
|
806
|
+
invokesWaiting += 1
|
|
807
|
+
local waited = 0
|
|
808
|
+
while not invokeHandlers[name] and waited < UNHANDLED_AFTER do
|
|
809
|
+
waited += task.wait(0.1)
|
|
810
|
+
end
|
|
811
|
+
invokesWaiting -= 1
|
|
812
|
+
entry = invokeHandlers[name]
|
|
813
|
+
if not entry then
|
|
814
|
+
warn(("[bridge] nothing answers the invoke '%s'"):format(name))
|
|
815
|
+
return false, "has no handler on the server"
|
|
816
|
+
end
|
|
817
|
+
end
|
|
818
|
+
if not admits(entry, name, { from = player, name = name, text = text }) then
|
|
819
|
+
return false, "is over its handler's limits"
|
|
820
|
+
end
|
|
821
|
+
local decoded, args = pcall(decodeArgs, text)
|
|
822
|
+
if not decoded then
|
|
823
|
+
serverBreach(player, "a packet out of shape")
|
|
824
|
+
return nil
|
|
825
|
+
end
|
|
826
|
+
local ran, result = pcall(entry.handler, player, table.unpack(args, 1, args.n))
|
|
827
|
+
if not ran then
|
|
828
|
+
warn(("[bridge] the handler of the invoke '%s' failed: %s"):format(name, tostring(result)))
|
|
829
|
+
return false, "failed on the server"
|
|
830
|
+
end
|
|
831
|
+
local encoded, answer = pcall(encodeArgs, table.pack(result))
|
|
832
|
+
if not encoded then
|
|
833
|
+
warn(("[bridge] the answer to the invoke '%s' could not be sent: %s"):format(name, tostring(answer)))
|
|
834
|
+
return false, "had an answer the bridge could not send"
|
|
835
|
+
end
|
|
836
|
+
return true, answer
|
|
837
|
+
end
|
|
838
|
+
|
|
190
839
|
local function startServer()
|
|
191
840
|
if script:FindFirstChildOfClass("Folder") then
|
|
192
841
|
error(ONE_VM, 0)
|
|
@@ -199,7 +848,10 @@ local function startServer()
|
|
|
199
848
|
packetRemote.Name = remoteName(salt, "packet")
|
|
200
849
|
keyRemote = Instance.new("RemoteFunction")
|
|
201
850
|
keyRemote.Name = remoteName(salt, "key")
|
|
202
|
-
|
|
851
|
+
invokeRemote = Instance.new("RemoteFunction")
|
|
852
|
+
invokeRemote.Name = remoteName(salt, "invoke")
|
|
853
|
+
invokeRemote.OnServerInvoke = answerInvoke
|
|
854
|
+
local remotes = { packetRemote, keyRemote, invokeRemote }
|
|
203
855
|
|
|
204
856
|
-- The key, once per player: the game's own script asks for it as it loads,
|
|
205
857
|
-- before an executor can, so a second ask is one
|
|
@@ -214,78 +866,48 @@ local function startServer()
|
|
|
214
866
|
end
|
|
215
867
|
|
|
216
868
|
packetRemote.OnServerEvent:Connect(function(player, blob)
|
|
217
|
-
local
|
|
218
|
-
if not
|
|
219
|
-
serverBreach(player, "a packet without a session")
|
|
220
|
-
return
|
|
221
|
-
end
|
|
222
|
-
-- Both before any of the crypto, which is what a flood would cost. Past
|
|
223
|
-
-- the rate a packet is dropped, not a breach: a game can burst.
|
|
224
|
-
if RATE_LIMIT > 0 then
|
|
225
|
-
local now = os.clock()
|
|
226
|
-
if now - session.window >= 1 then
|
|
227
|
-
session.window = now
|
|
228
|
-
session.count = 0
|
|
229
|
-
end
|
|
230
|
-
session.count += 1
|
|
231
|
-
if session.count > RATE_LIMIT then
|
|
232
|
-
return
|
|
233
|
-
end
|
|
234
|
-
end
|
|
235
|
-
if MAX_PACKET > 0 and #blob > MAX_PACKET then
|
|
236
|
-
serverBreach(player, "a packet over bridgeMaxPacket")
|
|
869
|
+
local packet = openPacket(player, blob, "last", isEventPacket)
|
|
870
|
+
if not packet then
|
|
237
871
|
return
|
|
238
872
|
end
|
|
239
|
-
local ok, raw = pcall(crypto.base64.decode, blob)
|
|
240
|
-
local sealed = ok and #raw >= 28 and {
|
|
241
|
-
iv = string.sub(raw, 1, 12),
|
|
242
|
-
ciphertext = string.sub(raw, 13, -17),
|
|
243
|
-
tag = string.sub(raw, -16),
|
|
244
|
-
}
|
|
245
|
-
local text = sealed and crypto.aesGcm.decrypt(session.key, sealed)
|
|
246
|
-
if not text then
|
|
247
|
-
serverBreach(player, "a packet the bridge did not seal")
|
|
248
|
-
return
|
|
249
|
-
end
|
|
250
|
-
local decoded, packet = pcall(HttpService.JSONDecode, HttpService, text)
|
|
251
|
-
if
|
|
252
|
-
not decoded
|
|
253
|
-
or type(packet) ~= "table"
|
|
254
|
-
or type(packet.s) ~= "number"
|
|
255
|
-
or (type(packet.n) ~= "string" and type(packet.b) ~= "string")
|
|
256
|
-
then
|
|
257
|
-
serverBreach(player, "a packet out of shape")
|
|
258
|
-
return
|
|
259
|
-
end
|
|
260
|
-
if packet.s <= session.last then
|
|
261
|
-
serverBreach(player, "a packet sent twice")
|
|
262
|
-
return
|
|
263
|
-
end
|
|
264
|
-
session.last = packet.s
|
|
265
873
|
if type(packet.b) == "string" then
|
|
266
874
|
serverBreach(player, "client: " .. packet.b)
|
|
267
875
|
return
|
|
268
876
|
end
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
877
|
+
for _, message in packet.m do
|
|
878
|
+
deliver(serverInbox, player, message[1], message[2])
|
|
879
|
+
end
|
|
272
880
|
end)
|
|
273
881
|
|
|
274
882
|
-- Decoys: nothing the game sends goes through them, so anything that does is
|
|
275
|
-
-- an executor firing every remote it can find
|
|
276
|
-
|
|
883
|
+
-- an executor firing every remote it can find. The first of each kind is a
|
|
884
|
+
-- honeypot, named by the salt so the client finds it; to anything else it is
|
|
885
|
+
-- one more decoy.
|
|
886
|
+
for n = 1, DECOYS do
|
|
277
887
|
local decoy = Instance.new("RemoteEvent")
|
|
278
|
-
decoy.Name = randomName()
|
|
279
|
-
decoy.OnServerEvent:Connect(function(player)
|
|
280
|
-
serverBreach(player, "fired a decoy")
|
|
281
|
-
end)
|
|
282
|
-
table.insert(remotes, decoy)
|
|
283
888
|
local decoyFunction = Instance.new("RemoteFunction")
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
889
|
+
if n == 1 then
|
|
890
|
+
decoy.Name = remoteName(salt, "honeypotEvent")
|
|
891
|
+
decoy.OnServerEvent:Connect(function(player, blob)
|
|
892
|
+
honeypot(player, blob, "honeyEvent", "fired a decoy")
|
|
893
|
+
end)
|
|
894
|
+
decoyFunction.Name = remoteName(salt, "honeypotFunction")
|
|
895
|
+
decoyFunction.OnServerInvoke = function(player, blob)
|
|
896
|
+
honeypot(player, blob, "honeyFunction", "invoked a decoy")
|
|
897
|
+
return nil
|
|
898
|
+
end
|
|
899
|
+
else
|
|
900
|
+
decoy.Name = randomName()
|
|
901
|
+
decoy.OnServerEvent:Connect(function(player)
|
|
902
|
+
serverBreach(player, "fired a decoy")
|
|
903
|
+
end)
|
|
904
|
+
decoyFunction.Name = randomName()
|
|
905
|
+
decoyFunction.OnServerInvoke = function(player)
|
|
906
|
+
serverBreach(player, "invoked a decoy")
|
|
907
|
+
return nil
|
|
908
|
+
end
|
|
288
909
|
end
|
|
910
|
+
table.insert(remotes, decoy)
|
|
289
911
|
table.insert(remotes, decoyFunction)
|
|
290
912
|
end
|
|
291
913
|
|
|
@@ -302,6 +924,7 @@ local function startServer()
|
|
|
302
924
|
Players.PlayerRemoving:Connect(function(player)
|
|
303
925
|
sessions[player] = nil
|
|
304
926
|
end)
|
|
927
|
+
RunService.Heartbeat:Connect(flushServer)
|
|
305
928
|
end
|
|
306
929
|
|
|
307
930
|
--------------------------------------------------------------------------------
|
|
@@ -310,19 +933,68 @@ end
|
|
|
310
933
|
|
|
311
934
|
local clientKey
|
|
312
935
|
local sequence = 0
|
|
936
|
+
local invokeSequence = 0
|
|
937
|
+
local honeypotEvent, honeypotFunction
|
|
938
|
+
local honeyEventId, honeyFunctionId = 0, 0
|
|
313
939
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
940
|
+
-- What the client sends this frame, each message already JSON
|
|
941
|
+
local clientOutbox = {}
|
|
942
|
+
|
|
943
|
+
local function seal(text)
|
|
318
944
|
local sealed = crypto.aesGcm.encrypt(clientKey, text)
|
|
319
945
|
return crypto.base64.encode(sealed.iv .. sealed.ciphertext .. sealed.tag)
|
|
320
946
|
end
|
|
321
947
|
|
|
948
|
+
-- How large a packet of messages this long is sealed, at most: the sequence
|
|
949
|
+
-- number and the JSON around them, the IV and the tag, in base64
|
|
950
|
+
local function sealedSize(length)
|
|
951
|
+
return 4 * math.ceil((length + 64) / 3)
|
|
952
|
+
end
|
|
953
|
+
|
|
954
|
+
local function sealMessages(texts)
|
|
955
|
+
sequence += 1
|
|
956
|
+
return seal('{"s":' .. sequence .. ',"m":[' .. table.concat(texts, ",") .. "]}")
|
|
957
|
+
end
|
|
958
|
+
|
|
959
|
+
-- A honeypot's next number, sealed, with as much padding as a packet might have
|
|
960
|
+
local function honey(id)
|
|
961
|
+
local padding = string.sub(crypto.randomHash().hex, 1, 8 + string.byte(crypto.randomBytes(1)) % 56)
|
|
962
|
+
return seal(HttpService:JSONEncode({ h = id, p = padding }))
|
|
963
|
+
end
|
|
964
|
+
|
|
965
|
+
-- The packet remote fired, and the event honeypot beside it
|
|
966
|
+
local function fire(blob)
|
|
967
|
+
packetRemote:FireServer(blob)
|
|
968
|
+
if honeypotEvent then
|
|
969
|
+
honeyEventId += 1
|
|
970
|
+
honeypotEvent:FireServer(honey(honeyEventId))
|
|
971
|
+
end
|
|
972
|
+
end
|
|
973
|
+
|
|
974
|
+
-- One packet for the frame, or as few as fit under bridgeMaxPacket
|
|
975
|
+
local function flushClient()
|
|
976
|
+
if #clientOutbox == 0 or not clientKey then
|
|
977
|
+
return
|
|
978
|
+
end
|
|
979
|
+
local queued = clientOutbox
|
|
980
|
+
clientOutbox = {}
|
|
981
|
+
local batch, length = {}, 0
|
|
982
|
+
for _, text in queued do
|
|
983
|
+
if MAX_PACKET > 0 and #batch > 0 and sealedSize(length + #text) > MAX_PACKET then
|
|
984
|
+
fire(sealMessages(batch))
|
|
985
|
+
batch, length = {}, 0
|
|
986
|
+
end
|
|
987
|
+
table.insert(batch, text)
|
|
988
|
+
length += #text + 1
|
|
989
|
+
end
|
|
990
|
+
fire(sealMessages(batch))
|
|
991
|
+
end
|
|
992
|
+
|
|
322
993
|
local function clientBreach(reason)
|
|
323
994
|
if clientKey and packetRemote then
|
|
324
995
|
pcall(function()
|
|
325
|
-
|
|
996
|
+
sequence += 1
|
|
997
|
+
fire(seal(HttpService:JSONEncode({ s = sequence, b = reason })))
|
|
326
998
|
end)
|
|
327
999
|
end
|
|
328
1000
|
end
|
|
@@ -342,17 +1014,64 @@ local function startClient()
|
|
|
342
1014
|
local holder = script:FindFirstChildOfClass("Folder")
|
|
343
1015
|
packetRemote = holder:WaitForChild(remoteName(holder.Name, "packet"))
|
|
344
1016
|
keyRemote = holder:WaitForChild(remoteName(holder.Name, "key"))
|
|
1017
|
+
invokeRemote = holder:WaitForChild(remoteName(holder.Name, "invoke"))
|
|
1018
|
+
-- None with no decoys
|
|
1019
|
+
honeypotEvent = holder:FindFirstChild(remoteName(holder.Name, "honeypotEvent"))
|
|
1020
|
+
honeypotFunction = holder:FindFirstChild(remoteName(holder.Name, "honeypotFunction"))
|
|
345
1021
|
script:SetAttribute("BridgeFound", true)
|
|
346
1022
|
clientKey = keyRemote:InvokeServer()
|
|
347
1023
|
|
|
348
|
-
packetRemote.OnClientEvent:Connect(function(
|
|
349
|
-
|
|
1024
|
+
packetRemote.OnClientEvent:Connect(function(blob)
|
|
1025
|
+
local ok, messages = pcall(HttpService.JSONDecode, HttpService, blob)
|
|
1026
|
+
if not ok or type(messages) ~= "table" then
|
|
1027
|
+
warn("[bridge] the server sent a packet the bridge could not read")
|
|
1028
|
+
return
|
|
1029
|
+
end
|
|
1030
|
+
for _, message in messages do
|
|
1031
|
+
if type(message) == "table" and type(message[1]) == "string" and type(message[2]) == "string" then
|
|
1032
|
+
deliver(clientInbox, nil, message[1], message[2])
|
|
1033
|
+
end
|
|
1034
|
+
end
|
|
350
1035
|
end)
|
|
1036
|
+
RunService.Heartbeat:Connect(flushClient)
|
|
351
1037
|
|
|
352
1038
|
-- Once found, the real remotes take names nothing else can look up, in the
|
|
353
1039
|
-- decoys' form
|
|
354
1040
|
packetRemote.Name = randomName()
|
|
355
1041
|
keyRemote.Name = randomName()
|
|
1042
|
+
invokeRemote.Name = randomName()
|
|
1043
|
+
for _, remote in { honeypotEvent, honeypotFunction } do
|
|
1044
|
+
remote.Name = randomName()
|
|
1045
|
+
end
|
|
1046
|
+
end
|
|
1047
|
+
|
|
1048
|
+
-- The server's handler of that name, called and waited for: its answer, or an
|
|
1049
|
+
-- error saying why there is none
|
|
1050
|
+
local function invoke(name, ...)
|
|
1051
|
+
if not clientKey then
|
|
1052
|
+
error("bridge: the server gave this client no key", 2)
|
|
1053
|
+
end
|
|
1054
|
+
local text = encodeMessage("invoke", name, ...)
|
|
1055
|
+
if MAX_PACKET > 0 and sealedSize(#text) > MAX_PACKET then
|
|
1056
|
+
error(("bridge.invoke: '%s' is %d bytes sealed, over bridgeMaxPacket"):format(name, sealedSize(#text)), 2)
|
|
1057
|
+
end
|
|
1058
|
+
-- What was sent before it goes first
|
|
1059
|
+
flushClient()
|
|
1060
|
+
invokeSequence += 1
|
|
1061
|
+
-- Its honeypot on a thread of its own, so the invoke waits for one answer
|
|
1062
|
+
if honeypotFunction then
|
|
1063
|
+
honeyFunctionId += 1
|
|
1064
|
+
task.spawn(honeypotFunction.InvokeServer, honeypotFunction, honey(honeyFunctionId))
|
|
1065
|
+
end
|
|
1066
|
+
local ok, answer = invokeRemote:InvokeServer(seal('{"s":' .. invokeSequence .. ',"i":' .. text .. "}"))
|
|
1067
|
+
if ok ~= true then
|
|
1068
|
+
error(("bridge.invoke: '%s' %s"):format(name, if type(answer) == "string" then answer else "was refused by the server"), 2)
|
|
1069
|
+
end
|
|
1070
|
+
local decoded, result = pcall(decodeArgs, answer)
|
|
1071
|
+
if not decoded then
|
|
1072
|
+
error(("bridge.invoke: '%s' had an answer the bridge could not read"):format(name), 2)
|
|
1073
|
+
end
|
|
1074
|
+
return result[1]
|
|
356
1075
|
end
|
|
357
1076
|
|
|
358
1077
|
if IS_SERVER then
|
|
@@ -376,19 +1095,27 @@ end
|
|
|
376
1095
|
local function makeHandle()
|
|
377
1096
|
if IS_SERVER then
|
|
378
1097
|
return table.freeze({
|
|
379
|
-
onServer = function(name, handler)
|
|
380
|
-
listen(serverInbox, name, handler)
|
|
1098
|
+
onServer = function(name, handler, options)
|
|
1099
|
+
listen(serverInbox, "onServer", name, handler, options)
|
|
381
1100
|
end,
|
|
382
1101
|
sendTo = function(player, name, ...)
|
|
383
|
-
|
|
1102
|
+
table.insert(serverOutbox, { to = player, text = encodeMessage("sendTo", name, ...) })
|
|
384
1103
|
end,
|
|
385
1104
|
sendAll = function(name, ...)
|
|
386
|
-
|
|
1105
|
+
table.insert(serverOutbox, { to = false, text = encodeMessage("sendAll", name, ...) })
|
|
1106
|
+
end,
|
|
1107
|
+
onInvoke = function(name, handler, options)
|
|
1108
|
+
local entry = newEntry("onInvoke", name, handler, options)
|
|
1109
|
+
if invokeHandlers[name] then
|
|
1110
|
+
error(("bridge.onInvoke: '%s' already has a handler, and an invoke has one answer"):format(name), 0)
|
|
1111
|
+
end
|
|
1112
|
+
invokeHandlers[name] = entry
|
|
387
1113
|
end,
|
|
388
1114
|
onBreach = function(handler)
|
|
389
1115
|
table.insert(breachHandlers, handler)
|
|
390
1116
|
end,
|
|
391
1117
|
send = wrongSide("send"),
|
|
1118
|
+
invoke = wrongSide("invoke"),
|
|
392
1119
|
onClient = wrongSide("onClient"),
|
|
393
1120
|
})
|
|
394
1121
|
end
|
|
@@ -397,18 +1124,18 @@ local function makeHandle()
|
|
|
397
1124
|
if not clientKey then
|
|
398
1125
|
error("bridge: the server gave this client no key", 2)
|
|
399
1126
|
end
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
if MAX_PACKET > 0 and #blob > MAX_PACKET then
|
|
404
|
-
error(("bridge.send: '%s' is %d bytes sealed, over bridgeMaxPacket"):format(tostring(name), #blob), 2)
|
|
1127
|
+
local text = encodeMessage("send", name, ...)
|
|
1128
|
+
if MAX_PACKET > 0 and sealedSize(#text) > MAX_PACKET then
|
|
1129
|
+
error(("bridge.send: '%s' is %d bytes sealed, over bridgeMaxPacket"):format(name, sealedSize(#text)), 2)
|
|
405
1130
|
end
|
|
406
|
-
|
|
1131
|
+
table.insert(clientOutbox, text)
|
|
407
1132
|
end,
|
|
408
|
-
|
|
409
|
-
|
|
1133
|
+
invoke = invoke,
|
|
1134
|
+
onClient = function(name, handler, options)
|
|
1135
|
+
listen(clientInbox, "onClient", name, handler, options)
|
|
410
1136
|
end,
|
|
411
1137
|
onServer = wrongSide("onServer"),
|
|
1138
|
+
onInvoke = wrongSide("onInvoke"),
|
|
412
1139
|
sendTo = wrongSide("sendTo"),
|
|
413
1140
|
sendAll = wrongSide("sendAll"),
|
|
414
1141
|
onBreach = wrongSide("onBreach"),
|