Compare commits

..

14 commits

2 changed files with 317 additions and 60 deletions

View file

@ -1,5 +1,5 @@
#!/usr/bin/python3 #!/usr/bin/python3
from datetime import datetime as dt from datetime import datetime as dt, UTC
from sys import stdout, stderr from sys import stdout, stderr
from typing import Union from typing import Union
@ -15,7 +15,8 @@ def log(
else: else:
stream = stdout stream = stdout
if time == "now": if time == "now":
dtime = dt.now() dtime = dt.now(UTC)
dtime.replace(tzinfo=UTC)
elif type(time) == str: elif type(time) == str:
raise ValueError('Only "now" is an accepted string argument for time') raise ValueError('Only "now" is an accepted string argument for time')
elif type(time) == dt: elif type(time) == dt:

372
server.py
View file

@ -1,5 +1,6 @@
import os, sys, asyncio, re, signal import os, sys, asyncio, re, signal
from firepup650 import console from platform import uname
from traceback import format_exc
from logs import log from logs import log
@ -13,11 +14,15 @@ G.servers = {}
G.clientsConnected = [] G.clientsConnected = []
port = 65048 port = 65048
G.msgs = [] G.msgs = []
G.remoteID = "firepi" G.remoteID = uname().node
G.event = asyncio.Event() G.event = asyncio.Event()
G.loop = asyncio.get_event_loop() G.loop = asyncio.get_event_loop()
G.interruptCount = 0 G.interruptCount = 0
G.killList = {}
G.outboundLinks = []
G.S2SLogs = []
saveLogs = True saveLogs = True
address = "0.0.0.0"
# Try to load a message log, if one exists # Try to load a message log, if one exists
try: try:
G.msgs = __import__("cache").msgs G.msgs = __import__("cache").msgs
@ -30,23 +35,53 @@ except Exception:
level="WARN", level="WARN",
) )
try: try:
filename = sys.argv.pop(0) # Ignore the filename
for arg in sys.argv: for arg in sys.argv:
if arg.startswith("--port"): if arg.startswith("--port") or arg.startswith("-p"):
port = int(arg.lstrip("-port= ")) port = int(arg.lstrip("-port="))
elif arg.startswith("-p"): elif arg in ["-c", "--no-cache"]:
port = int(arg.lstrip("-p= "))
elif arg in ["-n", "--no-cache"]:
log("Explicitly erasing cached messages") log("Explicitly erasing cached messages")
G.msgs = [] G.msgs = []
elif arg in ["-?", "-h", "--help"]: elif arg in ["-?", "-h", "--help"]:
print("TODO: Help menu soon") print(
f"""python3 {filename} <args>
All arguments are optional!
All areguments are *expected* to only be specified once, if it appears mutliple times, the last one takes priority.
The exception to the above rule is `--link`, since you could want to link to multiple other servers.
Accepted arguments:
-?, -h, --help - Triggers this help dialog
-l, --no-logs - Disables the saving of logs when the server shuts down
-c, --no-cache - Disables the loading of cached messages when the server boots
-p<number>, --port=<number> - Sets the port for the server to use, defaults to `65048`
--link=<host>:<port> - Establishes an S2S link with remote server <host> on port <port> when the server starts up
--hostname=<string> - Sets the hostname for this server to use when talking to other servers. Defaults to the current system's hostname (limit of 16 chars)
--address=<IP> - Sets the IP to listen on, defaults to all addresses (0.0.0.0)
Examples:
python3 {filename} --hostname=Fun-chat --link=chat.example.com:65048 --port=92628
python3 {filename} --no-logs --no-cache
python3 {filename} --address=127.0.0.1 -l -p=7288"""
)
exit(0) exit(0)
elif arg in ["-l", "--no-logs"]: elif arg in ["-l", "--no-logs"]:
log("Disabling saving of logs!") log("Explicitly disabling saving of logs!")
saveLogs = False saveLogs = False
elif arg.startswith("--link"):
G.outboundLinks.append((arg[7:].split(":")[0], int(arg.split(":")[1])))
elif arg.startswith("--hostname"):
G.remoteID = arg[11:]
elif arg.startswith("--address"):
address = arg[9:]
else:
log(f"Unrecognized argument {arg}!", "WARN")
except Exception: except Exception:
sys.tracebacklimit = 0 sys.tracebacklimit = 0
raise ValueError("Invalid arguments. Please refer to -? for usage.") from None raise ValueError("Invalid arguments. Please refer to --help for usage.") from None
if not saveLogs:
G.msgs.append(
b"[00-00-0000 00:00:00] Notice: Logging is disabled on this server instance!\n"
)
def raw(string: str) -> str: def raw(string: str) -> str:
@ -65,7 +100,7 @@ def fmt(msg: str, name: str = "", action: bool = False) -> str:
return f" {name}{' '*(20-len(name))}: {msg}" return f" {name}{' '*(20-len(name))}: {msg}"
async def handle_client(reader, writer): async def handleClient(reader, writer):
try: try:
global G global G
writer.write(b"Please identify yourself. Nick limit is 20 chars.\n") writer.write(b"Please identify yourself. Nick limit is 20 chars.\n")
@ -87,17 +122,18 @@ async def handle_client(reader, writer):
writer.close() writer.close()
await writer.wait_closed() await writer.wait_closed()
return return
if name in G.clientsConnected: if name.lower() in G.clientsConnected:
writer.write(f"Nick ({name}) in use\n".encode("utf8")) writer.write(f"Nick ({name}) in use\n".encode("utf8"))
await writer.drain() await writer.drain()
writer.close() writer.close()
await writer.wait_closed() await writer.wait_closed()
return return
if not name.startswith("S2S-"): if not name.startswith("S2S-"):
G.clientsConnected.extend([name]) G.clientsConnected.append(name.lower())
msgIndex = 0 msgIndex = 0
G.uniqueClients += 1 G.uniqueClients += 1
G.msgs.extend([log(f"{name} has connected to the server.")]) G.msgs.extend([log(f"{name} has connected to the server.")])
G.S2SLogs.append(("+", name, G.remoteID))
while 1: while 1:
try: try:
buffer = await asyncio.wait_for(reader.read(967), 0.1) buffer = await asyncio.wait_for(reader.read(967), 0.1)
@ -105,58 +141,103 @@ async def handle_client(reader, writer):
response = None response = None
if request.startswith("/mes "): if request.startswith("/mes "):
response = log(f"* {name}'s {request[5:]}") response = log(f"* {name}'s {request[5:]}")
G.S2SLogs.append(("A", (name + "'s", request[5:]), G.remoteID))
elif request.startswith("/me "): elif request.startswith("/me "):
response = log(f"* {name} {request[4:]}") response = log(f"* {name} {request[4:]}")
G.S2SLogs.append(("A", (name, request[4:]), G.remoteID))
elif request.startswith("/h"): elif request.startswith("/h"):
writer.write(b"TODO: Command listing\n") writer.write(
b"""Command List:
/me <action> - Sends a special message so it looks like you did <action>
/mes <action> - Same as /me, but adds a 's onto your nick
/afk [reason] - Optional reason, short hand for `/me is afk [to [reason]]`
/h, /help - Triggers this command listing
/back - Shorthand for `/me is back`
/quit - Disconnects you from the server\n"""
)
await writer.drain() await writer.drain()
elif request.startswith("/quit"): elif request.startswith("/quit"):
break break
elif request.startswith("/afk"): elif request.startswith("/afk"):
if len(request) > 5: if len(request) > 5:
response = log(f"* {name} is afk to {request[4:]}") response = log(f"* {name} is afk to {request[5:]}")
G.S2SLogs.append(
("A", (name, f"is afk to {request[5:]}"), G.remoteID)
)
else: else:
response = log(f"* {name} is afk") response = log(f"* {name} is afk")
G.S2SLogs.append(("A", (name, "is afk"), G.remoteID))
elif request.startswith("/back"): elif request.startswith("/back"):
response = log(f"* {name} is back") response = log(f"* {name} is back")
G.S2SLogs.append(("A", (name, "is back"), G.remoteID))
elif request: elif request:
response = log(f" {name}: {request}") response = log(f" {name}: {request}")
G.S2SLogs.append(("M", (name, request), G.remoteID))
if response: if response:
G.msgs.extend([response]) G.msgs.append(response)
except asyncio.TimeoutError: except asyncio.TimeoutError:
pass pass
if msgIndex < len(G.msgs): if msgIndex < len(G.msgs):
writer.writelines(G.msgs[msgIndex:]) writer.writelines(G.msgs[msgIndex:])
await writer.drain() await writer.drain()
msgIndex = len(G.msgs) msgIndex = len(G.msgs)
if name in G.killList and G.killList[name]: if name.lower() in G.killList and G.killList[name.lower()]:
writer.write(b"Your client has been killed by the server\n") writer.write(b"Your client has been killed by the server\n")
G.killList[name] = False G.killList[name.lower()] = False
break break
await writer.drain() await writer.drain()
writer.close() writer.close()
await writer.wait_closed() await writer.wait_closed()
G.uniqueClients -= 1 G.uniqueClients -= 1
G.msgs.append(log(f"{name} has disconnected from the server.")) G.msgs.append(log(f"{name} has disconnected from the server."))
G.clientsConnected.remove(name) G.clientsConnected.remove(name.lower())
G.S2SLogs.append(("-", name, G.remoteID))
else: # This is... probably a server? else: # This is... probably a server?
sName = name[4:] # Trim off the S2S label sName = name[4:] # Trim off the S2S label
log(f"Server link! Link from {sName}") if (
G.servers.get(sName, False) != False
): # Have to explicitly check, empty list is False, but does not == False
writer.close()
await writer.wait_closed()
return # Server is already "linked", drop the connection
if G.remoteID == sName: # Hey! you can't *also* be ***me***!
writer.close()
await writer.wait_closed()
return # drop "us"
G.msgs.append(log(f"{sName} has linked to the network"))
G.serverLinks += 1 G.serverLinks += 1
G.servers[sName] = [] G.servers[sName] = []
msgIndex = 0 msgIndex = 0
writer.write(b"I am awaiting your client listing.\n")
while 1: while 1:
client = raw((await reader.read(967)).decode("utf8")) client = raw((await reader.read(1024)).decode("utf8"))
if client == f"END OF CLIENT LISTING FROM {sName}": if client == f"END OF CLIENT LISTING FROM {sName}":
break break
if name in G.servers[sName] or name in G.clientsConnected: if (
client.lower() in G.servers[sName]
or client.lower() in G.clientsConnected
):
writer.write(b"K Client rejected: Already exists\n") writer.write(b"K Client rejected: Already exists\n")
await writer.drain() await writer.drain()
writer.write(b"I Added client.\n") continue
writer.write(b"I added that client.\n")
await writer.drain() await writer.drain()
G.msgs.append(log(f"{client} has connected from {sName}")) G.msgs.append(log(f"{client} has connected from {sName}"))
G.servers[sName].append(client) G.servers[sName].append(client.lower())
G.S2SLogs.append(("+", client, sName))
writer.write(f"{G.remoteID}\n".encode("utf8"))
await writer.drain()
await reader.read(1024)
for client in G.clientsConnected:
writer.write(f"{client}\n".encode("utf8"))
await writer.drain()
resp = raw((await reader.read(1024)).decode("utf8"))
if resp.startswith("K"):
G.killList[client.lower()] = True
writer.write(f"END OF CLIENT LISTING FROM {G.remoteID}\n".encode("utf8"))
await writer.drain()
G.clientsConnected.extend(G.servers[sName]) G.clientsConnected.extend(G.servers[sName])
msgInd = len(G.S2SLogs)
while 1: while 1:
try: try:
rawMsg = await asyncio.wait_for(reader.read(967), 0.1) rawMsg = await asyncio.wait_for(reader.read(967), 0.1)
@ -165,98 +246,273 @@ async def handle_client(reader, writer):
case "S": # Server notice case "S": # Server notice
G.msgs.extend([log(buffer[2:])]) G.msgs.extend([log(buffer[2:])])
writer.write(b"I Mmm... Blueberries\n") writer.write(b"I Mmm... Blueberries\n")
await writer.drain()
case "I": case "I":
pass pass
case "+": case "+":
cName = buffer[2:] cName = buffer[2:]
if cName not in G.clientsConnected: if cName.lower() not in G.clientsConnected:
G.msgs.append( G.msgs.append(
log(f"{cName} has connected from {sName}") log(f"{cName} has connected from {sName}")
) )
G.servers[sName].append(cName) G.servers[sName].append(cName.lower())
G.clientsConnected.append(cName) G.clientsConnected.append(cName.lower())
G.S2SLogs.append(("+", cName, sName))
writer.write(b"I Mmm... Pineapples\n") writer.write(b"I Mmm... Pineapples\n")
await writer.drain()
else: else:
writer.write(b"K Nick Collision") writer.write(f"K {cName}\n".encode("utf8"))
await writer.drain()
case "-": case "-":
cName = buffer[2:] cName = buffer[2:]
G.msgs.append(log(f"{cName} has disconnected from {sName}")) G.msgs.append(log(f"{cName} has disconnected from {sName}"))
G.servers[sName].remove(cName) G.servers[sName].remove(cName.lower())
G.clientsConnected.remove(cName) G.clientsConnected.remove(cName.lower())
G.S2SLogs.append(("-", cName, sName))
writer.write(b"I Mmm... Bananas\n") writer.write(b"I Mmm... Bananas\n")
await writer.drain()
case "M": case "M":
cName = buffer[2:].split("|", 1)[0] cName = buffer[2:].split("|", 1)[0]
message = buffer[2:].split("|", 1)[1] message = buffer[2:].split("|", 1)[1]
G.msgs.append(log(f" {cName}: {message}")) G.msgs.append(log(f" {cName}: {message}"))
G.S2SLogs.append(("M", (cName, message), sName))
writer.write( writer.write(
b"I Get these damn heretic ghost clients out of my store so i can buy my cult candles in peace." b"I Get these damn heretic ghost clients out of my store so i can buy my cult candles in peace.\n"
) )
await writer.drain()
case "A": case "A":
cName = buffer[2:].split("|", 1)[0] cName = buffer[2:].split("|", 1)[0]
message = buffer[2:].split("|", 1)[1] message = buffer[2:].split("|", 1)[1]
G.msgs.append(log(f"* {cName} {message}")) G.msgs.append(log(f"* {cName} {message}"))
G.S2SLogs.append(("A", (cName, message), sName))
writer.write(b"I Mmm... Strawberries\n") writer.write(b"I Mmm... Strawberries\n")
await writer.drain()
case "Q": case "Q":
break break
case "K": case "K":
cName = buffer[2:] cName = buffer[2:]
G.killList[cName] = True G.killList[cName] = True
writer.write(b"I Mmm... Blood\n") writer.write(b"I Mmm... Blood\n")
await writer.drain()
case _: case _:
writer.write( writer.write(
b"S Your server is doing drugs over here, sending me bullshit messages man - A fellow server\n" f"S Your server is doing drugs over here, sending me bullshit messages man - {G.remoteID}, A fellow server\n".encode(
"utf8"
)
) )
await writer.drain() await writer.drain()
except TimeoutError: except TimeoutError:
pass pass
for cName in G.serverLinks[sName]: while msgInd < len(G.S2SLogs):
type, data, server = G.S2SLogs[msgInd]
# match-case on type
if server != sName:
match type:
case "A":
nick, msg = data
writer.write(f"A {nick}|{msg}\n".encode("utf8"))
case "M":
nick, msg = data
writer.write(f"M {nick}|{msg}\n".encode("utf8"))
case "+":
writer.write(f"+ {data}\n".encode("utf8"))
case "-":
writer.write(f"- {data}\n".encode("utf8"))
case _:
pass
await writer.drain()
msgInd += 1
await writer.drain()
writer.close()
await writer.wait_closed()
for cName in G.servers[sName]:
G.msgs.append(log(f"{cName}'s server is going down")) G.msgs.append(log(f"{cName}'s server is going down"))
G.clientsConnected.remove(cName) G.S2SLogs.append(("-", cName, sName))
G.clientsConnected.remove(cName.lower())
G.serverLinks -= 1 G.serverLinks -= 1
G.servers.remove(sName) del G.servers[sName]
G.msgs.append(log(f"{sName} has de-linked from the network")) G.msgs.append(log(f"{sName} has de-linked from the network"))
except [ConnectionResetError, BrokenPipeError]: except (
ConnectionResetError,
BrokenPipeError,
IndexError,
): # Don't ask. IndexError needs to be caught here too.
if not name.startswith("S2S-"): if not name.startswith("S2S-"):
G.uniqueClients -= 1 G.uniqueClients -= 1
G.msgs.append(log(f"{name} has disconnected from the server.")) G.msgs.append(log(f"{name} has disconnected from the server."))
G.clientsConnected.remove(name) G.S2SLogs.append(("-", name, G.remoteID))
G.clientsConnected.remove(name.lower())
else: else:
for cName in G.serverLinks[name[4:]]: for cName in G.servers[name[4:]]:
G.msgs.append(log(f"{cName}'s server is going down")) G.msgs.append(log(f"{cName}'s server is going down"))
try: try:
G.clientsConnected.remove(cName) G.clientsConnected.remove(cName.lower())
G.S2SLogs.append(("-", cName, name[4:]))
except Exception: # Crash during connection sequence? except Exception: # Crash during connection sequence?
pass pass
G.serverLinks -= 1 G.serverLinks -= 1
G.servers.remove(name[4:]) del G.servers[name[4:]]
G.msgs.append(log(f"{name[4:]} has de-linked from the network")) G.msgs.append(log(f"{name[4:]} has de-linked from the network"))
async def run_server(port): async def connectServer(hostname: str, port: int):
global G global G
server = await asyncio.start_server(handle_client, "0.0.0.0", port) reader, writer = await asyncio.open_connection(hostname, port)
await reader.read(1024)
writer.write(f"S2S-{G.remoteID}\n".encode("utf8"))
await writer.drain()
await reader.read(1024)
for client in G.clientsConnected:
writer.write(f"{client}\n".encode("utf8"))
await writer.drain()
resp = raw((await reader.read(1024)).decode("utf8"))
if resp.startswith("K"):
G.killList[client.lower()] = True
writer.write(f"END OF CLIENT LISTING FROM {G.remoteID}\n".encode("utf8"))
await writer.drain()
rID = raw((await reader.read(16)).decode("utf8"))
try:
await asyncio.wait_for(reader.read(1024), 0.1)
except TimeoutError:
pass
if G.servers.get(rID, False) != False:
writer.close()
await writer.wait_closed()
return
if G.remoteID == rID:
writer.close()
await writer.wait_closed()
return
G.msgs.append(log(f"{rID} has linked to the network"))
G.serverLinks += 1
G.servers[rID] = []
writer.write(b"I recieved your remote ID, now awaiting client listing\n")
await writer.drain()
# recieve client list from the other server
while 1:
client = raw((await reader.read(1024)).decode("utf8"))
if client == f"END OF CLIENT LISTING FROM {rID}":
break
if client.lower() in G.servers[rID] or client.lower() in G.clientsConnected:
writer.write(b"K Client rejected: Already exists\n")
await writer.drain()
continue
writer.write(b"I added that client.\n")
await writer.drain()
G.msgs.append(log(f"{client} has connected from {rID}"))
G.servers[rID].append(client.lower())
G.clientsConnected.extend(G.servers[rID])
msgInd = len(G.S2SLogs)
try:
while 1:
try:
rawMsg = await asyncio.wait_for(reader.read(967), 0.1)
buffer = raw(rawMsg.decode("utf8"))
match buffer[0]:
case "S":
G.msgs.extend([log(buffer[2:])])
writer.write(b"I Mmm... Blueberries\n")
case "I":
pass
case "+":
cName = buffer[2:]
if cName.lower() not in G.clientsConnected:
G.msgs.append(log(f"{cName} has connected from {rID}"))
G.S2SLogs.append(("+", cName, rID))
G.servers[rID].append(cName.lower())
G.clientsConnected.append(cName.lower())
writer.write(b"I Mmm... Pineapples\n")
else:
writer.write(f"K {cName}\n".encode("utf8"))
case "-":
cName = buffer[2:]
G.msgs.append(log(f"{cName} has disconnected from {rID}"))
G.servers[rID].remove(cName.lower())
G.clientsConnected.remove(cName.lower())
G.S2SLogs.append(("-", cName, rID))
case "M":
cName = buffer[2:].split("|", 1)[0]
message = buffer[2:].split("|", 1)[1]
G.msgs.append(log(f" {cName}: {message}"))
G.S2SLogs.append(("M", (cName, message), rID))
writer.write(
b"I Get these damn heretic ghost clients out of my store so i can buy my cult candles in peace.\n"
)
case "A":
cName = buffer[2:].split("|", 1)[0]
message = buffer[2:].split("|", 1)[1]
G.S2SLogs.append(("A", (cName, message), rID))
G.msgs.append(log(f"* {cName} {message}"))
writer.write(b"I Mmm... Strawberries\n")
case "Q":
break
case "K":
cName = buffer[2:]
G.killList[cName.lower()] = True
writer.write(b"I Mmm... Blood\n")
case _:
writer.write(
f"S Your server is doing drugs over here, sending me bullshit messages man - {G.remoteID}, A fellow server\n".encode(
"utf8"
)
)
await writer.drain()
except TimeoutError:
pass
while msgInd < len(G.S2SLogs):
type, data, server = G.S2SLogs[msgInd]
if server != rID:
match type:
case "A":
nick, msg = data
writer.write(f"A {nick}|{msg}\n".encode("utf8"))
case "M":
nick, msg = data
writer.write(f"M {nick}|{msg}\n".encode("utf8"))
case "+":
writer.write(f"+ {data}\n".encode("utf8"))
case "-":
writer.write(f"- {data}\n".encode("utf8"))
case _:
pass
await writer.drain()
msgInd += 1
await writer.drain()
writer.close()
await writer.wait_closed()
for cName in G.servers[rID]:
G.msgs.append(log(f"{cName}'s server is going down"))
G.clientsConnected.remove(cName.lower())
G.serverLinks -= 1
del G.servers[rID]
G.msgs.append(log(f"{rID} has de-linked from the network"))
except (ConnectionResetError, BrokenPipeError, IndexError):
for cName in G.servers[rID]:
G.msgs.append(log(f"{cName}'s server is going down"))
try:
G.clientsConnected.remove(cName.lower())
except Exception:
pass
G.serverLinks -= 1
del G.servers[rID]
G.msgs.append(log(f"{rID} has de-linked from the network"))
async def runServer(address: str, port: int):
global G
server = await asyncio.start_server(handleClient, address, port)
log(f"Listening on port {port}...") log(f"Listening on port {port}...")
G.msgs.append(log("Server startup")) G.msgs.append(log("Server startup"))
links = []
for hostname, portNum in G.outboundLinks:
links.append(connectServer(hostname, portNum))
crash = False crash = False
try: try:
log("Waiting on the Interrupt Event to be set...") links.append(G.event.wait())
await G.event.wait() await asyncio.gather(*links)
log("Interrupt Event has been set, shutting down normally")
except Exception: except Exception:
crash = True crash = True
G.msgs.extend([log("Server crash", level="FATAL")[1:]]) G.msgs.append(log("Server crash", "FATAL")[1:])
log("Shutting down from Exception") log("Shutting down from Exception", "FATAL")
# TODO: Add format_exc here Err = format_exc()
for line in Err.split("\n"):
log(line, "ERROR")
finally: finally:
if not crash: if not crash:
G.msgs.extend([log("Server shutdown")]) G.msgs.append(log("Server shutdown"))
log("Kicking all clients as we go down") log("Kicking all clients as we go down")
server.close() server.close()
# server.abort_clients() # server.abort_clients()
@ -281,4 +537,4 @@ def interruptCatch(s, f):
signal.signal(signal.SIGINT, interruptCatch) signal.signal(signal.SIGINT, interruptCatch)
asyncio.run(run_server(port)) asyncio.run(runServer(address, port))