#!/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 <http://www.gnu.org/licenses/>.
#################################################################################
## 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.")