File content:

  1. --[[
  2. globalcombined lua fixes
  3. last updated 26/jan/2014
  4.  
  5. Notice: Add "set b_gameformat 6" to your server.cfg or config to restrict sniper rifle to 1 per team.
  6.  
  7. -- changelog:
  8. -- global ban table
  9. -- ip name/chat blocker for public servers
  10. -- duplicate name/guid kick
  11. -- typo fix on /ref and /rcon
  12. -- fixed an exploit with team command
  13. -- contains some fixes by Perlo_0ung?!
  14. -- implented b_gameformat check
  15. -- removed obsolete/unused stuff
  16. -- based on combinedfixes.lua
  17. -- first release
  18. -- all original work copyright -> reyalp@gmail.com
  19.  
  20. --]]
  21.  
  22. modname = "globalcombined"
  23. version = "4"
  24.  
  25.  
  26. global_ban_table = {}
  27. counter = {}
  28.  
  29. function et_InitGame(levelTime,randomSeed,restart)
  30. et.RegisterModname(""..modname.." "..version.." "..et.FindSelf())
  31. gameformat = tonumber(et.trap_Cvar_Get( "b_gameformat" ))
  32. maxclients = tonumber( et.trap_Cvar_Get( "sv_maxclients" ) )
  33. serverpassword = tonumber( et.trap_Cvar_Get( "g_password" ) )
  34. sniperaxis = 0
  35. sniperallies = 0
  36.  
  37. if gameformat == nil then
  38. et.trap_Cvar_Set( "b_gameformat", "0" )
  39. end
  40.  
  41. for cno=0, (maxclients-1) do
  42. counter[cno] = 0
  43. local guid = string.upper(et.Info_ValueForKey(et.trap_GetUserinfo(cno), "cl_guid"))
  44. global_ban_table[guid] = false
  45. end
  46.  
  47. end
  48.  
  49. -- client command checks, formerly wsfix
  50. -- prevent ws overrun exploit, crlf abuse
  51. -- history:
  52. -- 2 - bugfix
  53. -- TY McSteve for reporting this to us.
  54.  
  55. function et_ClientCommand(cno,cmd)
  56. local cmd = string.lower(cmd)
  57. local arg1 = string.lower(et.trap_Argv(1))
  58. local byte = string.byte(arg1,1)
  59.  
  60. if (cmd == "say" or cmd == "say_team" or cmd == "say_buddy" or cmd == "say_teamnl") and serverpassword == "" then
  61. if string.find(et.trap_Argv(1), "^(%d+).(%d+).(%d+).(%d+)") or string.find(et.trap_Argv(1), "^.(%w+):(%d+)") then
  62. return 1 -- abort message
  63. end
  64. end
  65.  
  66. if string.find(arg1, "^7ref") or string.find(arg1, "^7rcon") then
  67. et.trap_SendServerCommand( cno, "print \"Ckeck your typing: '"..arg1.."'\n\"" )
  68. return 1
  69. end
  70.  
  71. if cmd == "team" or cmd == "nextteam" then --exploit fix team command
  72. if string.len(arg1) > 1 then
  73. et.trap_SendServerCommand( cno , "print \"Invalid team join command.\n\"" )
  74. return 1
  75. end
  76. if arg1 ~= "" and byte ~= 98 and byte ~= 114 and byte ~= 115 then
  77. et.trap_SendServerCommand( cno , "print \"Invalid team join command.\n\"" )
  78. return 1
  79. end
  80. end
  81.  
  82. if cmd == "forcetapout" then --forcetapout bugfix
  83. if et.gentity_get(cno, "r.contents") == 0 then --contents 0 =nobody
  84. return 1 --prevent it
  85. end
  86. end
  87.  
  88. if cmd == "b_gameformat" then
  89. et.trap_SendServerCommand( cno,"print \"^wb_gameformat is: "..gameformat.." default: 0\n\"")
  90. return 1
  91. end
  92.  
  93.  
  94. if gameformat ~= "6" then -- gameformat check
  95.  
  96. if cmd == "class" then
  97. if et.trap_Argv(1) == "c" then
  98. if et.trap_Argv(2) == "3" then
  99. for j = 0, (maxclients - 1) do
  100. if getTeam(j) == 2 and getWeapon(j) == 41 then
  101. sniperallies = sniperallies +1
  102. end
  103. end
  104. if getWeapon(cno) == 41 then
  105. sniperallies = sniperallies -1
  106. end
  107. if sniperallies > 0 then
  108. sniperallies = 0
  109. setPlayerWeapon(cno , 10) -- Set Weapon to Sten
  110. return 1
  111. end
  112. for j = 0, (maxclients - 1) do
  113. if getTeam(j) == 1 and getWeapon(j) == 32 then
  114. sniperaxis = sniperaxis +1
  115. end
  116. end
  117. if getWeapon(cno) == 32 then
  118. sniperaxis = sniperaxis -1
  119. end
  120. if sniperaxis > 0 then
  121. sniperaxis = 0
  122. setPlayerWeapon(cno , 10) -- Set Weapon to Sten
  123. return 1
  124. end
  125. end
  126. end
  127. end
  128.  
  129. if cmd == "team" then
  130. if et.trap_Argv(1) == "b" then
  131. if et.trap_Argv(2) == "4" then
  132. if et.trap_Argv(3) == "25" then
  133. for j = 0, (maxclients - 1) do
  134. if getTeam(j) == 2 and getWeapon(j) == 25 then
  135. sniperallies = sniperallies +1
  136. end
  137. end
  138. if getWeapon(cno) == 25 then
  139. sniperallies = sniperallies -1
  140. end
  141. if sniperallies > 0 then
  142. sniperallies = 0
  143. setPlayerWeapon(cno , 10) -- Set Weapon to Sten
  144. return 1
  145. end
  146. end
  147. end
  148. end
  149. end
  150. if cmd == "team" then
  151. if et.trap_Argv(1) == "r" then
  152. if et.trap_Argv(2) == "4" then
  153. if et.trap_Argv(3) == "32" then
  154. for j = 0, (maxclients - 1) do
  155. if getTeam(j) == 1 and getWeapon(j) == 32 then
  156. sniperaxis = sniperaxis +1
  157. end
  158. end
  159. if getWeapon(cno) == 32 then
  160. sniperaxis = sniperaxis -1
  161. end
  162. if sniperaxis > 0 then
  163. sniperaxis = 0
  164. setPlayerWeapon(cno , 10) -- Set Weapon to Sten
  165. return 1
  166. end
  167. end
  168. end
  169. end
  170. end
  171. end -- gameformat check end
  172.  
  173. if cmd == "ws" then
  174. local n = tonumber(et.trap_Argv(1))
  175. if not n then
  176. et.G_LogPrint(string.format("wsfix: client %d bad ws not a number [%s]\n",cno,tostring(et.trap_Argv(1))))
  177. return 1
  178. end
  179.  
  180. if n < 0 or n > 21 then
  181. et.G_LogPrint(string.format("wsfix: client %d bad ws %d\n",cno,n))
  182. return 1
  183. end
  184. return 0
  185. end
  186. if cmd == "callvote" or cmd == "ref" or cmd == "sa" or cmd == "semiadmin" then
  187. local args=et.ConcatArgs(1)
  188. -- et.G_LogPrint(string.format("combinedfixes: client %d %s [%s]\n",cno,cmd,args))
  189. if string.find(args,"[\r\n]") then
  190. et.G_LogPrint(string.format("combinedfixes: client %d bad %s [%s]\n",cno,cmd,args))
  191. return 1;
  192. end
  193. return 0
  194. end
  195.  
  196. return 0
  197. end --end ClientCommand
  198.  
  199. -- prevent various borkage by invalid userinfo
  200. -- version: 4
  201. -- history:
  202. -- 4 - check length and IP
  203. -- 3 - check for name exploit against guidcheck
  204. -- 2 - fix nil var ref if kicked in RunFrame
  205. -- fix incorrect cno in log message for ClientConnect kick
  206. -- 1 - initial release
  207.  
  208. -- names that can be used to exploit some log parsers
  209. -- note: only console log parsers or print hooks should be affected,
  210. -- game log parsers don't see these at the start of a line
  211. -- "^etpro IAC" check is required for guid checking
  212. -- comment/uncomment others as desired, or add your own
  213. -- NOTE: these are patterns for string.find
  214. badnames = {
  215. '^ShutdownGame',
  216. '^ClientBegin',
  217. '^ClientDisconnect',
  218. '^ExitLevel',
  219. '^Timelimit',
  220. '^EndRound',
  221. '^etpro IAC',
  222. '^Vote',
  223. '^etpro privmsg',
  224. -- "say" is relatively likely to have false positives
  225. -- but can potentially be used to exploit things that use etadmin_mod style !commands
  226. -- '^say',
  227. '^Callvote',
  228. '^broadcast',
  229. '^badinfo',
  230. }
  231.  
  232. -- returns nil if ok, or reason
  233. function check_userinfo( cno )
  234. if et.gentity_get(cno,"ps.ping") == 0 then return end
  235. local userinfo = et.trap_GetUserinfo(cno)
  236. -- printf("check_userinfo: [%s]\n",userinfo)
  237.  
  238. -- bad things can happen if it's full
  239. if string.len(userinfo) > 980 then
  240. return "oversized"
  241. end
  242.  
  243. -- newlines can confuse various log parsers, and should never be there
  244. -- note this DOES NOT protect your log parsers, as the userinfo may
  245. -- already have been sent to the log
  246. if string.find(userinfo,"\n") then
  247. return "new line"
  248. end
  249.  
  250. -- the game never seems to make userinfos without a leading backslash,
  251. -- or with a trailing backslash, so reject those from the start
  252. if (string.sub(userinfo,1,1) ~= "\\" ) then
  253. return "missing leading slash"
  254. end
  255. -- shouldn't really be possible, since the engine stuffs ip\ip:port on the end
  256. if (string.sub(userinfo,-1,1) == "\\" ) then
  257. return "trailing slash"
  258. end
  259.  
  260. -- now that we know it is properly formed, count the slashes
  261. local n = 0
  262. for _ in string.gfind(userinfo,"\\") do
  263. n = n + 1
  264. end
  265.  
  266. if math.mod(n,2) == 1 then
  267. return "unbalanced"
  268. end
  269.  
  270. local m
  271. local t = {}
  272.  
  273. -- right number of slashes, check for dupe keys
  274. for m in string.gfind(userinfo,"\\([^\\]*)\\") do
  275. if string.len(m) == 0 then
  276. return "empty key"
  277. end
  278. m = string.lower(m)
  279. if t[m] then
  280. return "duplicate key"
  281. end
  282. t[m] = true
  283. end
  284.  
  285. -- they might hose the userinfo in some way that prevents the ip from being
  286. -- obtained. If so -> dump
  287. local ip = et.Info_ValueForKey( userinfo, "ip" )
  288. if ip == "" then
  289. return "missing ip"
  290. end
  291. -- printf("checkuserinfo: ip [%s]\n", ip)
  292.  
  293. -- make sure whatever is there is roughly valid while we are at it
  294. -- "localhost" may be present on a listen server. This module is not intended for listen servers.
  295. -- string.match 5.1.x
  296. -- string.find 5.0.x
  297. if string.find(ip,"^%d+%.%d+%.%d+%.%d+:%d+$") == nil then
  298. return "malformed ip"
  299. end
  300.  
  301. -- check for this to prevent exploitation of guidcheck
  302. -- note the proper solution would be for chats to always have a prefix in the console.
  303. -- Why the fuck does the server console need both
  304. -- say: [NW]reyalP: blah
  305. -- [NW]reyalP: blah
  306.  
  307. local name = et.Info_ValueForKey( userinfo, "name" )
  308. if name == "" then
  309. return "missing name"
  310. end
  311. -- printf("checkuserinfo %d name %s\n",cno,name)
  312. for _, badnamepat in ipairs(badnames) do
  313. local mstart,mend,cno = string.find(name,badnamepat)
  314. if mstart then
  315. return "name abuse"
  316. end
  317. end
  318. -- return nil
  319. end
  320.  
  321. -- 3.2.6 and earlier doesn't actually call et_ClientUserinfoChanged
  322. -- every time the userinfo changes,
  323. -- so we use et_RunFrame to check every so often
  324. -- comment this out or adjust to taste
  325. infocheck_lasttime=0
  326. infocheck_client=0
  327. -- check a client every 5 sec
  328. infocheck_freq=5000
  329.  
  330. function et_RunFrame( leveltime )
  331. if ( infocheck_lasttime + infocheck_freq > leveltime ) then
  332. return
  333. end
  334.  
  335. -- printf("infocheck %d %d\n", infocheck_client, leveltime)
  336. infocheck_lasttime = leveltime
  337. if et.gentity_get(infocheck_client, "pers.connected") ~= 0 then
  338. if et.gentity_get(infocheck_client,"ps.ping") ~= 0 then
  339. if ( et.gentity_get( infocheck_client, "inuse" ) ) then
  340. local reason = check_userinfo( infocheck_client )
  341. if ( reason ) then
  342. et.G_LogPrint(string.format("userinfocheck frame: client %d bad userinfo %s\n",infocheck_client,reason))
  343. et.trap_SetUserinfo( infocheck_client, "name\\badinfo" )
  344. et.trap_DropClient( infocheck_client, "bad userinfo", 0 )
  345. local guid = string.upper(et.Info_ValueForKey(et.trap_GetUserinfo(infocheck_client), "cl_guid"))
  346. global_ban_table[guid] = true
  347. end
  348. end
  349. end
  350. end
  351.  
  352. infocheck_client = infocheck_client + 1
  353. if ( infocheck_client >= tonumber(et.trap_Cvar_Get("sv_maxclients")) ) then
  354. infocheck_client = 0
  355. end
  356.  
  357. for cno=0, (maxclients-1) do
  358. counter[cno] = counter[cno] + 1
  359. if counter[cno] == 60000 then --lift ban after 1 minute (1 minute = 60000 ms)
  360. local guid = string.upper(et.Info_ValueForKey(et.trap_GetUserinfo(cno), "cl_guid"))
  361. global_ban_table[guid] = false
  362. end
  363. end
  364. end
  365.  
  366.  
  367. function et_ClientUserinfoChanged( cno )
  368. -- printf("clientuserinfochanged %d\n",cno)
  369. local reason = check_userinfo( cno )
  370. local guid = string.upper(et.Info_ValueForKey(et.trap_GetUserinfo(cno), "cl_guid"))
  371. if ( reason ) then
  372. et.G_LogPrint(string.format("userinfocheck infochanged: client %d bad userinfo %s\n",cno,reason))
  373. et.trap_SetUserinfo( cno, "name\\badinfo" )
  374. et.trap_DropClient( cno, "bad userinfo", 0 )
  375. global_ban_table[guid] = true
  376. end
  377. for client = 0, (maxclients - 1) do
  378. local player_guid = string.upper(et.Info_ValueForKey(et.trap_GetUserinfo(client), "cl_guid"))
  379. local player_name = et.Info_ValueForKey(et.trap_GetUserinfo(client), "name")
  380.  
  381. local player_name_cno = et.Info_ValueForKey(et.trap_GetUserinfo(cno), "name")
  382. local player_guid_cno = string.upper(et.Info_ValueForKey(et.trap_GetUserinfo(cno), "cl_guid"))
  383.  
  384. if player_name == player_name_cno and player_name_cno ~= "ETPlayer" and player_name_cno ~= "UnnamedPlayer" and cno ~= client then
  385. et.G_LogPrint(string.format("userinfocheck: client %d badinfo %s\n",cno,"duplictae name"))
  386. et.trap_DropClient( cno, "duplicate name", 0 )
  387. global_ban_table[guid] = true
  388. elseif player_guid == player_guid_cno and player_guid ~= "NO_GUID" and player_guid ~= "UNKNOWN" and cno ~= client then
  389. et.G_LogPrint(string.format("userinfocheck: client %d badinfo %s\n",cno,"duplictae guid"))
  390. et.trap_DropClient( cno, "duplicate guid", 0 )
  391. global_ban_table[guid] = true
  392. end
  393.  
  394. if (string.find(player_name_cno, "(%d+).(%d+).(%d+).(%d+)") or string.find(player_name_cno, "^.(%w+):(%d+)") or string.find(player_name_cno, "(%d+)%s(%d+)%s(%d+)%s(%d+)%s(%d+)")) and serverpassword == "" then
  395. et.G_LogPrint(string.format("userinfocheck: client %d badinfo %s\n",cno,"ip name"))
  396. et.trap_DropClient( cno, "ip name", 0 )
  397. global_ban_table[guid] = true
  398. end
  399. end
  400. end
  401.  
  402.  
  403. -- prevent etpro guid borkage
  404. -- version: 1
  405. -- TY pants
  406.  
  407. -- default to kick with no temp ban for now
  408. DEF_GUIDCHECK_BANTIME = 0
  409.  
  410. function bad_guid(cno,reason)
  411. local bantime = tonumber(et.trap_Cvar_Get( "guidcheck_bantime" ))
  412. if not bantime or bantime < 0 then
  413. bantime = DEF_GUIDCHECK_BANTIME
  414. end
  415.  
  416. et.G_LogPrint(string.format("guidcheck: client %d bad GUID %s\n",cno,reason))
  417. -- we don't send them the reason. They can figure it out for themselves.
  418. et.trap_DropClient(cno,"You are banned from this server",0)
  419. local guid = string.upper(et.Info_ValueForKey(et.trap_GetUserinfo(cno), "cl_guid"))
  420. global_ban_table[guid] = true
  421. end
  422.  
  423. function check_guid_line(text)
  424. --find a GUID line
  425. local guid,netname
  426. local mstart,mend,cno = string.find(text,"^etpro IAC: (%d+) GUID %[")
  427. if not mstart then
  428. return
  429. end
  430. text=string.sub(text,mend+1)
  431. --GUID] [NETNAME]\n
  432. mstart,mend,guid = string.find(text,"^([^%]]*)%] %[")
  433. if not mstart then
  434. bad_guid(cno,"couldn't parse guid")
  435. return
  436. end
  437. --NETNAME]\n
  438. text=string.sub(text,mend+1)
  439.  
  440. netname = et.gentity_get(cno,"pers.netname")
  441.  
  442. mstart,mend = string.find(text,netname,1,true)
  443. if not mstart or mstart ~= 1 then
  444. bad_guid(cno,"couldn't parse name")
  445. return
  446. end
  447.  
  448. text=string.sub(text,mend+1)
  449. if text ~= "]\n" then
  450. bad_guid(cno,"trailing garbage")
  451. return
  452. end
  453.  
  454. -- printf("guidcheck: etpro GUID %d %s %s\n",cno,guid,netname)
  455.  
  456. -- {N} is too complicated!
  457. mstart,mend = string.find(guid,"^%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x$")
  458. if not mstart then
  459. bad_guid(cno,"malformed")
  460. return
  461. end
  462. -- printf("guidcheck: OK\n")
  463. end
  464.  
  465. -- limit fakeplayers DOS
  466. -- http://aluigi.altervista.org/fakep.htm
  467. -- used if cvar is not set
  468. -- confugration:
  469. -- set ip_max_clients cvar as desired. If not set, defaults to the value below.
  470. --FAKEPLIMIT_VERSION = "1.0"
  471. DEF_IP_MAX_CLIENTS = 3
  472.  
  473. et.G_Printf = function(...)
  474. et.G_Print(string.format(unpack(arg)))
  475. end
  476.  
  477. function IPForClient(cno)
  478. -- TODO listen servers may be 'localhost'
  479. local userinfo = et.trap_GetUserinfo( cno )
  480. if userinfo == "" then
  481. return ""
  482. end
  483. local ip = et.Info_ValueForKey( userinfo, "ip" )
  484. -- find IP and strip port
  485. local ipstart, ipend, ipmatch = string.find(ip,"(%d+%.%d+%.%d+%.%d+)")
  486. -- don't error out if we don't match an ip
  487. if not ipstart then
  488. return ""
  489. end
  490. -- et.G_Printf("IPForClient(%d) = [%s]\n",cno,ipmatch)
  491. return ipmatch
  492. end
  493.  
  494. function et_ClientConnect( cno, firstTime, isBot )
  495. -- userinfocheck stuff. Do this before IP limit
  496. -- printf("connect %d\n",cno)
  497. if et.gentity_get(cno,"ps.ping") ~= 0 then --allow omnibots
  498.  
  499. local reason = check_userinfo( cno )
  500. if ( reason ) then
  501. et.G_LogPrint(string.format("userinfocheck connect: client %d bad userinfo %s\n",cno,reason))
  502. return "bad userinfo"
  503. end
  504.  
  505. -- note IP validity should be enforced by userinfocheck stuff
  506. local ip = IPForClient( cno )
  507. local count = 1 -- we count as the first one
  508. local max = tonumber(et.trap_Cvar_Get( "ip_max_clients" ))
  509. if not max or max <= 0 then
  510. max = DEF_IP_MAX_CLIENTS
  511. end
  512. -- et.G_Printf("firstTime %d\n",firstTime);
  513. -- it's probably safe to only do this on firsttime, but checking
  514. -- every time doesn't hurt much
  515.  
  516. -- validate userinfo to filter out the people blindly using luigi's code
  517. local userinfo = et.trap_GetUserinfo( cno )
  518. -- et.G_Printf("userinfo: [%s]\n",userinfo)
  519. if et.Info_ValueForKey( userinfo, "rate" ) == "" then
  520. et.G_Printf(""..modname..": invalid userinfo from %s\n",ip)
  521. return "invalid connection"
  522. end
  523.  
  524. for i = 0, et.trap_Cvar_Get("sv_maxclients") - 1 do
  525. -- pers.connected is set correctly for fake players
  526. -- can't rely on userinfo being empty
  527. if i ~= cno and et.gentity_get(i,"pers.connected") > 0 and ip == IPForClient(i) then
  528. count = count + 1
  529. if count > max then
  530. et.G_Printf(""..modname..": too many connections from %s\n",ip)
  531. -- TODO should we drop / ban all connections from this IP ?
  532. return string.format("only %d connections per IP are allowed on this server",max)
  533. end
  534. end
  535. end
  536. end
  537. local guid = string.upper(et.Info_ValueForKey(et.trap_GetUserinfo(cno), "cl_guid"))
  538. if global_ban_table[guid] then
  539. return "You are disallowed to reconnect for one minute"
  540. end
  541. end
  542. -- NoAutoDeclare()
  543.  
  544. -- Perlo_0ung
  545. -- rifle module
  546.  
  547. function et_ClientSpawn(cno,revived)
  548. if revived == 0 and et.gentity_get(cno, "sess.playerType") == 2 then
  549. et.gentity_set(cno,"ps.ammo",39,3)
  550. et.gentity_set(cno,"ps.ammo",40,3)
  551. end
  552. end
  553.  
  554. -- Perlo_0ung?!
  555. -- votefix version 2
  556. -- lowers the mapname in "callvote map" command. This fixes the bug with the wrong mapscripthash of .script files.
  557. -- note mapscripts (.script) have to be lower caps
  558. et.CS_VOTE_STRING = 7
  559.  
  560. function et_Print(text)
  561. check_guid_line(text)
  562.  
  563. local t = ParseString(text) --Vote Passed: Change map to suppLY
  564. if t[2] == "Passed:" and t[4] == "map" then
  565. if string.find(t[6],"%u") == nil or t[6] ~= getCS() then return 1 end
  566. local mapfixed = string.lower(t[6])
  567. et.trap_SendConsoleCommand( et.EXEC_APPEND, "ref map " .. mapfixed .. "\n" )
  568. end
  569. end
  570.  
  571. -- helper functions
  572.  
  573. function ParseString(inputString)
  574. local i = 1
  575. local t = {}
  576. for w in string.gfind(inputString, "([^%s]+)%s*") do
  577. t[i]=w
  578. i=i+1
  579. end
  580. return t
  581. end
  582.  
  583. function getCS()
  584. local cs = et.trap_GetConfigstring(et.CS_VOTE_STRING)
  585. local t = ParseString(cs)
  586. return t[4]
  587. end
  588.  
  589. function getWeapon(playerID)
  590. return et.gentity_get(playerID, "sess.playerWeapon")
  591. end
  592.  
  593. function getTeam(playerID)
  594. return et.gentity_get(playerID, "sess.sessionTeam")
  595. end
  596.  
  597. function setPlayerWeapon(playerID , weapon)
  598. et.gentity_set(playerID, "sess.latchPlayerWeapon", weapon)
  599. end