@tilua-types/bridge 1.0.0 → 2.0.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 CHANGED
@@ -40,6 +40,55 @@ Every handler of a name runs, on the server and the client alike, and what comes
40
40
  before the first one waits for it, up to 256 messages. A name still without a
41
41
  handler after ten seconds is warned about.
42
42
 
43
+ What a side sends in one frame goes out together at the next `Heartbeat`, in the
44
+ order it was sent: one sealed packet from a client — or as few as fit under
45
+ `bridgeMaxPacket` — and one remote call to each player from the server, or a
46
+ single `FireAllClients` when the frame held only `sendAll`. So `bridgeRateLimit`
47
+ counts packets, not messages.
48
+
49
+ ### A handler's own limits
50
+
51
+ A handler can take its own limits as a third argument, on top of the config's:
52
+
53
+ ```ts
54
+ bridge.onServer("Chat", (player: Player, text: string) => { ... }, {
55
+ rateLimit: 5, // messages of this name a player may have it run a second
56
+ maxPacket: 512, // the largest message, in bytes of its arguments as JSON
57
+ })
58
+ ```
59
+
60
+ Past either, the message is dropped for that handler alone — before its
61
+ arguments are decoded — and another handler of the name still gets it. The
62
+ first message over `maxPacket` is warned about. Neither is a breach: the client
63
+ does not know a handler's limits, so a fair one can cross them. `onClient` takes
64
+ the same options, counted per handler.
65
+
66
+ ### Asking the server
67
+
68
+ `bridge.invoke` asks the server and waits for its answer, through a RemoteFunction:
69
+
70
+ ```ts
71
+ // server: one handler a name; what it returns goes back
72
+ bridge.onInvoke("Price", (player: Player, item: string) => {
73
+ return prices[item]
74
+ })
75
+
76
+ // client
77
+ const price = bridge.invoke("Price", "sword")
78
+ ```
79
+
80
+ The request is sealed and numbered like a packet, on a count of its own, and
81
+ counts toward `bridgeRateLimit`; the answer comes back plain, to that client
82
+ alone, carrying what arguments carry. What the client sent before the invoke goes
83
+ out first, so the handler sees it. `onInvoke` takes a handler's limits as
84
+ `onServer` does. The handler runs on the invoke's own thread, so it may yield.
85
+
86
+ `invoke` raises when there is no answer: no handler within ten seconds (the invoke
87
+ waits for one, as a message would), over the handler's limits, or the handler
88
+ failed. The client is told only that — the error itself is warned on the server.
89
+ A handler that returns nothing answers `nil`. There is no invoke from the server
90
+ to a client: a client that never answers would hold the server's thread for good.
91
+
43
92
  What cannot run does not build: `bridge` read as a value, a `.client` script
44
93
  calling a server's member or the other way round, and any output but `"files"`.
45
94
 
@@ -69,6 +118,12 @@ calling a server's member or the other way round, and any output but `"files"`.
69
118
  salt: the real remotes are named by it under the secret, in a folder named
70
119
  after it, shuffled among the decoys, so a name kept from another server finds
71
120
  nothing. The client renames the real ones once found, in the decoys' form.
121
+ - **Honeypots.** One decoy RemoteEvent and one decoy RemoteFunction are named by
122
+ the salt too. The game's client fires the first beside every packet and
123
+ invokes the second beside every invoke, each with a number one past the last,
124
+ sealed with AES-GCM, so a spy watching the remotes sees them carry traffic like
125
+ the real ones. The server lets the next number be; anything else through them —
126
+ a blob it did not seal, or a replay of one it did — is a breach.
72
127
 
73
128
  On the client a breach is reported to the server through the bridge.
74
129
 
@@ -76,7 +131,7 @@ On the client a breach is reported to the server through the bridge.
76
131
 
77
132
  With no handler, a breach bans the player for good across the experience, their
78
133
  alt accounts too, through `Players:BanAsync`; a ban that fails is a kick. In Studio
79
- it only warns. A file that loads within five seconds of the breach can still take
134
+ it is a kick that says why, and no ban. A file that loads within five seconds of the breach can still take
80
135
  it. To answer another way, register a handler on the server, which replaces the
81
136
  ban entirely:
82
137
 
@@ -117,9 +172,21 @@ keeps one secret while it runs. A project from before this has a
117
172
  The module runs in one Luau VM: calling `bridge` from an Actor stops with an error
118
173
  saying so.
119
174
 
120
- Client to server, arguments travel as JSON: strings, numbers, booleans, and tables
121
- of them. Not Instances, and no `nil` between arguments. Server to client they are
122
- a remote's arguments, as they are.
175
+ Both ways, arguments travel as JSON: strings, numbers, booleans, `nil` — between
176
+ and after the others too, counted as `select("#", ...)` counts them — and tables
177
+ of them. An Instance travels as its path of names from `game` and is looked up
178
+ again on arrival, the first child of each name: one the other side does not have
179
+ — not replicated, destroyed, or in no game — arrives as `nil`, inside a table as a
180
+ missing key. Roblox's value types travel as their components and are made again
181
+ on arrival: `Vector3`, `Vector2`, `Vector3int16`, `Vector2int16`, `CFrame`,
182
+ `Color3`, `BrickColor`, `UDim`, `UDim2`, `Rect`, `NumberRange`,
183
+ `NumberSequence`, `ColorSequence`, `Ray`, `Region3` (axis-aligned, as
184
+ `Region3.new` makes it), `PhysicalProperties`, `DateTime`, and an `EnumItem` by
185
+ its enum's name and its own. Each is checked part by part where it arrives — as
186
+ many numbers as it takes — and one that does not make the type is `nil`. The rest
187
+ (`Font`, `TweenInfo`, …) is what `JSONEncode` makes of it. A table key that starts
188
+ with `\1` is the bridge's mark, so a key of the game's that does is escaped on the
189
+ way and arrives as it was.
123
190
 
124
191
  The obfuscation hides the secret and every string in the module, but not the
125
192
  names of members it reads (`.OnServerInvoke`, `crypto.aesGcm`): lua-obfuscator
@@ -130,7 +197,8 @@ keeps `a.b`'s `b` as written.
130
197
  `npm test` in the repository compiles a project with it — claims only in the
131
198
  files that call `bridge`, nothing in the runtime or the exports — and runs the
132
199
  module as server and client over a fake network, plain and obfuscated: traffic
133
- both ways, then a late executor making up a token, firing the packet remote with
200
+ both ways, one packet a frame each way in order, invokes answered and refused, Instances, value types and `nil`s found again,
201
+ each handler's limits, then a late executor making up a token, firing the packet remote with
134
202
  its own bytes and with a captured packet, asking for the
135
203
  key, and firing and invoking every decoy. Each is refused and reported. The
136
204
  network is a fake: nothing here has run in Roblox.
@@ -138,12 +206,12 @@ network is a fake: nothing here has run in Roblox.
138
206
  <!-- compatibility:start -->
139
207
  ## Compatibility
140
208
 
141
- `@tilua-types/bridge@1.0.0` works with these tilua packages. *Released with* is the
209
+ `@tilua-types/bridge@2.0.0` works with these tilua packages. *Released with* is the
142
210
  version it was built and tested against.
143
211
 
144
212
  | Package | Range | Released with | |
145
213
  | --- | --- | --- | --- |
146
214
  | `@tilua-types/crypto` | `^1.0.0` | 1.0.0 | dependency |
147
215
  | `@tilua-types/roblox` | `^10.1.2` | 10.1.2 | dependency |
148
- | `@tilua/parser` | `^9.1.2` | 9.1.2 | peer |
216
+ | `@tilua/parser` | `^9.1.2` | 9.1.4 | peer |
149
217
  <!-- compatibility:end -->
package/index.d.tilua CHANGED
@@ -3,36 +3,60 @@
3
3
  --
4
4
  -- -- client
5
5
  -- bridge.send("Buy", "sword", 2)
6
+ -- const price = bridge.invoke("Price", "sword")
6
7
  -- bridge.onClient("Toast", (text: string) => { ... })
7
8
  --
8
9
  -- -- server
9
- -- bridge.onServer("Buy", (player: Player, item: string, count: number) => { ... })
10
+ -- bridge.onServer("Buy", (player: Player, item: string, count: number) => { ... }, { rateLimit: 5 })
10
11
  -- bridge.sendTo(player, "Toast", "bought")
12
+ -- bridge.onInvoke("Price", (player: Player, item: string) => { return 100 })
11
13
  -- bridge.onBreach((player: Player, reason: string) => { player:Kick() })
12
14
  --
13
15
  -- `bridge` is only ever called. Kept in a variable, exported or passed along, it
14
16
  -- does not build: the handle stays in the file that claimed it. Each file that
15
17
  -- calls it claims it as it loads.
16
18
  --
17
- -- Arguments travel as JSON: strings, numbers, booleans, and tables of them.
18
- -- Client to server, each packet is sealed with AES-GCM and numbered.
19
+ -- Arguments travel as JSON: strings, numbers, booleans, `nil`, and tables of
20
+ -- them, and Instances by their path from `game`, found again on arrival — `nil`
21
+ -- where the other side has nothing there. Vector3, CFrame, Color3, UDim2, EnumItem
22
+ -- and Roblox's other value types go by their components and are made again. What
23
+ -- is sent in one frame goes as one
24
+ -- packet at the next Heartbeat. Client to server, each packet is sealed with
25
+ -- AES-GCM and numbered.
26
+
27
+ -- A handler's own limits, on top of the bridge's. Past either, a message is
28
+ -- dropped before its arguments are decoded. 0 or missing for no limit.
29
+ type bridge.HandlerOptions = {
30
+ -- Messages of this name a sender may have this handler run a second
31
+ rateLimit?: number,
32
+ -- The largest message, in bytes of its arguments as JSON
33
+ maxPacket?: number,
34
+ }
19
35
 
20
36
  declare bridge: {
21
37
  -- Client: to the server's handler of that name
22
38
  send: (name: string, ...args: any[]) => void,
39
+ -- Client: the server's onInvoke handler of that name, waited for: what it
40
+ -- returns. An error when there is no answer — no handler within ten
41
+ -- seconds, over its limits, or the handler failed.
42
+ invoke: (name: string, ...args: any[]) => any,
23
43
  -- Client: what the server sends by that name. Every handler of a name runs;
24
44
  -- what comes before the first waits for it.
25
- onClient: (name: string, handler: (...args: any[]) => void) => void,
45
+ onClient: (name: string, handler: (...args: any[]) => void, options?: bridge.HandlerOptions) => void,
26
46
  -- Server: what a client sends by that name, with the player first. Every
27
47
  -- handler of a name runs; what comes before the first waits for it.
28
- onServer: (name: string, handler: (player: Player, ...args: any[]) => void) => void,
48
+ onServer: (name: string, handler: (player: Player, ...args: any[]) => void, options?: bridge.HandlerOptions) => void,
49
+ -- Server: what answers a client's bridge.invoke of that name, with the player
50
+ -- first; what it returns goes back. One handler a name.
51
+ onInvoke: (name: string, handler: (player: Player, ...args: any[]) => any, options?: bridge.HandlerOptions) => void,
29
52
  -- Server: to one client
30
53
  sendTo: (player: Player, name: string, ...args: any[]) => void,
31
54
  -- Server: to every client
32
55
  sendAll: (name: string, ...args: any[]) => void,
33
56
  -- Server: a client used a token it did not earn, sent a packet the bridge
34
57
  -- did not seal, sent one twice or over bridgeMaxPacket, asked for its key
35
- -- twice, or touched a decoy. A handler is the game's whole answer. With none,
36
- -- the player and their alt accounts are banned for good.
58
+ -- twice, touched a decoy, or replayed a honeypot. A handler is the game's
59
+ -- whole answer. With none, the player and their alt accounts are banned for
60
+ -- good; in Studio the player is kicked instead.
37
61
  onBreach: (handler: (player: Player, reason: string) => void) => void,
38
62
  }
package/lowering.mjs CHANGED
@@ -33,10 +33,10 @@ import { bridgeSource, obfuscateBridge } from "./generate.mjs"
33
33
  /** @typedef {import("@tilua/parser").LoweringPlugin} LoweringPlugin */
34
34
 
35
35
  const HANDLE = "__tilua_bridge"
36
- const CLIENT_MEMBERS = new Set(["send", "onClient"])
37
- const SERVER_MEMBERS = new Set(["onServer", "sendTo", "sendAll", "onBreach"])
36
+ const CLIENT_MEMBERS = new Set(["send", "invoke", "onClient"])
37
+ const SERVER_MEMBERS = new Set(["onServer", "onInvoke", "sendTo", "sendAll", "onBreach"])
38
38
  /** Members taking a handler, which runs on a thread of the module's. */
39
- const HANDLERS = new Set(["onClient", "onServer", "onBreach"])
39
+ const HANDLERS = new Set(["onClient", "onServer", "onInvoke", "onBreach"])
40
40
  export const MODULE_NAME = "_bridge.luau"
41
41
 
42
42
  /** A handler runs on a thread the module starts, away from the `xpcall` that
@@ -108,7 +108,7 @@ const plugin = {
108
108
  return {
109
109
  callee: member === "onBreach"
110
110
  ? `(function(handler) ${handle}.${member}(${guard}(handler)) end)`
111
- : `(function(name, handler) ${handle}.${member}(name, ${guard}(handler)) end)`,
111
+ : `(function(name, handler, options) ${handle}.${member}(name, ${guard}(handler), options) end)`,
112
112
  }
113
113
  },
114
114
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tilua-types/bridge",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "description": "bridge: Roblox client-server networking through a per-game, obfuscated module that hands each file its handle once, with AES-GCM packets and decoy remotes",
5
5
  "license": "ISC",
6
6
  "type": "module",
@@ -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 only warns, so a test cannot ban whoever runs it.
68
- local function ban(player, reason)
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] or RunService:IsStudio() then
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 a ban.
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
- ban(player, reason)
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
- local function newInbox()
121
- return { handlers = {}, waiting = {}, held = 0, noted = {}, full = false }
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
- local function deliver(inbox, name, ...)
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 _, handler in handlers do
128
- task.spawn(handler, ...)
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, tostring(name)))
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, { name = name, args = table.pack(...) })
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(tostring(name)))
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, handler)
595
+ table.insert(handlers, entry)
155
596
  return
156
597
  end
157
- inbox.handlers[name] = { handler }
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
- task.spawn(handler, table.unpack(message.args, 1, message.args.n))
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
- local clientInbox = newInbox()
177
- local packetRemote, keyRemote
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
- session = { key = crypto.randomBytes(32), given = false, last = 0, window = 0, count = 0 }
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
- local remotes = { packetRemote, keyRemote }
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 session = sessions[player]
218
- if not session or not session.given or type(blob) ~= "string" then
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
- local args = type(packet.a) == "table" and packet.a or {}
270
- local count = type(packet.c) == "number" and math.clamp(packet.c, 0, 255) or #args
271
- deliver(serverInbox, packet.n, player, table.unpack(args, 1, count))
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
- for _ = 1, DECOYS do
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
- decoyFunction.Name = randomName()
285
- decoyFunction.OnServerInvoke = function(player)
286
- serverBreach(player, "invoked a decoy")
287
- return nil
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
- local function seal(packet)
315
- sequence += 1
316
- packet.s = sequence
317
- local text = HttpService:JSONEncode(packet)
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
- packetRemote:FireServer(seal({ b = reason }))
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(name, ...)
349
- deliver(clientInbox, name, ...)
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
- packetRemote:FireClient(player, name, ...)
1102
+ table.insert(serverOutbox, { to = player, text = encodeMessage("sendTo", name, ...) })
384
1103
  end,
385
1104
  sendAll = function(name, ...)
386
- packetRemote:FireAllClients(name, ...)
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
- -- The arguments as an array and their count apart: a JSON array cannot
401
- -- say how many trailing nils there were
402
- local blob = seal({ n = name, a = { ... }, c = select("#", ... ) })
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
- packetRemote:FireServer(blob)
1131
+ table.insert(clientOutbox, text)
407
1132
  end,
408
- onClient = function(name, handler)
409
- listen(clientInbox, name, handler)
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"),