summaryrefslogblamecommitdiff
path: root/server.py
blob: 3f60c08eedd83506c7107fff027d6974a245985e (plain) (tree)
1
2
                  
                                                                                 














                                                                           
                                                                                 
 
                 
                                            

                                              

                     
                                              
 
                
                                             
                                          
                                                   







                                                               
               
 












                                                                      
                                                                             

















                                                                                
            
                                             

                                               
                                 
                                         


                                                                            
                                                              
                 
                                          
                                            


                                        
                                   
 
                                 

                                                                     
                                                         
                                 

                        
                                 


                                                                
                                         



                                                                   
 








                                                                          
                     
                                                 
                                                              
                                                           
                  
 

                                                                                 
                                                                         
 


                             
                           
                         

                               
 




                                                                          
                                     
                                                    







                                 




                                        
                                                  









                                                                   






                                                                               





















                                                         









                                                                         

                                                               
                                        
                
               
    
                                      
               
                                



                                                      



                                            
                                     
                                                                
                                    
                                          
                                


                                                             

                                  
                                                   




                                                


                                                        

                                                             
                                                                                                                                        
                 
                                     

                          
                               

                          

                                                         
                                              




                                     





                                              

                                                                
                                                                        



                                                                               



                                                        
















                                                                                 

                                             
       
                                           
                         
 


                                                

                                             




                                             
                         

 
#!/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.")