-
Notifications
You must be signed in to change notification settings - Fork 0
/
EtherHammerX_Server.lua
294 lines (243 loc) · 10.8 KB
/
EtherHammerX_Server.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
---[[
--- EtherHammer - Server Script.
---
--- @author asledgehammer, JabDoesThings, 2024
---]]
local Packet = require 'asledgehammer/network/Packet';
local PlayerListener = require 'asledgehammer/network/PlayerListener';
-- Check to see if CraftHammer is installed.
if kickPlayerFromServer == nil then
print(
'[EtherHammerX] :: !!! WARNING: CraftHammer isn\'t installed on the server! ' ..
'Kicking players from server-side is disabled. !!!'
);
print('[EtherHammerX] :: To install CraftHammer, follow this link and grab the latest version here: '
.. 'XLps://discord.gg/r6PeSFuJDU'
);
end
-- (Only run on server-side of a multiplayer session)
if isClient() or not isServer() then return end
(function()
-- The packet-module identity.
local MODULE_ID = 'EtherHammerX';
--- (Login statuses)
local STATUS_AWAIT_GREET = 1;
local STATUS_AWAIT_VERIFICATION = 2;
local STATUS_AWAIT_SENT_REQUEST = 3;
local STATUS_SENT_VERIFICATION_REQUEST = 4;
-- (Heartbeat statuses)
local STATUS_AWAIT_SENT_HEARTBEAT_REQUEST = 5;
local STATUS_SENT_HEARTBEAT_REQUEST = 6;
local STATUS_VERIFIED = 7;
--- @type number
--- The time (in second(s)) that the client must respond at the time on login.
local TIME_TO_GREET = 10;
--- @type number
--- The time (in second(s)) that the client must respond after a request is sent post-greeting.
local TIME_TO_VERIFY = 120;
--- @type boolean
--- If true, players are pinged for a new key and verification is done.
local SHOULD_HEARTBEAT = true;
--- @type number
--- The time (in second(s)) that the client must respond (At the time of request) periodically.
local TIME_TO_HEARTBEAT = 20;
local TIME_TO_TICK = 5;
local function run()
--- @type table<string, number>
local playerStatuses = {};
--- @type table<string, string>
local playerKeys = {};
--- @type table<string, number>
local playerFragments = {};
--- @type table<string, number>
local playerRequestLast = {};
--- Sends a login verification request to cycle the key, requesting for the current key as well.
---
--- @param player IsoPlayer The player object.
--- @param username string The player username.
---
--- @return void
local function requestVerification(player, username)
-- Create server-side key-fragment.
local fragment1 = getTimeInMillis();
playerFragments[username] = fragment1;
local packet = Packet(MODULE_ID, 'join_request', { message = fragment1 });
packet:encrypt('EtherHammerX_' .. username, function()
packet:sendToPlayer(player);
-- Start the timer only after encrypting the packet and sending it.
playerStatuses[username] = STATUS_SENT_VERIFICATION_REQUEST;
playerRequestLast[username] = getTimeInMillis();
end);
playerStatuses[username] = STATUS_AWAIT_SENT_REQUEST;
end
--- Sends a followup request to cycle the key, requesting for the current key as well.
---
--- @param player IsoPlayer The player object.
--- @param username string The player username.
---
--- @return void
local requestHeartbeat = function(player, username)
-- Create new server-side key-fragment.
local fragment1 = getTimeInMillis();
playerFragments[username] = fragment1;
playerRequestLast[username] = fragment1;
local packet = Packet(MODULE_ID, 'heartbeat_request', { message = fragment1 });
packet:encrypt(playerKeys[username], function()
packet:sendToPlayer(player);
-- Start the timer only after encrypting the packet and sending it.
playerStatuses[username] = STATUS_SENT_HEARTBEAT_REQUEST;
playerRequestLast[username] = getTimeInMillis();
end);
playerStatuses[username] = STATUS_AWAIT_SENT_HEARTBEAT_REQUEST;
end
--- Handles the cleanup of player-resources when logging out.
---
--- @param username string The username of the player logging out.
---
--- @return void
local function processLogout(username)
-- Dispose of status & request times.
playerStatuses[username] = nil;
playerRequestLast[username] = nil;
playerKeys[username] = nil;
end
--- (Generic kick-player function)
---
--- @param player IsoPlayer The player to kick.
--- @param username string The username of the player.
--- @param reason string | nil (Optional) The reason the player is kicked.
---
--- @return void
local function kick(player, username, reason)
if kickPlayerFromServer == nil then
print(
'# [EtherHammerX] :: !!! WARNING: CraftHammer isn\'t installed on the server! ' ..
'Kicking players from server-side is disabled. (Cannot kick player "' .. username .. '") !!!'
);
return;
end
local message = 'Kicking player \'' .. username .. '\'.';
if reason then
message = message .. ' (Reason: \'' .. reason .. '\')';
end
kickPlayerFromServer(player, reason);
end
--- Checks on the players' status and handles them.
---
--- @param player IsoPlayer The player to check.
--- @param username string The username of the player.
---
--- @return void
local function onPlayerTick(player, username)
local status = playerStatuses[username];
if status == STATUS_AWAIT_GREET then
local timeNow = getTimeInMillis();
local timeRequest = playerRequestLast[username];
if timeRequest == nil then
timeRequest = getTimeInMillis();
playerRequestLast[username] = timeRequest;
end
if timeNow - timeRequest > TIME_TO_GREET * 1000 then
kick(player, username);
return;
end
elseif status == STATUS_AWAIT_VERIFICATION then
requestVerification(player, username);
elseif status == STATUS_SENT_VERIFICATION_REQUEST then
local timeNow = getTimeInMillis();
local timeRequest = playerRequestLast[username];
-- If greater than TIME_TO_VERIFY Seconds, kick player.
if timeNow - timeRequest > TIME_TO_VERIFY * 1000 then
kick(player, username);
return;
end
elseif status == STATUS_VERIFIED then
if SHOULD_HEARTBEAT then
local timeNow = getTimeInMillis();
local timeRequest = playerRequestLast[username];
if timeNow - timeRequest > TIME_TO_HEARTBEAT * 1000 then
requestHeartbeat(player, username);
end
end
elseif status == STATUS_SENT_HEARTBEAT_REQUEST then
local timeNow = getTimeInMillis();
local timeRequest = playerRequestLast[username];
if timeNow - timeRequest > TIME_TO_VERIFY * 1000 then
kick(player, username);
return;
end
end
end
--- Handles packets received for each player.
---
--- @param player IsoPlayer The player that sent the packet.
--- @param id string The identity of the packet sent.
--- @param data table Additional data provided for the packet.
---
--- @return void
local function onReceivePacket(player, id, data)
if id == 'join_response' or id == 'heartbeat_response' then
local username = player:getUsername();
-- Create the next key.
local fragment1 = playerFragments[username];
local fragment2 = data.message;
-- (Heartbeat) Verify that the returned key is the current key.
if id == 'heartbeat_response' then
local keyOld = playerKeys[username];
if data.key ~= keyOld then
kick(player, username);
return;
end
end
-- Create the next key.
playerKeys[username] = MODULE_ID .. '_' .. username .. fragment1 .. fragment2;
-- The player is now verified.
playerStatuses[username] = STATUS_VERIFIED;
if id == 'join_response' then
print('[EtherHammerX] :: Player \'' .. tostring(username) .. '\' verified.');
end
elseif id == 'handshake_request' then
local username = player:getUsername();
requestVerification(player, username);
end
end
local function onPlayerLogin(player)
local username = player:getUsername();
-- Signal that the player needs verification.
playerStatuses[username] = STATUS_AWAIT_GREET;
playerKeys[username] = MODULE_ID .. '_' .. username;
print('[EtherHammerX] :: Player \'' .. tostring(username) .. '\' joined the game.');
onPlayerTick(player, username);
end
local function onPlayerLogout(player)
local username = player:getUsername();
processLogout(username);
print('[EtherHammerX] :: Player \'' .. tostring(username) .. '\' left the game.');
end
--- @type number
local tickTimeNow = -1;
--- @type number
local tickTimeLast = -1;
local function onTick()
tickTimeNow = getTimeInMillis();
-- Only run once every TIME_TO_TICK second(s).
if tickTimeNow - tickTimeLast < TIME_TO_TICK * 1000 then return end
tickTimeLast = tickTimeNow;
for username, player in pairs(PlayerListener.players) do
onPlayerTick(player, username);
end
end
Events.OnClientCommand.Add(function(module, command, player, args)
if module ~= MODULE_ID then return end
local username = player:getUsername();
local packet = Packet(module, command, args);
packet:decrypt(playerKeys[username], function()
onReceivePacket(player, packet.command, packet.data);
end);
end);
Events.OnServerPlayerLogin.Add(onPlayerLogin);
Events.OnServerPlayerLogout.Add(onPlayerLogout);
Events.OnTickEvenPaused.Add(onTick);
end
Events.OnServerStarted.Add(run);
end)();