123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486 |
- #!/usr/bin/env python3
- ## Written by demure (demuredemeanor)
- ## This tool makes generates a static html page for showing an endlessh scoreboard.
- ## This program depends on ... to work
- ## TODO: Add flag to exclude IPs
- ## TODO: Add flag to disable GeoIp and sleep
- ## TODO: Add flag to change log location
- ## TODO: Add flag to change output file
- ## TODO: Use config file
- ## TODO: Sort default output, maybe by Org
- ## NOTE: best point to apply is the html table string
- ### Imports ### {{{
- ## Imports
- import sys ## Need for argv
- import os ## For file open
- import datetime ## For log line time and math
- import re ## For RegEx, <3
- import time ## For sleep between geoip lookups
- import inspect ## For cleaning up multi line strings
- import hashlib ## For hiding the IPs
- import requests ## For curl type requests
- ### End Imports ### }}}
- ### f_open_log_file ### {{{
- def f_open_log_file():
- Log_Location = '/var/log/' ## Declare log path
- Log_Name = 'endlessh.log' ## Declare log name
- ## Open file
- for filename in os.listdir(Log_Location):
- if filename == Log_Name:
- fh = open(os.path.join(Log_Location, Log_Name), "r")
- FilterArray = f_parse_log(fh) ## Parse log and store
- # FilterArray.sort() ## Sort array to make next part easier
- fh.close() ## Clean up and close file
- return(FilterArray) ## Return FilterArray
- ### End f_open_log_file ### }}}
- ### f_parse_log ### {{{
- def f_parse_log(fh):
- ## Declare variables
- CutOff = int(datetime.datetime.utcnow().strftime("%s")) - int(datetime.timedelta(days=7).total_seconds())
- LineParse = []
- LineHost = ""
- LineTime = ""
- FilterArray = []
- for x in fh: ## Iterate over file
- LineParse = x.split() ## Split line into array
- if LineParse[1] == "CLOSE": ## Only process CLOSE lines
- ## Convert line timestamp to epoch
- LineTime = int(datetime.datetime.strptime(LineParse[0], '%Y-%m-%dT%H:%M:%S.%fZ').strftime("%s"))
- if LineTime > CutOff: ## Discard lines that are too old
- if re.match('host=::ffff:', LineParse[2]) is not None:
- LineHost = re.sub('host=::ffff:', "", LineParse[2]) ## Clean up IPv4 host
- else:
- LineHost = re.sub('host=', "", LineParse[2]) ## Clean up IPv6 host
- LineTime = re.sub('time=', "", LineParse[5]) ## Clean up time
- FilterArray.append((LineHost, LineTime)) ## Add to Array
- return(FilterArray) ## Return FilterArray
- ### End f_parse_log ###}}}
- ### f_dict_computation ### {{{
- def f_dict_computation(FilterArray):
- ## Declare variables
- HostDict = dict()
- Host = ""
- HostTime = ""
- GeoLookup = ""
- for x in FilterArray:
- Host = x[0]
- HostTime = float(x[1])
- if Host in HostDict: ## Test if Host already in HostDict
- HostDict[Host]["count"] += 1
- if HostTime < HostDict[Host]["min"]: ## Replace min if new HostTime is less
- HostDict[Host]["min"] = HostTime
- if HostTime > HostDict[Host]["max"]: ## Replace max if new HostTime is more
- HostDict[Host]["max"] = HostTime
- HostDict[Host]["sum"] += HostTime ## Add current HostTime to sum
- HostDict[Host]["ave"] = HostDict[Host]["sum"] / HostDict[Host]["count"]
- else: ## Initialize if not already in HostDict
- HostDict[Host] = dict()
- HostDict[Host]["count"] = 1
- HostDict[Host]["min"] = HostTime
- HostDict[Host]["max"] = HostTime
- HostDict[Host]["sum"] = HostTime
- HostDict[Host]["ave"] = HostTime / 1
- ### GeoIp lookup ### {{{
- ## Get newline output, splitline into array
- ## NOTE: Delay must be enabled if real lookup enabled!
- # time.sleep(0.5) ## Add delay to not exceed lookup limit
- time.sleep(1) ## Add delay to not exceed lookup limit
- GeoLookup = requests.get('http://ip-api.com/line/' + Host + '?fields=status,message,country,regionName,org').text.splitlines()
- # GeoLookup = ['success', 'DEBUG_Country', 'DEBUG_State', 'DEBUG_Org'] ## DEBUG pass
- # GeoLookup = ['fail', 'Reserved'] ## DEBUG fail
- ## Check for lack of response
- if not bool(GeoLookup) == False:
- ## Check for successful lookup
- if GeoLookup[0] == 'success':
- HostDict[Host]["country"] = GeoLookup[1]
- HostDict[Host]["region"] = GeoLookup[2]
- HostDict[Host]["org"] = GeoLookup[3]
- else:
- ## If lookup had issues, set null
- HostDict[Host]["country"] = "Error2"
- HostDict[Host]["region"] = ""
- HostDict[Host]["org"] = ""
- else:
- ## If lookup had issues, set null
- HostDict[Host]["country"] = "Error1"
- HostDict[Host]["region"] = ""
- HostDict[Host]["org"] = ""
- ### End GeoIp lookup ### }}}
- return(HostDict)
- ### End f_dict_computation ### }}}
- ### f_dict_total ### {{{
- def f_dict_total(HostDict):
- ## Declare variables
- HighDict = dict()
- ServerDict = dict()
- Uniq = 0
- ## Process for HighDict
- for Host in HostDict:
- Time = HostDict[Host]["max"]
- Country = HostDict[Host]["country"]
- Count = HostDict[Host]["count"]
- Ave = HostDict[Host]["ave"]
- Sum = HostDict[Host]["sum"]
- if 'count' not in HighDict: ## Initialize if not set
- HighDict["count"] = dict(Count=Count,Host=Host,Country=Country)
- HighDict["max"] = dict(Time=Time,Host=Host,Country=Country)
- HighDict["ave"] = dict(Time=0,Host="None Qualified",Country="") ## First run not qualified to win
- HighDict["sum"] = dict(Time=0,Host="None Qualified",Country="") ## First run not qualified to win
- else:
- if Count > HighDict["count"]["Count"]: ## Update count if greater
- HighDict["count"] = dict(Count=Count,Host=Host,Country=Country)
- if Time > HighDict["max"]["Time"]: ## Update max time if greater
- HighDict["max"] = dict(Time=Time,Host=Host,Country=Country)
- if Ave > HighDict["ave"]["Time"] and Count >= 3: ## Update ave time if greater, and count three or more
- HighDict["ave"] = dict(Time=Ave,Host=Host,Country=Country)
- if Sum > HighDict["sum"]["Time"] and Count >= 2: ## Update total time if greater, and count two or more
- HighDict["sum"] = dict(Time=Sum,Host=Host,Country=Country)
- ## Process for ServerDict
- for Host in HostDict:
- Count = HostDict[Host]["count"]
- Sum = HostDict[Host]["sum"]
- if 'count' not in ServerDict: ## Initialize if not set
- ServerDict["count"] = Count
- ServerDict["sum"] = Sum
- ServerDict["uniq"] = 1
- else:
- ServerDict["count"] += Count
- ServerDict["sum"] += Sum
- ServerDict["uniq"] += 1
- return(HighDict,ServerDict)
- ### End f_dict_total ### }}}
- ### f_gen_html ### {{{
- def f_gen_html(HighDict,HostDict,ServerDict):
- ## Declare variables
- s_main_table = ""
- ### s_html_top ### {{{
- s_html_top = inspect.cleandoc("""
- <html>
- <center>
- <body>
- <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>
- <h2>Tar Pit Score Board</h2>
- <h3>Info</h3>
- <p>Here is a little Score Board of <strike>ssh attempts</strike> players from the last seven days.<BR>
- To <i>"keep things fair"</i>, players who try too many times are given a time out.</p>
- """)
- ### End s_html_top ### }}}
- ### s_top_table ### {{{
- s_top_table = """
- <h3>Top Players</h3>
- <table id="topTable">
- <tr><th>Category</th><th>Value</th><th>Host</th><th>Country</th></tr>
- <tr><td>Most Attempts</td><td align=right>{Count}</td><td>{CHost}</td><td>{CCountry}</td></tr>
- <tr><td>Longest Conn</td><td align=right>{LTime}</td><td>{LHost}</td><td>{LCountry}</td></tr>
- <tr><td>Highest Ave*</td><td align=right>{ATime}</td><td>{AHost}</td><td>{ACountry}</td></tr>
- <tr><td>Highest Total*</td><td align=right>{TTime}</td><td>{THost}</td><td>{TCountry}</td></tr>
- </table>
- <p><small>* Top Average requires three connections, <BR>and Top Total two connections to qualify.</small></p>
- """.format(Count=HighDict["count"]["Count"],
- CHost=HighDict["count"]["Host"],
- CCountry=HighDict["count"]["Country"],
- LTime=f_mins(HighDict["max"]["Time"]),
- LHost=HighDict["max"]["Host"],
- LCountry=HighDict["max"]["Country"],
- ATime=f_mins(HighDict["ave"]["Time"]),
- AHost=HighDict["ave"]["Host"],
- ACountry=HighDict["ave"]["Country"],
- TTime=f_mins(HighDict["sum"]["Time"]),
- THost=HighDict["sum"]["Host"],
- TCountry=HighDict["sum"]["Country"])
- ### End s_top_table ### }}}
- ### s_main_table_pre ### {{{
- s_main_table_pre = """
- <h3>Player List</h3>
- <p>In the past seven days there have been {TPlayers} players, and {TAttempts} connections.<BR>
- The average connection time is {TAve} minutes,<BR>
- with a total accumulated time of {TTime} minutes.<BR>
- <small>All <b>Conn</b>ection lengths are in minutes.<BR>
- Number columns may be sorted by clicking on their header.</small></p>
- <table id="mainTable">
- <thead>
- <tr><th>Host</th><th>Country</th><th>Region</th><th>Org</th><th>Attempts</th><th>Shortest Conn</th><th>Longest Conn</th><th>Average Conn</th><th>Total Conns</th></tr>
- </thead>
- <tbody>""".format(TAttempts=ServerDict["count"],
- TPlayers=ServerDict["uniq"],
- TAve=f_mins(ServerDict["sum"] / ServerDict["count"]),
- TTime=f_mins(ServerDict["sum"]))
- ### End s_main_table_pre ### }}}
- ### s_main_table ### {{{
- for Host in HostDict:
- ## Make easy to read in code without added newlines
- s_main_table_temp = (
- "<tr>"
- "<td>{Host}</td>"
- "<td>{Country}</td>"
- "<td>{Region}</td>"
- "<td>{Org}</td>"
- "<td align=right>{Count}</td>"
- "<td align=right>{Min}</td>"
- "<td align=right>{Max}</td>"
- "<td align=right>{Ave:.3f}</td>"
- "<td align=right>{Sum:.3f}</td>"
- "</tr>"
- ).format(Host=Host,
- Country=HostDict[Host]["country"],
- Region=HostDict[Host]["region"],
- Org=HostDict[Host]["org"],
- Count=HostDict[Host]["count"],
- Min=f_mins(HostDict[Host]["min"]),
- Max=f_mins(HostDict[Host]["max"]),
- Ave=f_mins(HostDict[Host]["ave"]),
- Sum=f_mins(HostDict[Host]["sum"]))
- s_main_table = """{0}
- {1}""".format(s_main_table, s_main_table_temp)
- ### End s_main_table ### }}}
- ### s_main_table_post ### {{{
- ## This was broken off of s_html_bottom so that the .format didn't error
- ## No inspect.cleandoc as it would collapse to beginning of line
- s_main_table_post = """
- </tbody>
- </table>
- <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>
- which parses an <a href="https://github.com/skeeto/endlessh">endlessh</a> log file.</p>
- <p><b>Last updated {Date}</b></p>
- """.format(Date=datetime.datetime.utcnow().strftime("%a %d %b %Y %H:%M:%S UTC"))
- ### End s_main_table_post ### }}}
- ### s_html_bottom ### {{{
- s_html_bottom = inspect.cleandoc("""
- <!-- from https://developer.mozilla.org/en-US/docs/Web/HTML/Element/table -->
- <script>
- for (let table of document.querySelectorAll('#mainTable')) {
- for (let th of table.tHead.rows[0].cells) {
- th.onclick = function(){
- const tBody = table.tBodies[0];
- const rows = tBody.rows;
- for (let tr of rows) {
- Array.prototype.slice.call(rows)
- .sort(function(tr1, tr2){
- const cellIndex = th.cellIndex;
- return tr1.cells[cellIndex].textContent.localeCompare(tr2.cells[cellIndex].textContent);
- })
- .forEach(function(tr){
- this.appendChild(this.removeChild(tr));
- }, tBody);
- }
- }
- }
- }
- </script>
- </body>
- </center>
- </html>
- """)
- ### End s_html_bottom ### }}}
- return(s_html_top,s_top_table,s_main_table_pre,s_main_table,s_main_table_post,s_html_bottom)
- ### End f_gen_html ### }}}
- ### f_write_out_file ### {{{
- def f_write_out_file(s_output_html):
- Out_Path = '/var/www/pit/index.html' ## Set path to output
- OutFile = open(Out_Path, "w") ## Open file for writing
- for String in s_output_html: ## Iterate over strings
- OutFile.write(String) ## Write each string in order
- OutFile.close() ## Close file
- ### End f_write_out_file ### }}}
- ### f_hash ### {{{
- ## Make the Hashing a simple function call
- def f_hash(Host):
- ## Has, and salt with repeatability
- return(hashlib.sha512(os.uname()[1].encode('utf-8') + Host.encode('utf-8')).hexdigest()[0:16])
- ### End f_hash ### }}}
- ### f_mins ### {{{
- ## Convert and round seconds to minutes
- def f_mins(Time):
- return(round((Time / 60), 3))
- ### End f_mins ### }}}
- ## DEBUG
- ### f_debug_print ### {{{
- def f_debug_print(Mode,HostDict,HighDict,ServerDict):
- if Mode == 0 or Mode == 1:
- for Host in HostDict:
- ## Print host matches
- print(("Host: {Host}, "
- "Country: {Country}, "
- "Region: {Region}, "
- "Org: {Org}, "
- "Count: {Count}, "
- "Min: {Min}, "
- "Max: {Max}, "
- "Ave: {Ave:.3f}, "
- "Sum: {Sum:.3f}"
- ).format(Host=Host,
- Country=HostDict[Host]["country"],
- Region=HostDict[Host]["region"],
- Org=HostDict[Host]["org"],
- Count=HostDict[Host]["count"],
- Min=f_mins(HostDict[Host]["min"]),
- Max=f_mins(HostDict[Host]["max"]),
- Ave=f_mins(HostDict[Host]["ave"]),
- Sum=f_mins(HostDict[Host]["sum"])))
- if Mode == 0 or Mode == 2:
- ## Print High Scores
- print(("Most Attempts: {Count}, "
- "Host: {Host}, "
- "Country: {Country}"
- ).format(Count=HighDict["count"]["Count"],
- Host=HighDict["count"]["Host"],
- Country=HighDict["count"]["Country"]))
- print(("Longest Conn: {Time}, "
- "Host: {Host}, "
- "Country: {Country}"
- ).format(Time=f_mins(HighDict["max"]["Time"]),
- Host=HighDict["max"]["Host"],
- Country=HighDict["max"]["Country"]))
- print(("Highest Average: {Time}, "
- "Host: {Host}, "
- "Country: {Country}"
- ).format(Time=f_mins(HighDict["ave"]["Time"]),
- Host=HighDict["ave"]["Host"],
- Country=HighDict["ave"]["Country"]))
- print(("Highest Total: {Time}, "
- "Host: {Host}, "
- "Country: {Country}"
- ).format(Time=f_mins(HighDict["sum"]["Time"]),
- Host=HighDict["sum"]["Host"],
- Country=HighDict["sum"]["Country"]))
- if Mode == 0 or Mode == 3:
- print(("Total Attempts: {TAttempts}, "
- "Total Players: {TPlayers}, "
- "Total Ave Conn: {TAve}, "
- "Total Conn Time: {TTime}"
- ).format(TAttempts=ServerDict["count"],
- TPlayers=ServerDict["uniq"],
- TAve=f_mins(ServerDict["sum"] / ServerDict["count"]),
- TTime=f_mins(ServerDict["sum"])))
- ### f_debug_print ### }}}
- ### Arguments ### {{{
- ## Parse the command line arguments
- def parse_argv(argv):
- arg1=argv[1]
- ## Single augment
- if len(argv) == 2:
- ## Option help
- if arg1 == "-h" or arg1 == "--help" or arg1 == "help":
- msg_help()
- return
- ## Option version
- if arg1 == "-V" or arg1 == "--version" or arg1 == "version":
- msg_version()
- return
- ## If they get here, the didn't match; print help.
- print("["+arg1+"] invalid use, or invalid option!")
- msg_help()
- return
- ## Two Augments
- elif len(argv) > 2:
- arg2=argv[2]
- ## Option mute
- if arg1 == "-e" or arg1 == "--exclude" or arg1 == "exclude":
- if re.match("^(\d+)$", arg2):
- mute_toggle(arg2)
- else:
- print("["+arg2+"] is not a valid sink input!")
- msg_help()
- return
- return
- ### End Arguments ### }}}
- ## Print help message
- def msg_help():
- help_msg = inspect.cleandoc("""
- Usage: endlessh_scoreboard.py [OPTION...]
- When run without OPTION, will DO A THING
- When run with BLAH, WILL DO OTHER THING
- -e, --exclude Exclude IP [ip1] [ip2] [ip3]...
- -V, --version Display version information
- -h, --help Display this help message
- """)
- print(help_msg)
- quit()
- return
- ## Print version number message
- def msg_version():
- version_msg="endlessh_scoreboard version: 0.3.2"
- print(version_msg)
- quit()
- return
- ## Main
- def main(argv):
- ## Process input, if given
- if len(argv) > 1:
- parse_argv(argv)
- FilterArray = f_open_log_file()
- HostDict = f_dict_computation(FilterArray)
- HighDict,ServerDict = f_dict_total(HostDict)
- s_output_html=f_gen_html(HighDict,HostDict,ServerDict)
- f_write_out_file(s_output_html)
- # f_debug_print(3,HostDict,HighDict,ServerDict)
- if __name__ == '__main__':
- main(sys.argv)
|