add mrc_client.py
This commit is contained in:
parent
596f90e763
commit
3265b358aa
459
mrc_client.py
Executable file
459
mrc_client.py
Executable 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
|
||||
|
Loading…
x
Reference in New Issue
Block a user