endlessh_scoreboard.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501
  1. #!/usr/bin/env python3
  2. ## Written by demure (demuredemeanor)
  3. ## This tool makes generates a static html page for showing an endlessh scoreboard.
  4. ## This program depends on ... to work
  5. ## TODO: Add flag to exclude IPs
  6. ## TODO: Add flag to disable GeoIp and sleep
  7. ## TODO: Add flag to change log location
  8. ## TODO: Add flag to change output file
  9. ## TODO: Use config file
  10. ## TODO: Sort default output, maybe by Org
  11. ## NOTE: best point to apply is the html table string
  12. ### Imports ### {{{
  13. ## Imports
  14. import sys ## Need for argv
  15. import os ## For file open
  16. import datetime ## For log line time and math
  17. import re ## For RegEx, <3
  18. import time ## For sleep between geoip lookups
  19. import inspect ## For cleaning up multi line strings
  20. import hashlib ## For hiding the IPs
  21. import requests ## For curl type requests
  22. ### End Imports ### }}}
  23. ### f_open_log_file ### {{{
  24. def f_open_log_file():
  25. Log_Location = '/var/log/' ## Declare log path
  26. Log_Name = 'endlessh.log' ## Declare log name
  27. ## Open file
  28. for filename in os.listdir(Log_Location):
  29. if filename == Log_Name:
  30. fh = open(os.path.join(Log_Location, Log_Name), "r")
  31. FilterArray = f_parse_log(fh) ## Parse log and store
  32. # FilterArray.sort() ## Sort array to make next part easier
  33. fh.close() ## Clean up and close file
  34. return(FilterArray) ## Return FilterArray
  35. ### End f_open_log_file ### }}}
  36. ### f_parse_log ### {{{
  37. def f_parse_log(fh):
  38. ## Declare variables
  39. CutOff = int(datetime.datetime.utcnow().strftime("%s")) - int(datetime.timedelta(days=7).total_seconds())
  40. LineParse = []
  41. LineHost = ""
  42. LineTime = ""
  43. FilterArray = []
  44. for x in fh: ## Iterate over file
  45. LineParse = x.split() ## Split line into array
  46. if LineParse[1] == "CLOSE": ## Only process CLOSE lines
  47. ## Convert line timestamp to epoch
  48. LineTime = int(datetime.datetime.strptime(LineParse[0], '%Y-%m-%dT%H:%M:%S.%fZ').strftime("%s"))
  49. if LineTime > CutOff: ## Discard lines that are too old
  50. if re.match('host=::ffff:', LineParse[2]) is not None:
  51. LineHost = re.sub('host=::ffff:', "", LineParse[2]) ## Clean up IPv4 host
  52. else:
  53. LineHost = re.sub('host=', "", LineParse[2]) ## Clean up IPv6 host
  54. LineTime = re.sub('time=', "", LineParse[5]) ## Clean up time
  55. FilterArray.append((LineHost, LineTime)) ## Add to Array
  56. return(FilterArray) ## Return FilterArray
  57. ### End f_parse_log ###}}}
  58. ### f_dict_computation ### {{{
  59. def f_dict_computation(FilterArray):
  60. ## Declare variables
  61. HostDict = dict()
  62. Host = ""
  63. HostTime = ""
  64. GeoLookup = ""
  65. for x in FilterArray:
  66. Host = x[0]
  67. HostTime = float(x[1])
  68. if Host in HostDict: ## Test if Host already in HostDict
  69. HostDict[Host]["count"] += 1
  70. if HostTime < HostDict[Host]["min"]: ## Replace min if new HostTime is less
  71. HostDict[Host]["min"] = HostTime
  72. if HostTime > HostDict[Host]["max"]: ## Replace max if new HostTime is more
  73. HostDict[Host]["max"] = HostTime
  74. HostDict[Host]["sum"] += HostTime ## Add current HostTime to sum
  75. HostDict[Host]["ave"] = HostDict[Host]["sum"] / HostDict[Host]["count"]
  76. else: ## Initialize if not already in HostDict
  77. HostDict[Host] = dict()
  78. HostDict[Host]["count"] = 1
  79. HostDict[Host]["min"] = HostTime
  80. HostDict[Host]["max"] = HostTime
  81. HostDict[Host]["sum"] = HostTime
  82. HostDict[Host]["ave"] = HostTime / 1
  83. ### GeoIp lookup ### {{{
  84. ## Get newline output, splitline into array
  85. ## NOTE: Delay must be enabled if real lookup enabled!
  86. # time.sleep(0.5) ## Add delay to not exceed lookup limit
  87. time.sleep(1) ## Add delay to not exceed lookup limit
  88. GeoLookup = requests.get('http://ip-api.com/line/' + Host + '?fields=status,message,country,regionName,org').text.splitlines()
  89. # GeoLookup = ['success', 'DEBUG_Country', 'DEBUG_State', 'DEBUG_Org'] ## DEBUG pass
  90. # GeoLookup = ['fail', 'Reserved'] ## DEBUG fail
  91. ## Check for lack of response
  92. if not bool(GeoLookup) == False:
  93. ## Check for successful lookup
  94. if GeoLookup[0] == 'success':
  95. HostDict[Host]["country"] = GeoLookup[1]
  96. HostDict[Host]["region"] = GeoLookup[2]
  97. HostDict[Host]["org"] = GeoLookup[3]
  98. else:
  99. ## If lookup had issues, set null
  100. HostDict[Host]["country"] = "Error2"
  101. HostDict[Host]["region"] = ""
  102. HostDict[Host]["org"] = ""
  103. else:
  104. ## If lookup had issues, set null
  105. HostDict[Host]["country"] = "Error1"
  106. HostDict[Host]["region"] = ""
  107. HostDict[Host]["org"] = ""
  108. ### End GeoIp lookup ### }}}
  109. return(HostDict)
  110. ### End f_dict_computation ### }}}
  111. ### f_dict_total ### {{{
  112. def f_dict_total(HostDict):
  113. ## Declare variables
  114. HighDict = dict()
  115. ServerDict = dict()
  116. Uniq = 0
  117. ## Process for HighDict
  118. for Host in HostDict:
  119. Time = HostDict[Host]["max"]
  120. Country = HostDict[Host]["country"]
  121. Count = HostDict[Host]["count"]
  122. Ave = HostDict[Host]["ave"]
  123. Sum = HostDict[Host]["sum"]
  124. if 'count' not in HighDict: ## Initialize if not set
  125. HighDict["count"] = dict(Count=Count,Host=Host,Country=Country)
  126. HighDict["max"] = dict(Time=Time,Host=Host,Country=Country)
  127. HighDict["ave"] = dict(Time=0,Host="None Qualified",Country="") ## First run not qualified to win
  128. HighDict["sum"] = dict(Time=0,Host="None Qualified",Country="") ## First run not qualified to win
  129. else:
  130. if Count > HighDict["count"]["Count"]: ## Update count if greater
  131. HighDict["count"] = dict(Count=Count,Host=Host,Country=Country)
  132. if Time > HighDict["max"]["Time"]: ## Update max time if greater
  133. HighDict["max"] = dict(Time=Time,Host=Host,Country=Country)
  134. if Ave > HighDict["ave"]["Time"] and Count >= 3: ## Update ave time if greater, and count three or more
  135. HighDict["ave"] = dict(Time=Ave,Host=Host,Country=Country)
  136. if Sum > HighDict["sum"]["Time"] and Count >= 2: ## Update total time if greater, and count two or more
  137. HighDict["sum"] = dict(Time=Sum,Host=Host,Country=Country)
  138. ## Process for ServerDict
  139. for Host in HostDict:
  140. Count = HostDict[Host]["count"]
  141. Sum = HostDict[Host]["sum"]
  142. if 'count' not in ServerDict: ## Initialize if not set
  143. ServerDict["count"] = Count
  144. ServerDict["sum"] = Sum
  145. ServerDict["uniq"] = 1
  146. else:
  147. ServerDict["count"] += Count
  148. ServerDict["sum"] += Sum
  149. ServerDict["uniq"] += 1
  150. return(HighDict,ServerDict)
  151. ### End f_dict_total ### }}}
  152. ### f_gen_html ### {{{
  153. def f_gen_html(HighDict,HostDict,ServerDict):
  154. ## Declare variables
  155. s_main_table = ""
  156. ### s_html_top ### {{{
  157. s_html_top = inspect.cleandoc("""
  158. <html>
  159. <center>
  160. <body>
  161. <style> table {border-spacing: 0; border: 1px solid black; font-family: monospace;} th {cursor: pointer;} th, td {border: 1px solid black; border-collapse: collapse; padding: 2px;}</style>
  162. <h2>Tar Pit Score Board</h2>
  163. <h3>Info</h3>
  164. <p>Here is a little Score Board of <strike>ssh attempts</strike> players from the last seven days.<BR>
  165. To <i>"keep things fair"</i>, players who try too many times are given a time out.</p>
  166. """)
  167. ### End s_html_top ### }}}
  168. ### s_top_table ### {{{
  169. s_top_table = """
  170. <h3>Top Players</h3>
  171. <table id="topTable">
  172. <tr><th>Category</th><th>Value</th><th>Host</th><th>Country</th></tr>
  173. <tr><td>Most Attempts</td><td align=right>{Count}</td><td>{CHost}</td><td>{CCountry}</td></tr>
  174. <tr><td>Longest Conn</td><td align=right>{LTime}</td><td>{LHost}</td><td>{LCountry}</td></tr>
  175. <tr><td>Highest Ave*</td><td align=right>{ATime}</td><td>{AHost}</td><td>{ACountry}</td></tr>
  176. <tr><td>Highest Total*</td><td align=right>{TTime}</td><td>{THost}</td><td>{TCountry}</td></tr>
  177. </table>
  178. <p><small>* Top Average requires three connections, <BR>and Top Total two connections to qualify.</small></p>
  179. """.format(Count=HighDict["count"]["Count"],
  180. CHost=HighDict["count"]["Host"],
  181. CCountry=HighDict["count"]["Country"],
  182. LTime=f_mins(HighDict["max"]["Time"]),
  183. LHost=HighDict["max"]["Host"],
  184. LCountry=HighDict["max"]["Country"],
  185. ATime=f_mins(HighDict["ave"]["Time"]),
  186. AHost=HighDict["ave"]["Host"],
  187. ACountry=HighDict["ave"]["Country"],
  188. TTime=f_mins(HighDict["sum"]["Time"]),
  189. THost=HighDict["sum"]["Host"],
  190. TCountry=HighDict["sum"]["Country"])
  191. ### End s_top_table ### }}}
  192. ### s_main_table_pre ### {{{
  193. s_main_table_pre = """
  194. <h3>Player List</h3>
  195. <p>In the past seven days there have been {TPlayers} players, and {TAttempts} connections.<BR>
  196. The average connection time is {TAve} minutes,<BR>
  197. with a total accumulated time of {TTime} minutes.<BR>
  198. <small>All <b>Conn</b>ection lengths are in minutes.<BR>
  199. Number columns may be sorted by clicking on their header.</small></p>
  200. <table id="mainTable">
  201. <!--<tr><th></th><th colspan="3" scope="colgroup">Geo Data</th><th></th><th colspan="4" scope="colgroup">Time (Minutes)</th></tr>-->
  202. <tr><th onclick="sortTable(0)">Host</th><th onclick="sortTable(1)">Country</th><th onclick="sortTable(2)">Region</th><th onclick="sortTable(3)">Org</th><th onclick="sortTable(4)">Attempts</th><th onclick="sortTable(5)">Shortest Conn</th><th onclick="sortTable(6)">Longest Conn</th><th onclick="sortTable(7)">Average Conn</th><th onclick="sortTable(8)">Total Conns</th></tr>""".format(TAttempts=ServerDict["count"],
  203. TPlayers=ServerDict["uniq"],
  204. TAve=f_mins(ServerDict["sum"] / ServerDict["count"]),
  205. TTime=f_mins(ServerDict["sum"]))
  206. ### End s_main_table_pre ### }}}
  207. ### s_main_table ### {{{
  208. for Host in HostDict:
  209. ## Make easy to read in code without added newlines
  210. s_main_table_temp = (
  211. "<tr>"
  212. "<td>{Host}</td>"
  213. "<td>{Country}</td>"
  214. "<td>{Region}</td>"
  215. "<td>{Org}</td>"
  216. "<td align=right>{Count}</td>"
  217. "<td align=right>{Min}</td>"
  218. "<td align=right>{Max}</td>"
  219. "<td align=right>{Ave:.3f}</td>"
  220. "<td align=right>{Sum:.3f}</td>"
  221. "</tr>"
  222. ).format(Host=Host,
  223. Country=HostDict[Host]["country"],
  224. Region=HostDict[Host]["region"],
  225. Org=HostDict[Host]["org"],
  226. Count=HostDict[Host]["count"],
  227. Min=f_mins(HostDict[Host]["min"]),
  228. Max=f_mins(HostDict[Host]["max"]),
  229. Ave=f_mins(HostDict[Host]["ave"]),
  230. Sum=f_mins(HostDict[Host]["sum"]))
  231. s_main_table = """{0}
  232. {1}""".format(s_main_table, s_main_table_temp)
  233. ### End s_main_table ### }}}
  234. ### s_main_table_post ### {{{
  235. ## This was broken off of s_html_bottom so that the .format didn't error
  236. ## No inspect.cleandoc as it would collapse to beginning of line
  237. s_main_table_post = """
  238. </table>
  239. <p>This page is brought to you by a python <a href="https://notabug.org/demure/scripts/src/master/endlessh_scoreboard.py">script</a>,<BR>
  240. which parses an <a href="https://github.com/skeeto/endlessh">endlessh</a> log file.</p>
  241. <p><b>Last updated {Date}</b></p>
  242. """.format(Date=datetime.datetime.utcnow().strftime("%a %d %b %Y %H:%M:%S UTC"))
  243. ### End s_main_table_post ### }}}
  244. ### s_html_bottom ### {{{
  245. s_html_bottom = inspect.cleandoc("""
  246. <!-- from https://www.w3schools.com/howto/howto_js_sort_table.asp -->
  247. <script>
  248. function sortTable(n) {
  249. var table, rows, switching, i, x, y, shouldSwitch, dir, switchcount = 0;
  250. table = document.getElementById("mainTable");
  251. switching = true;
  252. dir = "asc";
  253. while (switching) {
  254. switching = false;
  255. rows = table.rows;
  256. for (i = 1; i < (rows.length - 1); i++) {
  257. shouldSwitch = false;
  258. x = rows[i].getElementsByTagName("TD")[n];
  259. y = rows[i + 1].getElementsByTagName("TD")[n];
  260. if (dir == "asc") {
  261. if (Number(x.innerHTML) < Number(y.innerHTML)) {
  262. shouldSwitch = true;
  263. break;
  264. }
  265. } else if (dir == "desc") {
  266. if (Number(x.innerHTML) > Number(y.innerHTML)) {
  267. shouldSwitch = true;
  268. break;
  269. }
  270. }
  271. }
  272. if (shouldSwitch) {
  273. rows[i].parentNode.insertBefore(rows[i + 1], rows[i]);
  274. switching = true;
  275. switchcount ++;
  276. } else {
  277. if (switchcount == 0 && dir == "asc") {
  278. dir = "desc";
  279. switching = true;
  280. }
  281. }
  282. }
  283. }
  284. </script>
  285. </body>
  286. </center>
  287. </html>
  288. """)
  289. ### End s_html_bottom ### }}}
  290. return(s_html_top,s_top_table,s_main_table_pre,s_main_table,s_main_table_post,s_html_bottom)
  291. ### End f_gen_html ### }}}
  292. ### f_write_out_file ### {{{
  293. def f_write_out_file(s_output_html):
  294. Out_Path = '/var/www/pit/index.html' ## Set path to output
  295. OutFile = open(Out_Path, "w") ## Open file for writing
  296. for String in s_output_html: ## Iterate over strings
  297. OutFile.write(String) ## Write each string in order
  298. OutFile.close() ## Close file
  299. ### End f_write_out_file ### }}}
  300. ### f_hash ### {{{
  301. ## Make the Hashing a simple function call
  302. def f_hash(Host):
  303. ## Has, and salt with repeatability
  304. return(hashlib.sha512(os.uname()[1].encode('utf-8') + Host.encode('utf-8')).hexdigest()[0:16])
  305. ### End f_hash ### }}}
  306. ### f_mins ### {{{
  307. ## Convert and round seconds to minutes
  308. def f_mins(Time):
  309. return(round((Time / 60), 3))
  310. ### End f_mins ### }}}
  311. ## DEBUG
  312. ### f_debug_print ### {{{
  313. def f_debug_print(Mode,HostDict,HighDict,ServerDict):
  314. if Mode == 0 or Mode == 1:
  315. for Host in HostDict:
  316. ## Print host matches
  317. print(("Host: {Host}, "
  318. "Country: {Country}, "
  319. "Region: {Region}, "
  320. "Org: {Org}, "
  321. "Count: {Count}, "
  322. "Min: {Min}, "
  323. "Max: {Max}, "
  324. "Ave: {Ave:.3f}, "
  325. "Sum: {Sum:.3f}"
  326. ).format(Host=Host,
  327. Country=HostDict[Host]["country"],
  328. Region=HostDict[Host]["region"],
  329. Org=HostDict[Host]["org"],
  330. Count=HostDict[Host]["count"],
  331. Min=f_mins(HostDict[Host]["min"]),
  332. Max=f_mins(HostDict[Host]["max"]),
  333. Ave=f_mins(HostDict[Host]["ave"]),
  334. Sum=f_mins(HostDict[Host]["sum"])))
  335. if Mode == 0 or Mode == 2:
  336. ## Print High Scores
  337. print(("Most Attempts: {Count}, "
  338. "Host: {Host}, "
  339. "Country: {Country}"
  340. ).format(Count=HighDict["count"]["Count"],
  341. Host=HighDict["count"]["Host"],
  342. Country=HighDict["count"]["Country"]))
  343. print(("Longest Conn: {Time}, "
  344. "Host: {Host}, "
  345. "Country: {Country}"
  346. ).format(Time=f_mins(HighDict["max"]["Time"]),
  347. Host=HighDict["max"]["Host"],
  348. Country=HighDict["max"]["Country"]))
  349. print(("Highest Average: {Time}, "
  350. "Host: {Host}, "
  351. "Country: {Country}"
  352. ).format(Time=f_mins(HighDict["ave"]["Time"]),
  353. Host=HighDict["ave"]["Host"],
  354. Country=HighDict["ave"]["Country"]))
  355. print(("Highest Total: {Time}, "
  356. "Host: {Host}, "
  357. "Country: {Country}"
  358. ).format(Time=f_mins(HighDict["sum"]["Time"]),
  359. Host=HighDict["sum"]["Host"],
  360. Country=HighDict["sum"]["Country"]))
  361. if Mode == 0 or Mode == 3:
  362. print(("Total Attempts: {TAttempts}, "
  363. "Total Players: {TPlayers}, "
  364. "Total Ave Conn: {TAve}, "
  365. "Total Conn Time: {TTime}"
  366. ).format(TAttempts=ServerDict["count"],
  367. TPlayers=ServerDict["uniq"],
  368. TAve=f_mins(ServerDict["sum"] / ServerDict["count"]),
  369. TTime=f_mins(ServerDict["sum"])))
  370. ### f_debug_print ### }}}
  371. ### Arguments ### {{{
  372. ## Parse the command line arguments
  373. def parse_argv(argv):
  374. arg1=argv[1]
  375. ## Single augment
  376. if len(argv) == 2:
  377. ## Option help
  378. if arg1 == "-h" or arg1 == "--help" or arg1 == "help":
  379. msg_help()
  380. return
  381. ## Option version
  382. if arg1 == "-V" or arg1 == "--version" or arg1 == "version":
  383. msg_version()
  384. return
  385. ## If they get here, the didn't match; print help.
  386. print("["+arg1+"] invalid use, or invalid option!")
  387. msg_help()
  388. return
  389. ## Two Augments
  390. elif len(argv) > 2:
  391. arg2=argv[2]
  392. ## Option mute
  393. if arg1 == "-e" or arg1 == "--exclude" or arg1 == "exclude":
  394. if re.match("^(\d+)$", arg2):
  395. mute_toggle(arg2)
  396. else:
  397. print("["+arg2+"] is not a valid sink input!")
  398. msg_help()
  399. return
  400. return
  401. ### End Arguments ### }}}
  402. ## Print help message
  403. def msg_help():
  404. help_msg = inspect.cleandoc("""
  405. Usage: endlessh_scoreboard.py [OPTION...]
  406. When run without OPTION, will DO A THING
  407. When run with BLAH, WILL DO OTHER THING
  408. -e, --exclude Exclude IP [ip1] [ip2] [ip3]...
  409. -V, --version Display version information
  410. -h, --help Display this help message
  411. """)
  412. print(help_msg)
  413. quit()
  414. return
  415. ## Print version number message
  416. def msg_version():
  417. version_msg="endlessh_scoreboard version: 0.3.2"
  418. print(version_msg)
  419. quit()
  420. return
  421. ## Main
  422. def main(argv):
  423. ## Process input, if given
  424. if len(argv) > 1:
  425. parse_argv(argv)
  426. FilterArray = f_open_log_file()
  427. HostDict = f_dict_computation(FilterArray)
  428. HighDict,ServerDict = f_dict_total(HostDict)
  429. s_output_html=f_gen_html(HighDict,HostDict,ServerDict)
  430. f_write_out_file(s_output_html)
  431. # f_debug_print(3,HostDict,HighDict,ServerDict)
  432. if __name__ == '__main__':
  433. main(sys.argv)