diff --git a/mrc_client_encryption.pyc b/mrc_client_encryption.pyc new file mode 100644 index 0000000..9df0e85 Binary files /dev/null and b/mrc_client_encryption.pyc differ diff --git a/mrc_client_v3.py b/mrc_client_v3.py new file mode 100755 index 0000000..38aa444 --- /dev/null +++ b/mrc_client_v3.py @@ -0,0 +1,497 @@ +#!/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, ssl +import time, socket, errno, string, platform, traceback + +import mrc_client_encryption + +# 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.3.0a" +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 [mL] (with encryption!)" % 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~ +# Sat Aug 27 23:37:50 2022 IN: SERVER~~~MeaTLoTioN~~testing~ROOMTOPIC:testing: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() + + if 'ROOMTOPIC' 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]) + newTopic = message.split(':')[2] + clogfile = "%s%smrcchat.log" % (mrcdir, os.sep) + clog = open(clogfile, "a") + clog.write("%s %s %s\n" % (ltime, room, message)) + clog.close() + + + ltime=time.asctime(time.localtime(time.time())) + packet = data.split("~") + if packet[5]: + room = packet[5] + else: + room = packet[2] + message = stripmci(packet[6]) + #if room == 'testing': + # 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: + # Encrypt message + new_fl = mrc_client_encryption.encrypt_message(fl) + send_server(new_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) + + if fromuser != 'SERVER': + # Decrypt messages + server_data = mrc_client_encryption.decrypt_message(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 +