#!/usr/bin/python3 ################################################################################# # This file is part of Mana Spheres. # Copyright (C) 2020 Jesusalva # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . ################################################################################# ## Global Modules import threading, time, json, syslog, base64 LOG_AUTH=(syslog.LOG_NOTICE | syslog.LOG_AUTH) # Tweak as needed/desired ## Semi-local modules from websock import WebSocketServer, WebSocket ## Local Modules from utils import stdout, now, clients, debug from consts import MAX_CLIENTS, PACKET_ACK import protocol, security, player, utils, traceback ############################################################### # Configuration f=open("pass.json", "r") p=json.load(f) f.close() PORT=p["PORT"] SECURE=p["SSL"] ############################################################### # Main Class class WebSocketConn(WebSocket): def handle(self): global clients """ Called when websocket frame is received. To access the frame data call self.data. i.e. The client sent us a message \o/ If the frame is Text then self.data is a unicode object. If the frame is Binary then self.data is a bytearray object. """ print("Message received from %s - %s" % (self.address[0], self.data)) print("Cache Recv: %s" % self.cacher) print("Cache Send: %s" % self.caches) # Check cache first try: if (base64.b64encode(bytearray(self.data, 'utf-8')) == self.cacher): print("Replying from cache... (%s)" % self.caches) self.send_message(self.caches) if (self.caches == "NACK\n"): security.score(self, 1) return else: print("Not cached, continue") except: traceback.print_exc() stdout("Cache lookup skipped (ERROR)") # Handle packet try: r=protocol.parse(self.data, self) stdout("Status: %s" % str(r[0]), 2) stdout("Reply: %s" % r[1], 2) if r[0] < PACKET_ACK: self.caches = "NACK\n" stdout("%s - %s" % (self.address[0], r[1])) syslog.syslog(LOG_AUTH, "%s - %s" % (self.address[0], r[1])) self.send_message("NACK\n") security.score(self, security.get_score(r[0])) else: self.caches = str(r[1]) self.send_message(str(r[1])) except: traceback.print_exc() self.send_message("ERROR\n") self.caches = "ERROR\n" stdout("Message sent", 2) self.cacher = base64.b64encode(bytearray(self.data, 'utf-8')) #stdout(self.address[0] + u' - %s' % (self.data)) #self.send_message('ACK') def connected(self): global clients, blacklist """ Called when a websocket client connects to the server. """ #print(self.address, 'connected') #for client in clients: # print(repr(client)) # client.send_message(self.address[0] + u' - connected') stdout(self.address[0] + u' - connected') # Keep only a "sane" amount of clients connected to the server. # This program must not, under any circumstances, interfer # on the operation of Moubootaur Legends, which has total priority # over the VM resources if (len(clients) > MAX_CLIENTS): self.close(self, status=1000, reason='Server is full') else: clients.append(self) # Blacklisted if (security.is_banned(self.address[0])): stdout("Found in K-Line, connection was dropped.") self.close(self, status=1000, reason="K-Lined") return # TODO: Drop OLD connections when same IP tries to connect # TODO: Either auto-ban or limit to <= 3 connections from same IP at once # TODO: Also, inheir the previous connection MS score and MS Auth # Extend self class self.MS_score = 0 self.MS_auth = False self.token = "0" self.userid = 0 self.caches = "INIT" self.cacher = "INIT" def handle_close(self): global clients """ Called when a websocket server gets a Close frame from a client. """ print(self.address, 'closed') stdout(self.address[0] + u' - disconnected') try: clients.remove(self) except ValueError: pass except: traceback.print_exc() if self.token != "0": try: player.clear(self.token) except: traceback.print_exc() stdout("Error at player.clear", 0) ########################## # Useful functions: # send_message() # send_fragment_start() / send_fragment() / send_fragment_end() # ########################## def MainWebsocket(): if SECURE: server = WebSocketServer('', PORT, WebSocketConn, certfile="certificate.pem", keyfile="key.pem") print("Begin Secure Websocket at %d" % PORT) else: server = WebSocketServer('', PORT, WebSocketConn) print("Begin UNENCRYPTED Websocket at %d" % PORT) t = threading.Thread(target=server.serve_forever) t.daemon = True # Main server should not be a daemon? t.start() return #while True: # time.sleep(86400) # Broadcast function def sendmsg(m, t="raw"): global clients # Format message according to type if t == "ping": msg="pong" else: msg=u"%s" % m # Send message for c in clients: c.send_message(msg) return # Disconnect function def disconnect(ip): global clients totaldc=0 for c in clients: if c.address[0] == ip: c.close(status=1000, reason="Remote host closed connection.") totaldc+=1 return totaldc ############################################################### # Begin stuff stdout("Starting at: T-%d" % (now()), 0) syslog.openlog() MainWebsocket() try: print("Serving at port %d" % PORT) while True: command=str(input("> ")) # No command inserted? Do nothing if command == "": continue # We have a command, prepare it for processing stdout("CONSOLE: %s" % command) cmd=command.split(' ')[0].lower() com=" ".join(command.split(' ')[1:]) # Parse the command # TODO: grant gems to an user if cmd in ["broadcast", "global", "msg", "say", "kami"]: sendmsg("NOTICE:" + com) elif cmd in ["ban", "nuke", "kb"]: security.ban_ip(com) totaldc=disconnect(com) stdout("BAN: Disconnected %d clients." % totaldc) del totaldc elif cmd in ["unban"]: security.unban_ip(com) elif cmd in ["permaban", "block", "kline"]: security.ban_ip(com) f=open("K-Line.txt", "a") f.write(com+"\n") f.close() stdout("%s has been K-Lined." % com) totaldc=disconnect(com) stdout("Disconnected %d clients." % totaldc) del totaldc elif cmd in ["exit", "quit", "close", "term", "end"]: stdout("Preparing to close server...") sendmsg("NOTICE:Server is going down for scheduled maintenance. Please close, wait five minutes, and then re-open the app.") break elif cmd in ["dbg", "debug"]: if debug == 2: debug = 0 utils.debug = 0 else: debug += 1 utils.debug += 1 stdout("Changed debug mode to %d" % debug, 0) elif cmd in ["raw", "eval"] and debug: try: print(eval(com)) except: traceback.print_exc() print("[RAW] Error.") elif cmd in ["run", "exec"] and debug: try: exec(com) except: traceback.print_exc() print("[RUN] Error.") elif cmd in ["status", "st"]: stdout("Total clients connected: %d" % len(clients)) stdout("Total blacklist size: %d" % len(security.blacklist)) elif cmd in ["list", "all"]: stdout("Total clients connected: %d" % len(clients)) for cli in clients: print("[%d] %s - %s" % (cli.userid, cli.token, cli.address[0])) elif cmd in ["kick", "dc"]: totaldc=disconnect(com) stdout("Disconnected %d clients." % totaldc) del totaldc elif cmd in ["ddos", "dcall"]: totaldc=0 for c in clients: if c.token == "0" or c.userid < 1: c.close(status=1000, reason="Remote host closed connection.") totaldc+=1 stdout("Disconnected %d clients." % totaldc) del totaldc elif cmd in ["ddosban", "dcbanall"]: totaldc=0 for c in clients: if c.token == "0" or c.userid < 1: security.ban_ip(c.address[0], now()+1800) c.close(status=1000, reason="Remote host closed connection.") totaldc+=1 stdout("Disconnected and banned %d clients." % totaldc) del totaldc else: stdout("ERROR: Unknown command.") except: stdout("Abrupt error: Terminating!", 0) traceback.print_exc() # Wait a bit before disconnecting all clients # To make sure any pending sendmsg() will arrive time.sleep(0.20) for c in clients: c.close(reason="Server is shutting down") # Make cleanup, just in case c.close() missed time.sleep(1.0) player.sql_routine() print("Server finished.")