-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathQuery.lua
213 lines (176 loc) · 5.64 KB
/
Query.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
local PACKET_TYPE_HANDSHAKE = 9
local PACKET_TYPE_STAT = 0
local PACKET_MAGIC = { 0xFE, 0xFD }
local PACKET_STAT_PADDING_1 = { 0x73, 0x70, 0x6C, 0x69, 0x74, 0x6E, 0x75, 0x6D, 0x00, 0x80, 0x00 }
local PACKET_STAT_PADDING_2 = { 0x01, 0x70, 0x6C, 0x61, 0x79, 0x65, 0x72, 0x5F, 0x00, 0x00 }
local CHALLENGE_TOKEN = os.time()
local g_UDPSocket = nil
local g_IniFile = nil
function Initialize(Plugin)
Plugin:SetName("Query")
Plugin:SetVersion(1)
-- Use the InfoReg shared library to process the Info.lua file:
dofile(cPluginManager:GetPluginsPath() .. "/InfoReg.lua")
RegisterPluginInfoCommands()
RegisterPluginInfoConsoleCommands()
local Callbacks =
{
OnReceivedData = function (a_Endpoint, a_Data, a_RemotePeer, a_RemotePort)
HandlePacket(a_Data, a_RemotePeer, a_RemotePort)
end,
OnError = function (a_Endpoint, a_ErrorCode, a_ErrorMsg)
LOG("Error in Query UDP: " .. a_ErrorCode .. " (" .. a_ErrorMsg .. ")")
end,
}
g_IniFile = cIniFile()
g_IniFile:ReadFile("settings.ini")
if g_IniFile:GetValueSetB("Query", "Enabled", true) then
local Port = g_IniFile:GetValueSetI("Query", "Port", 25565)
g_UDPSocket = cNetwork:CreateUDPEndpoint(Port, Callbacks)
g_UDPSocket:EnableBroadcasts()
LOG("Started query server on port " .. tostring(Port) .. "/udp.")
else
LOG("Not starting query server; disabled in settings.ini.")
end
g_IniFile:WriteFile("settings.ini")
LOG("Initialized " .. Plugin:GetName() .. " v." .. Plugin:GetVersion())
return true
end
function OnDisable()
if g_UDPSocket ~= nil then
g_UDPSocket:Close()
g_UDPSocket = nil
end
LOG("Disabled Query!")
end
function UDPSend(a_Data, a_Host, a_Port)
local Callbacks =
{
OnError = function (a_Endpoint, a_ErrorCode, a_ErrorMsg)
LOG("Error in Query UDP sending: " .. a_ErrorCode .. " (" .. a_ErrorMsg .. ")")
end,
OnReceivedData = function ()
-- ignore
end,
}
g_UDPSocket:Send(a_Data, a_Host, a_Port)
end
function HandlePacket(a_Data, a_Host, a_Port)
if not(PacketHasMagic(a_Data)) then
return
end
local PacketType = a_Data:byte(3)
if PacketType == PACKET_TYPE_HANDSHAKE then
HandlePacketHandshake(a_Data, a_Host, a_Port)
elseif PacketType == PACKET_TYPE_STAT then
HandlePacketStat(a_Data, a_Host, a_Port)
end
end
function HandlePacketHandshake(a_Data, a_Host, a_Port)
local SessionId = PacketReadInt(a_Data:sub(4))
local Data = PacketCreate(PACKET_TYPE_HANDSHAKE, SessionId, tostring(CHALLENGE_TOKEN) .. "\0")
UDPSend(Data, a_Host, a_Port)
end
function HandlePacketStat(a_Data, a_Host, a_Port)
local SessionId = PacketReadInt(a_Data:sub(4))
local SuppliedTokenBytes = PacketReadInt(a_Data:sub(8))
local SuppliedToken = 0
for i = 1, 4 do
SuppliedToken = SuppliedToken * (2 ^ 8) + SuppliedTokenBytes[i]
end
if SuppliedToken ~= CHALLENGE_TOKEN then
return
end
if a_Data:len() == 11 then
HandleBasicStat(SessionId, a_Host, a_Port)
elseif a_Data:len() == 15 then
HandleFullStat(SessionId, a_Host, a_Port)
end
end
function HandleBasicStat(a_SessionId, a_Host, a_Port)
local Stat = StatData()
local Message =
{
Stat["hostname"],
Stat["gametype"],
Stat["map"],
Stat["numplayers"],
Stat["maxplayers"],
Stat["hostport"],
Stat["hostip"],
}
local Data = PacketCreate(PACKET_TYPE_STAT, a_SessionId, table.concat(Message, "\0") .. "\0")
UDPSend(Data, a_Host, a_Port)
end
function HandleFullStat(a_SessionId, a_Host, a_Port)
local Players = {}
cRoot:Get():ForEachPlayer(function (a_Player)
if #Players < 16 then
Players[#Players + 1] = a_Player:GetName()
end
end)
local Message = string.char(unpack(PACKET_STAT_PADDING_1))
for k, v in pairs(StatData()) do
Message = Message .. string.format("%s\0%s\0", k, v)
end
Message = Message .. "\0"
Message = Message .. string.char(unpack(PACKET_STAT_PADDING_2))
Message = Message .. table.concat(Players, "\0") .. "\0\0"
local Data = PacketCreate(PACKET_TYPE_STAT, a_SessionId, Message)
UDPSend(Data, a_Host, a_Port)
end
function StatData()
local Server = cRoot:Get():GetServer()
local PluginManager = cRoot:Get():GetPluginManager()
-- currently hardcoded
local Version = "1.8"
local Plugins = {}
cPluginManager:ForEachPlugin(
function (aPlugin)
-- If plugin object exists
if aPlugin ~= nil then
-- If plugin successfully loaded
if aPlugin:GetStatus() == 0 then
Plugins[#Plugins + 1] = string.format("%s v%s", aPlugin:GetName(), tostring(aPlugin:GetVersion()));
end
end
end
);
-- these keys are the ones in the key-value section of a full stat as listed in http://wiki.vg/Query
return {
-- actually the motd, but called hostname for some reason
hostname = Server:GetDescription(),
-- hardcoded as per http://wiki.vg/Query
gametype = "SMP",
-- hardcoded as per http://wiki.vg/Query
game_id = "MINECRAFT",
version = Version,
plugins = string.format("Сuberite %s: %s", Version, table.concat(Plugins, "; ")),
map = cRoot:Get():GetDefaultWorld():GetName(),
numplayers = tostring(Server:GetNumPlayers()),
maxplayers = tostring(Server:GetMaxPlayers()),
hostport = g_IniFile:GetValue("Server", "Ports"),
-- from MCServer source, file src/OSSupport/ServerHandleImpl.cpp
-- in cServerHandleImpl::Listen(UInt16 a_Port)
-- `memset(&name, 0, sizeof(name))` binds socket to all interfaces
hostip = "0.0.0.0",
}
end
function PacketCreate(a_PacketType, a_SessionId, a_Message)
return string.char(a_PacketType) .. string.char(unpack(a_SessionId)) .. a_Message
end
function PacketHasMagic(a_Data)
return a_Data:byte(1) == PACKET_MAGIC[1] and a_Data:byte(2) == PACKET_MAGIC[2]
end
function PacketReadInt(a_Data)
local x = {}
for i = 1, 4 do
b = a_Data:byte(i)
if b == nil then
x[i] = 0
else
x[i] = b
end
end
return x
end