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 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425
|
#!/usr/bin/env ruby
# customise these if required (currently checks ports 27900-27999) - note that
# grep uses '.' as a wildcard that matchs exactly one character
# used when called with the -p option (display pids) - requires admin perms
$lsof_string = 'lsof -i udp -l | grep crded | grep 279..'
# used when called without the -p option (no pids) - can be run by any user
$netstat_string = 'netstat -u -l -n | grep 279..'
=begin
ALIEN ARENA SERVER BROWSER (for server admins) V1.1
Copyright (C) 2007 Tony Jackson
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Tony Jackson can be contacted at tonyj@cooldark.com
=end
require 'socket'
require 'rbconfig.rb'
include Config
$debug = false # set to true/false to enable/disable debug output
$offline = false # set to true/false to enable/disable offline debug mode (extra data files required)
=begin
The ServerLink class allows simple access to Alien Arena master/games servers, without
having to worry about the details of the UDP protocols involved
Before any other calls, set_protocol(<type>) should be called with either 'master' or 'game'.
query(<'ip:port' string array>) then queries each server in the array, returning an
array containing a list of servers that replied. query() is blocking, so may take a
little while depending on how many servers are being queried.
Once a query is complete, information can be read about each server individually,
using one of:
get_master_list('ip:port') - in the case of having queried one or more master servers
=> returns array of 'server:ip' strings for each game server registered
get_server_info('ip:port') - in the case of having queried one or more games servers
=> Returns hash of server parameters
get_player_info('ip:port') - in the case of having queried one or more games servers
=> Returns array of hashs (each hash containing one players worth of information)
On the next query() call, any old server/player/master data is cleared and replaced
=end
class ServerLink
def initialize
set_protocol('master') # default to master mode (rather than leave uninitialised)
# default - try three times for each server
#@tries = 3 # TODO: currently not used
@buffers = Hash.new # to store the reply UDP packet payload from each server
@pings = Hash.new # to store array of ping times for each server
end
# set type of link - master or game
def set_protocol (type = 'master')
if type == 'master'
@protocol = 'master'
elsif type == 'game'
@protocol = 'game'
end
end
# query all servers in array (can be multiple masters, as well as games servers)
def query(servers)
if servers == nil
return []
end
# in offline mode, return data from a set of files rather than by querying live servers
if $offline
return offline_query(servers)
end
# select the query string to put in the outboand UDP packet
if @protocol == 'master'
query = "query"
elsif @protocol == 'game'
query = "status\n"
else
return []
end
connections = Hash.new
failed = Array.new
@buffers.clear
@pings.clear
# Get the time before sending any packets, to time server response time (ping)
starttime = Time.now
# send a UDP packet to each server in the array
servers.each do
| server | # Here server is of form 'ip:port' string
begin
socket = UDPSocket.new # open works in the same way
socket.connect(server.split(':')[0], server.split(':')[1])
socket.send(query, 0)
#socket.send(nil, 0) # Test failure case
connections[socket] = server # hash keyed on socket resource, containing server string
rescue
# some failure making a socket or sending a message - add to list of failed servers
#socket.close - can't use this - may not even be open
failed << server
next
end
end
# remove servers from list if socket failed to create/send UDP packet
servers -= failed
# check that we have at least one open UDP socket
if connections.length == 0
return servers
end
selectsocketlist = connections.keys # get list of sockets from hash
while result = select(selectsocketlist, nil, nil, 0.5) # select() waits for one or more socket to get a read event, or times out
ping = (Time.now - starttime)*1000
# store the time at which this server (or multiple servers) responded, and store the replies in our array of buffers
result[0].each do
|socket|
begin
# here connections[socket] gives us the 'ip:port' string of the associated server
@buffers[connections[socket]] = socket.recv(2048) # big enough to cover both games server replies and master reply with 256 servers (payload 12+6*256 bytes)
@pings[connections[socket]] = ping
selectsocketlist.delete(socket) # delete from array now that we have a reply
socket.close
rescue
next
end
# test code to dump UDP payload contents to file for offline debug mode (make sure debug/ directory exists first)
# file = open("debug/#{connections[socket].split(':')[0]}-#{connections[socket].split(':')[1]}.dmp", 'wb')
# file.write(@buffers[connections[socket]])
# file.close
end
end
# here selectsocketlist will contain an array of failed sockets
selectsocketlist.each do
|socket|
servers -= [connections[socket]]
socket.close
end
# return array of servers that responded to our queries
return servers
end
# offline mode query (for debug), that uses local files instead of querying live servers
def offline_query(servers)
replied = Array.new
if $offline != true
return
end
servers.each do
|server|
begin
filename = server.split(':')[0]+'-'+server.split(':')[1]+'.dmp'
fakeserver = File.open("debug/#{filename}", 'rb')
@buffers[server] = fakeserver.read()
fakeserver.close
@pings[server] = 100
replied << server
rescue
end
end
return replied
end
# parses UDP payload received from a master server and returns array of 'ip:port' strings
def get_master_list server
if @protocol != 'master'
return []
end
####################################
# Buffer is 0xFFFF long in game (65536)
# Data is this format:
# Ignore first 12 bytes 'servers '
# Four byte address
# Two byte port
# Four byte address
# Two byte port
# ....
# Up to 256 servers (hard coded limit in game)
####################################
servers = Array.new
buff = @buffers[server]
if buff == nil
return servers
end
buff = buff[12..-1] # Chop off first 12 chars
0.step(buff.length-6, 6) do
|i|
ip,port = buff.unpack("@#{i}Nn") # @#{i} denotes offset 'i' into buffer
servers << inet_ntoa(ip).to_s + ':' + port.to_s
end
return servers # This may be an empty array
end
# return hash of server parameters from UDP packet data
def get_server_info server
if @protocol != 'game'
return []
end
#example server strings
#buffer = "print\n\\mapname\\ctf-killbox\\needpass\\0\\gamedate\\Jan 31 2007\\gamename\\data1\\maxspectators\\4\\Admin\\Forsaken\\website\\http://www.alienarena.info\\sv_joustmode\\0\\maxclients\\16\\protocol\\34\\cheats\\0\\timelimit\\0\\fraglimit\\10\\dmflags\\2641944\\deathmatch\\1\\version\\6.03 x86 Jan 7 2007 Win32 RELEASE\\hostname\\Alienarena.info - CTF\\gamedir\\arena\\game\\arena\n"
#buffer = "print\n\\mapname\\DM-OMEGA\\needpass\\0\\maxspectators\\4\\gamedate\\Jan 9 2007\\gamename\\data1\\sv_joustmode\\0\\maxclients\\8\\protocol\\34\\cheats\\0\\timelimit\\0\\fraglimit\\10\\dmflags\\16\\version\\6.03 x86 Jan 7 2007 Win32 RELEASE\\hostname\\pufdogs hell\\gamedir\\arena\\game\\arena\n3 17 \"test chap\" \"loopback\"\n0 0 \"Cyborg\" \"127.0.0.1\"\n3 0 \"Squirtney\" \"127.0.0.1\"\n0 0 \"Butthead\" \"127.0.0.1\"\n"
buffer = @buffers[server]
buffer = buffer.split("\n")
serverinfo = buffer[1].split('\\')[1..-1]
if serverinfo.length % 2 == 0 # check even number of keys
serverinfo = Hash[*serverinfo]
else
serverinfo = Hash.new # empty hash
end
serverinfo['numplayers'] = buffer[2..-1].length
serverinfo['ping'] = @pings[server]
return serverinfo
end
# returns array of hashs about each player on a server
def get_player_info server
if @protocol != 'game'
return []
end
#example server strings
#buffer = "print\n\\mapname\\ctf-killbox\\needpass\\0\\gamedate\\Jan 31 2007\\gamename\\data1\\maxspectators\\4\\Admin\\Forsaken\\website\\http://www.alienarena.info\\sv_joustmode\\0\\maxclients\\16\\protocol\\34\\cheats\\0\\timelimit\\0\\fraglimit\\10\\dmflags\\2641944\\deathmatch\\1\\version\\6.03 x86 Jan 7 2007 Win32 RELEASE\\hostname\\Alienarena.info - CTF\\gamedir\\arena\\game\\arena\n"
#buffer = "print\n\\mapname\\DM-OMEGA\\needpass\\0\\maxspectators\\4\\gamedate\\Jan 9 2007\\gamename\\data1\\sv_joustmode\\0\\maxclients\\8\\protocol\\34\\cheats\\0\\timelimit\\0\\fraglimit\\10\\dmflags\\16\\version\\6.03 x86 Jan 7 2007 Win32 RELEASE\\hostname\\pufdogs hell\\gamedir\\arena\\game\\arena\n3 17 \"test chap\" \"loopback\"\n0 0 \"Cyborg\" \"127.0.0.1\"\n3 0 \"Squirtney\" \"127.0.0.1\"\n0 0 \"Butthead\" \"127.0.0.1\"\n"
buffer = @buffers[server]
buffer = buffer.split("\n")
playerbuff = buffer[2..-1]
playerinfo = Array.new # array of hashs
playerbuff.each do
| line |
player = Hash.new
# each line is of form 3 17 "test chap" "12.34.56.78" (note spaces in names)
space_delimited = line.split(' ')
quote_delimited = line.split('"')
player['score'] = space_delimited[0]
player['ping'] = space_delimited[1]
player['name'] = quote_delimited[1]
if quote_delimited[3] != nil
player['ip'] = quote_delimited[3]
end
playerinfo << player
end
return playerinfo
end
def inet_aton ip
ip.split(/\./).map{|c| c.to_i}.pack("C*").unpack("N").first
end
def inet_ntoa n
[n].pack("N").unpack("C*").join "."
end
# get raw UDP payload response from a particular server (debug only)
def get_response(server)
if @buffers.include?(server)
return @buffers[server]
else
return nil
end
end
end
=begin
This is the application class responsible for handling the GUI
and launching games/URLS. It uses the ServerLink class to query
servers and get meaningful responses.
=end
# Program starts here
if $*.include?('--help') then
puts 'Server Status for Alien Arena servers'
puts ''
puts ' Optional arguments:'
puts ' -r Resolve player IPs to addresses'
puts ' -p Display PIDs (requires admin perms)'
puts ' --help This message'
puts ''
puts ' Edit this file (svstat) if you use ports in a range other than 27900-27999'
puts ''
exit
end
localports = Array.new
localpids = Array.new
if $*.include?('-p') then
lsof = IO.popen($lsof_string,'r')
lsof.each { |line|
split = line.split(' ')
localports << split.last.split(':').last
localpids << split[1]
}
lsof.close
else
ns = IO.popen($netstat_string,'r')
ns.each { |line|
split = line.split(' ')
localports << split[3].split(':').last
localpids << '---'
}
ns.close
end
# Use this code to get list of hostnames configured in arena/*.cfg files
#hostnames = Dir['alienarena2007/arena/*'].collect{ |file|
# if file.split('.').last == 'cfg'
# File.open(file).find{
# |line|
# split = line.split(' ')
# split[0] == 'set' and split[1] == 'hostname'
# }
# end
# }.compact.collect{ |line| line.split('"')[1]}.compact
@serverlink = ServerLink.new
@serverlink.set_protocol('game')
servers = localports.collect{|port| 'localhost:'+port}
responsive_servers = @serverlink.query(servers)
#title = "#{responsive_servers.length}/#{servers.length} local servers responded"
#puts title
puts '-'*80
puts 'PID Port Name Map Pl Bot'
puts '-'*80
responsive_servers.each {
|server|
serverinfo = @serverlink.get_server_info(server)
playerinfo = @serverlink.get_player_info(server)
port = server.split(':').last
players = 0
bots = 0
playerinfo.each { |player|
# can't use IP address to determin bots since 6.10 security fix - they're all 127.0.0.1
# if player['ip'] != '127.0.0.1' then
# players += 1
# else
# bots += 1
# end
# have to use ping, which is less reliable
if player['ping'].to_i > 0 then
players += 1
else
bots += 1
end
}
puts "#{localpids[localports.index(port)].concat(' '*8).slice(0,8)} #{port.concat(' '*6).slice(0,6)} #{serverinfo['hostname'].concat(' '*38).slice(0,38)} #{serverinfo['mapname'].concat(' '*16).slice(0,16)} #{players.to_s.concat(' '*2).slice(0,2)} #{bots}"
# playerinfo.each { |player|
# puts " #{player['name'].concat(' '*16).slice(0,16)} ping #{player['ping'].to_str.concat(' '*8).slice(0,8)} score #{player['score'].to_str.concat(' '*8).slice(0,8)} #{player['ip']}"
# }
}
puts '-'*80
puts 'Player name Ping Score IP Server name'
puts '-'*80
responsive_servers.each {
|server|
serverinfo = @serverlink.get_server_info(server)
playerinfo = @serverlink.get_player_info(server)
port = server.split(':').last
players = 0
bots = 0
playerinfo.each { |player|
if player['ping'].to_i > 0 then
puts "#{player['name'].concat(' '*15).slice(0,15)} #{player['ping'].to_str.concat('ms').concat(' '*6).slice(0,6)} #{player['score'].to_str.concat(' '*5).slice(0,5)} #{player['ip'].concat(' '*21).slice(0,21)} #{serverinfo['hostname'].concat(' '*29).slice(0,29)}"
if $*.include?('-r') then
playerip = player['ip'].split(':')[0]
playerport = player['ip'].split(':')[1]
nslookup = IO.popen("nslookup #{playerip} | grep \"name =\"",'r')
result = nslookup.gets
if(result != nil) then
playerip = result.split('name = ')[1].chomp[0..-2]
else
playerip = 'unresolvable'
end
nslookup.close
puts " (#{playerip})"
# puts "#{player['name'].concat(' '*14).slice(0,14)} #{player['ping'].to_str.concat('ms').concat(' '*6).slice(0,6)} #{player['score'].to_str.concat(' '*5).slice(0,5)} #{playerip.concat(':').concat(playerport).concat(' '*52).slice(0,52)}"
end
end
}
}
#puts $*
|