add mrc_client.py

This commit is contained in:
MeaTLoTioN 2022-06-07 23:40:02 +01:00
parent 596f90e763
commit 3265b358aa

459
mrc_client.py Executable file
View File

@ -0,0 +1,459 @@
#!/usr/bin/python
# ::::: __________________________________________________________________ :::::
# : ____\ ._ ____ _____ __. ____ ___ _______ .__ ______ .__ _____ .__ _. /____ :
# __\ .___! _\__/__ / _|__ / _/_____ __| \ gRK __|_ \ __ |_ \ !___. /__
# \ ! ___/ |/ /___/ | \__\ ._/ __\/ \ \___/ |/ \/ \_./ \___ ! /
# /__ (___ /\____\____|\ ____| / /___|\ ______. ____\|\ ___) __\
# /____ \_/ ___________ \_/ __ |__/ _______ \_/ ____ |___/ _____ \_/ ____\
# : /________________________________________________________________\ :
# ::::: + p H E N O M p R O D U C T I O N S + :::::
# ==============================================================================
#
# -----------------------------------------
# - modName: mrc_client multiplexer -
# - majorVersion: 1.2 -
# - minorVersion: 9 -
# - author: Stackfault -
# - publisher: Phenom Productions -
# - website: https://www.phenomprod.com -
# - email: stackfault@bottomlessabyss.net -
# - bbs: bbs.bottomlessabyss.net:2023 -
# -----------------------------------------
#
# Based on previous work from Gryphon of Cyberia BBS
#
# The code have been completely reviewed and improved everywhere I could see
# room for improvement without breaking compatibility.
#
# Major changes:
#
# v1.2.5
#
# - Error trapping on all critical locations
# - Socket routine rewrite and now non-blocking
# - Internal auto-restart, no need for an external restart script
# - New commands added and supported by the new server code
# - Message serialization allowing very fast message rate and proper display order
# - Bonus server stats data allowing an in-bbs applet to show MRC status (See samples)
# - Graceful client shutdown notification to the server
# - New BBS information subsystem allowing BBS to provide connection info details
# - New startup check to allow for smoother installation and configuration
#
# v1.2.7
#
# - Update available/Client too old notifications and validation
# - Client latency reporting
# - Added support for upcoming server activity level stats
# - Increase both frequency and tolerance of stats reporting
# - Other smaller fixes
#
# v1.2.9
# - Improved handling of incomplete and invalid packets
# - Implemented client stats transfer
# - Increased tcp buffer size
# - Improved the stats reporting
# - Rebased the message serialization
#
# v1.2.9a
# - Add room to chat log [mL]
#
# Make sure to use the new mrc_config.py so you can take advantage of some new
# features.
#
import os, os.path, sys, fnmatch, glob, re
import time, socket, errno, string, platform, traceback
# Import site config
from mrc_config import *
msleep = lambda x: time.sleep(x/1000.0)
bbsdir = os.getcwd()
# Change this info
tempdir = "%s%stemp" % (bbsdir, os.sep)
datadir = "%s%sdata" % (bbsdir, os.sep)
chatdats = "%s%schat*.dat" % (datadir, os.sep)
# Align this path with the MRC MPL (Default: {mrcdatadir}/mrc)
mrcdir = "%s%smrc" % (datadir, os.sep)
# Platform information
version = "1.2.9"
platform_name = "MYSTIC"
system_name = platform.system()
machine_arch = platform.machine()
debugflag = True
version_string = "%s/%s.%s/%s" % (platform_name, system_name, machine_arch, version)
client_version = "Multi Relay Chat Client v%s [sf]" % version
# Check for command-line args
if(len(sys.argv) < 3) :
print "Usage : mrc_client.py hostname port"
sys.exit(1)
# Global vars
host = sys.argv[1]
port = int(sys.argv[2])
intv = [1, 2, 5, 10, 30, 60, 120, 180, 240, 300] # Auto-restart intervals
timebase = int(time.time())
registry = {}
mrcstats = ''
# Strip MCI color codes
def stripmci(text):
return re.sub('\|[0-9]{2}', '', text)
# Sat Feb 6 16:55:18 2021 IN: SERVER~~~MeaTLoTioN~~a789653hgjk3876yhuk~ROOMTOPIC:a789653hgjk3876yhuk:test~
# User chatlog for DLCHATLOG
def chatlog(data):
if "CLIENT~" not in data and "SERVER~" not in data:
ltime=time.asctime(time.localtime(time.time()))
packet = data.split("~")
if packet[5]:
room = packet[5]
else:
room = packet[2]
message = stripmci(packet[6])
clogfile = "%s%smrcchat.log" % (mrcdir, os.sep)
clog = open(clogfile, "a")
clog.write("%s %s %s\n" % (ltime, room, message))
clog.close()
# Console logger
def logger(loginfo):
ltime = time.asctime(time.localtime(time.time()))
print "%s %s" % (ltime, loginfo.strip())
sys.stdout.flush()
# Socket sender to server
def send_server(data):
global registry
if data:
try:
regstp = int(time.time()*1000)
regstr = data.strip()
registry[regstr] = regstp
mrcserver.send(data)
except:
logger("Connection error")
try:
mrcserver.shutdown(2)
except:
pass
finally:
mrcserver.close()
# Temp files cleaning routine
def clean_files():
mrcfiles = os.listdir( mrcdir )
for file in mrcfiles:
if fnmatch.fnmatch(file,'*.mrc'):
mrcfile = "%s%s%s" % (mrcdir,os.sep,file)
os.remove(mrcfile)
# Read queued file from MRC, ignoring stale files older than 10s
def send_mrc():
mrcfiles = os.listdir( mrcdir )
for file in mrcfiles:
if fnmatch.fnmatch(file,'*.mrc'):
mrcfile = "%s%s%s" % (mrcdir,os.sep,file)
ft = os.path.getmtime(mrcfile)
# Do not forward packets older than 10s
if time.time() - ft < 10:
try:
# Avoid reading a file still open by the MPL by
# opening it read-write to check for locking
f = open(mrcfile,"r+")
fl = f.readline()
f.close()
if fl.count("~") > 5:
mline = fl.split("~")
fromuser = mline[0]
frombbs = mline[1]
touser = mline[3]
message = mline[6]
if message == "VERSION":
deliver_mrc("CLIENT~~~%s~%s~~|07- %s~" % (fromuser, frombbs, client_version))
send_server(fl)
elif touser == "CLIENT" and message == "LATENCY":
deliver_mrc("SERVER~~~CLIENT~%s~~LATENCY:%s~" % (frombbs, latency))
elif touser == "CLIENT" and message == "STATS":
deliver_mrc("SERVER~~~CLIENT~%s~~STATS:%s~" % (frombbs, mrcstats))
else:
send_server(fl)
if debugflag:
logger("OUT: %s" % fl)
os.remove(mrcfile)
else:
if debugflag:
logger("File %s contains invalid packet" % mrcfile)
except IOError:
if debugflag:
logger("MRC file still busy")
pass
except:
if debugflag:
logger("Error:" + traceback.print_exc())
else:
os.remove(mrcfile)
# Write time serialized file for display in MRC
def deliver_mrc( server_data ):
global registry
global latency
global mrcstats
global timebase
curtime = int(time.time()*1000)
if server_data.strip() in registry.keys():
pkttime = int(registry[server_data.strip()])
roundtrip = curtime - pkttime
if roundtrip > 1:
latency = roundtrip
if debugflag:
logger("LATENCY: Current: %s - Packet: %s = RoundTrip: %s" % (curtime, pkttime, roundtrip))
registry.clear()
# Make up a serialized filename based on time
fileserial = int((time.time()-timebase) * 1000)
# Wrap the file serial if longer than 8 chars
if fileserial > 99999999:
timebase = int(time.time())
fileserial = int((time.time()-timebase) * 1000)
# Zeropad the filename
filename = "%08d.mrc" % fileserial
try:
packet = server_data.split("~")
fromuser = packet[0]
fromsite = packet[1]
fromroom = packet[2]
touser = packet[3]
tosite = packet[4]
toroom = packet[5]
message = packet[6]
except:
logger("Bad packet: %s" % server_data)
return
if debugflag: logger("IN: %s" % server_data)
# Manage server PINGs
if fromuser == "SERVER" and message.lower() == "ping":
send_im_alive()
# Manage update available notifications
elif fromuser == "SERVER" and message.startswith("NEWUPDATE:"):
logger("Upgrade is available, consider upgrading at your earliest convenience")
logger("You are using version %s" % version)
logger("Latest version is %s" % message.split(":")[1])
# Manage old clients
elif fromuser == "SERVER" and message.startswith("OLDVERSION:"):
logger("Your client is too old and can no longer be used.")
logger("You are using version %s" % version)
logger("Latest version is %s" % message.split(":")[1])
raise KeyboardInterrupt
#elif fromuser == "SERVER" and message.startswith("ROOMTOPIC:"):
# ltime=time.asctime(time.localtime(time.time()))
# MM = message.split(":")
# room = MM[1]
# topic = MM[2]
# clogfile = "%s%smrcchat.log" % (mrcdir, os.sep)
# clog = open(clogfile, "a")
# clog.write("%s %s %s\n" % (ltime, "NEWTOPIC{" + room + "}", topic))
# clog.close()
else:
# Manage server stats
if fromuser == "SERVER" and message.startswith("STATS:"):
statsfile = "%s%smrcstats.dat" % (mrcdir, os.sep)
try:
f = open(statsfile, "w")
f.write(message.split(":")[1])
f.close()
mrcstats = message.split(":")[1]
except:
logger("Cannot write server stats to %s" % statsfile)
chatlog(server_data)
for f in glob.iglob(chatdats):
if not 'chatroom' in f:
chatfile="%s%schat" % (datadir,os.sep)
xy = f.replace(chatfile,tempdir)
xy = xy[:-4]
inusefile = "%s%stchat.inuse" % (xy,os.sep)
if os.path.isfile(inusefile):
mrcfile = "%s%s%s" % (xy,os.sep,filename)
openfile = open(mrcfile,"a")
openfile.write(server_data)
openfile.close()
msleep(5)
# Respond to server PING
def send_im_alive():
data = "CLIENT~%s~~SERVER~~~IMALIVE:%s~\n" % (bbsname,bbsname)
send_server(data)
# Send graceful shutdown request to server when exited
def send_shutdown():
data = "CLIENT~%s~~SERVER~~~SHUTDOWN~\n" % bbsname
send_server(data)
# Request server stats for applet
def request_stats():
data = "CLIENT~%s~~SERVER~~~STATS~\n" % bbsname
send_server(data)
# Send BBS additional info for INFO command
def send_bbsinfo():
prefix = "CLIENT~%s~~SERVER~ALL~~" % bbsname
packet = prefix + "INFOWEB:%s~\n" % info_web
packet += prefix + "INFOTEL:%s~\n" % info_telnet
packet += prefix + "INFOSSH:%s~\n" % info_ssh
packet += prefix + "INFOSYS:%s~\n" % info_sysop
packet += prefix + "INFODSC:%s~\n" % info_desc
send_server(packet)
# Handle different line separator scenarios
def check_separator(data):
if data.count("\r\n"): return("\r\n")
elif data.count("\n\r"): return("\n\r")
elif data.count("\r"): return("\r")
else: return("\n")
# Main process loop
def mainproc():
global delay
global mrcserver
global latency
mrcserver = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
mrcserver.settimeout(5)
restart = 0
readbuffer = ''
tdat = ''
latency = 0
# Non-blocking socket loop to improve speed
try:
mrcserver.connect((host, port))
mrcserver.setblocking(0)
mrcserver.send("%s~%s" % (bbsname, version_string))
logger("Connected to Multi Relay Chat host %s port %d" % (host, port))
delay = 0
except:
logger("Unable to connect to %s:%d" % (host,port))
return
send_bbsinfo()
send_im_alive()
loop = 1800
while True:
msleep(10)
send_mrc()
loop += 1
try:
readbuffer = mrcserver.recv(8192)
except socket.error, e:
err = e.args[0]
if err == errno.EAGAIN or err == errno.EWOULDBLOCK:
# Request stats every 20 seconds
if loop > 2000:
request_stats()
loop = 0
continue
else:
restart = 1
else:
if readbuffer:
sep = check_separator(readbuffer)
tdat = readbuffer.split(sep)
for data in tdat:
if data:
deliver_mrc(data)
else:
restart = 1
# Handle socket restarts with socket shutdowns
if restart:
logger("Lost connection to server\n")
try:
mrcserver.shutdown(2)
finally:
mrcserver.close()
return
# Some validation of config to ensure smoother operation
def check_startup():
failed = 0
if not os.path.exists(mrcdir):
os.makedirs(mrcdir)
if len(stripmci(bbsname)) < 5:
print "Config: 'bbsname' should be set to something sensible"
failed = 1
if len(stripmci(bbsname)) > 40:
print "Config: 'bbsname' cannot be longer than 40 characters after PIPE codes evaluation"
failed = 1
for param in ['info_web' 'info_telnet', 'info_ssh', 'info_sysop', 'info_desc']:
if len(stripmci(param)) > 64:
print "Config: '%s' cannot be longer than 64 characters after PIPE codes evaluation" % param
failed = 1
for param in ['info_web' 'info_telnet', 'info_ssh', 'info_sysop', 'info_desc']:
if len(param) > 128:
print "Config: '%s' cannot be longer than 128 characters including PIPE codes" % param
failed = 1
if failed:
print "This must be fixed in mrc_config.py"
sys.exit()
# Main loop
if __name__ == "__main__":
logger(client_version)
check_startup()
delay = 0
while True:
try:
mainproc()
# Incremental auto-restart built-in
logger("Reconnecting in %d seconds" % intv[delay])
time.sleep(intv[delay])
delay += 1
if delay > 9: delay = 0
except KeyboardInterrupt:
logger("Shutting down")
try:
send_shutdown()
try:
mrcserver.shutdown(2)
except:
pass
finally:
mrcserver.close()
sys.exit()
except:
continue