summaryrefslogtreecommitdiffstats
path: root/scripts/quake3-info.nse
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/quake3-info.nse')
-rw-r--r--scripts/quake3-info.nse256
1 files changed, 256 insertions, 0 deletions
diff --git a/scripts/quake3-info.nse b/scripts/quake3-info.nse
new file mode 100644
index 0000000..197137e
--- /dev/null
+++ b/scripts/quake3-info.nse
@@ -0,0 +1,256 @@
+local comm = require "comm"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local stringaux = require "stringaux"
+local table = require "table"
+
+description = [[
+Extracts information from a Quake3 game server and other games which use the same protocol.
+]]
+
+---
+-- @usage
+-- nmap -sU -sV -Pn --script quake3-info.nse -p <port> <target>
+--
+-- @output
+-- PORT STATE SERVICE VERSION
+-- 27960/udp open quake3 Quake 3 dedicated server
+-- | quake3-info:
+-- | PLAYERS:
+-- | 1. cyberix (frags: 0/20, ping: 4)
+-- | BASIC OPTIONS:
+-- | capturelimit: 8
+-- | dmflags: 0
+-- | elimflags: 0
+-- | fraglimit: 20
+-- | gamename: baseoa
+-- | mapname: oa_dm1
+-- | protocol: 71
+-- | timelimit: 0
+-- | version: ioq3 1.36+svn1933-1/Ubuntu linux-x86_64 Apr 4 2011
+-- | videoflags: 7
+-- | voteflags: 767
+-- | OTHER OPTIONS:
+-- | bot_minplayers: 0
+-- | elimination_roundtime: 120
+-- | g_allowVote: 1
+-- | g_altExcellent: 0
+-- | g_delagHitscan: 0
+-- | g_doWarmup: 0
+-- | g_enableBreath: 0
+-- | g_enableDust: 0
+-- | g_gametype: 0
+-- | g_instantgib: 0
+-- | g_lms_mode: 0
+-- | g_maxGameClients: 0
+-- | g_needpass: 0
+-- | g_obeliskRespawnDelay: 10
+-- | g_rockets: 0
+-- | g_voteGametypes: /0/1/3/4/5/6/7/8/9/10/11/12/
+-- | g_voteMaxFraglimit: 0
+-- | g_voteMaxTimelimit: 0
+-- | g_voteMinFraglimit: 0
+-- | g_voteMinTimelimit: 0
+-- | sv_allowDownload: 0
+-- | sv_floodProtect: 1
+-- | sv_hostname: noname
+-- | sv_maxPing: 0
+-- | sv_maxRate: 0
+-- | sv_maxclients: 8
+-- | sv_minPing: 0
+-- | sv_minRate: 0
+-- |_ sv_privateClients: 0
+
+author = "Toni Ruottu"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe", "version"}
+
+
+local function range(first, last)
+ local list = {}
+ for i = first, last do
+ table.insert(list, i)
+ end
+ return list
+end
+
+portrule = shortport.version_port_or_service(range(27960, 27970), {'quake3'}, 'udp')
+
+local function parsefields(data)
+ local fields = {}
+ local parts = stringaux.strsplit("\\", data)
+ local nullprefix = table.remove(parts, 1)
+ if nullprefix ~= "" then
+ stdnse.debug2("unrecognized field format, skipping options")
+ return {}
+ end
+ for i = 1, #parts, 2 do
+ local key = parts[i]
+ local value = parts[i + 1]
+ fields[key] = value
+ end
+ return fields
+end
+
+local function parsename(data)
+ local parts = stringaux.strsplit('"', data)
+ if #parts ~= 3 then
+ return nil
+ end
+ local e1 = parts[1]
+ local name = parts[2]
+ local e2 = parts[3]
+ local extra = e1 .. e2
+ if extra ~= "" then
+ return nil
+ end
+ return name
+end
+
+local function parseplayer(data)
+ local parts = stringaux.strsplit(" ", data)
+ if #parts < 3 then
+ stdnse.debug2("player info line is missing elements, skipping a player")
+ return nil
+ end
+ if #parts > 3 then
+ stdnse.debug2("player info line has unknown elements, skipping a player")
+ return nil
+ end
+ local player = {}
+ player.frags = parts[1]
+ player.ping = parts[2]
+ player.name = parsename(parts[3])
+ if player.name == nil then
+ stdnse.debug2("invalid player name serialization, skipping a player")
+ return nil
+ end
+ return player
+end
+
+local function parseplayers(data)
+ local players = {}
+ for _, p in ipairs(data) do
+ local player = parseplayer(p)
+ if player then
+ table.insert(players, player)
+ end
+ end
+ return players
+end
+
+local function is_leader(a, b)
+ local collide = a.name == b.name
+ local even = a.frags == b.frags
+ local leads = a.frags > b.frags
+ local alphab = a.name > b.name
+ local faster = a.ping > b.ping
+ return leads or (even and alphab) or (even and collide and faster)
+end
+
+local function formatplayers(players, fraglimit)
+ table.sort(players, is_leader)
+ local printable = {}
+ for i, player in ipairs(players) do
+ local name = player.name
+ local ping = player.ping
+ local frags = player.frags
+ if fraglimit then
+ frags = string.format("%s/%s", frags, fraglimit)
+ end
+ table.insert(printable, string.format("%d. %s (frags: %s, ping: %s)", i, name, frags, ping))
+ end
+ printable["name"] = "PLAYERS:"
+ return printable
+end
+
+local function formatfields(fields, title)
+ local printable = {}
+ for key, value in pairs(fields) do
+ local kv = string.format("%s: %s", key, value)
+ table.insert(printable, kv)
+ end
+ table.sort(printable)
+ printable["name"] = title
+ return printable
+end
+
+local function assorted(fields)
+ local basic = {}
+ local other = {}
+ for key, value in pairs(fields) do
+ if string.find(key, "_") == nil then
+ basic[key] = value
+ else
+ other[key] = value
+ end
+ end
+ return basic, other
+end
+
+action = function(host, port)
+ local GETSTATUS = "\xff\xff\xff\xffgetstatus\n"
+ local STATUSRESP = "\xff\xff\xff\xffstatusResponse"
+
+ local status, data = comm.exchange(host, port, GETSTATUS, {["proto"] = "udp"})
+ if not status then
+ return
+ end
+ local parts = stringaux.strsplit("\n", data)
+ local header = table.remove(parts, 1)
+ if header ~= STATUSRESP then
+ return
+ end
+ if #parts < 2 then
+ stdnse.debug2("incomplete status response, script abort")
+ return
+ end
+ local nullend = table.remove(parts)
+ if nullend ~= "" then
+ stdnse.debug2("missing terminating endline, script abort")
+ return
+ end
+ local field_data = table.remove(parts, 1)
+ local player_data = parts
+
+ local fields = parsefields(field_data)
+ local players = parseplayers(player_data)
+
+ local basic, other = assorted(fields)
+
+ -- Previously observed version strings:
+ -- "tremulous 1.1.0 linux-x86_64 Aug  5 2010"
+ -- "ioq3 1.36+svn1933-1/Ubuntu linux-x86_64 Apr  4 2011"
+ local versionline = basic["version"]
+ if versionline then
+ local fields = stringaux.strsplit(" ", versionline)
+ local product = fields[1]
+ local version = fields[2]
+ local osline = fields[3]
+ port.version.name = "quake3"
+ port.version.product = product
+ port.version.version = version
+ if string.find(osline, "linux") then
+ port.version.ostype = "Linux"
+ end
+ if string.find(osline, "win") then
+ port.version.ostype = "Windows"
+ end
+ nmap.set_port_version(host, port)
+ end
+
+ local fraglimit = fields["fraglimit"]
+ if not fraglimit then
+ fraglimit = "?"
+ end
+
+ local response = {}
+ table.insert(response, formatplayers(players, fraglimit))
+ table.insert(response, formatfields(basic, "BASIC OPTIONS:"))
+ if nmap.verbosity() > 0 then
+ table.insert(response, formatfields(other, "OTHER OPTIONS:"))
+ end
+ return stdnse.format_output(true, response)
+end