diff --git a/classes/config.py b/classes/config.py index 11f5b99..7cb3d88 100644 --- a/classes/config.py +++ b/classes/config.py @@ -4,6 +4,7 @@ from pathlib import Path from typing import Union from classes.vessel import Vessel +from classes.user import User class MonsterConfig: @@ -28,16 +29,20 @@ class MonsterConfig: # Read Vessels from the config file if section.startswith("Vessel"): self.vessels.append(Vessel.fromConfig(parser[section])) + if section.startswith("User"): + self.users.append(User.fromConfig(parser[section])) try: self.pyadonis = Path(parser["MONSTER"]["PyAdonis"]) except KeyError: print(f"PyAdonis is not defined in the MONSTER section of {path}, some features may be missing.") + def __init__(self, path: Union[str, Path]) -> None: """Initialize a new (empty) MonsterConfig object """ self.vessels = [] + self.users = [] self.pyadonis = None if path: diff --git a/classes/user.py b/classes/user.py new file mode 100644 index 0000000..aa7f5e2 --- /dev/null +++ b/classes/user.py @@ -0,0 +1,45 @@ +from classes.database import Database + +from configparser import SectionProxy +from typing import Optional, Union +from datetime import datetime + +from MySQLdb.cursors import DictCursor +from bcrypt import hashpw, gensalt + +from const import * + + +class User: + """Class describing a User + """ + @classmethod + def fromConfig(cls, config: SectionProxy): + """Create User object from a User section in the Config file + + Args: + config (configparser.SectionProxy): User section defining a User + + Raises: + KeyError: Raised if section does not contain Password parameter + + Returns: + classes.user.User: User object for the user specified in + the config section + """ + + return cls(config.name.split()[1], config["Password"]) + + + def __init__(self, username: str, password: str) -> None: + """Initialize new Vessel object + + Args: + name (str): Name of the Vessel + """ + self.username = username + self.password = password + + + def validatePasword(self, password) -> bool: + return password == self.password \ No newline at end of file diff --git a/const.py b/const.py index 4b62c7c..d43b097 100644 --- a/const.py +++ b/const.py @@ -13,6 +13,7 @@ QUERY_USER_CREATE = "INSERT INTO mdl_user(username, email, firstname, lastname, QUERY_ASSIGN_ROLE = "INSERT INTO mdl_role_assignments(roleid, contextid, userid, timemodified) VALUES (%s, %s, %s, %s)" QUERY_GET_ROLE = "SELECT * FROM mdl_role_assignments WHERE contextid = %s AND userid = %s" QUERY_GET_USERID = "SELECT * FROM mdl_user WHERE username = %s" +QUERY_USER_GET_PASSWORD = "SELECT password FROM mdl_user WHERE id = %s" QUERY_COURSE = "SELECT * FROM mdl_course" QUERY_ENROL = "SELECT * FROM mdl_enrol" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b3dd93a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +mysqlclient +paramiko +sshtunnel \ No newline at end of file diff --git a/server.py b/server.py new file mode 100644 index 0000000..ffdfea5 --- /dev/null +++ b/server.py @@ -0,0 +1,143 @@ +import socketserver +import socket +import threading +import time +import pathlib +import json + +from classes.config import MonsterConfig + + +class ReportMonsterHandler(socketserver.BaseRequestHandler): + def requires_login(func, *args, **kwargs) : + def inner(self, *args, **kwargs) : + if not self.user: + return "95 Authentication Required: use login to authenticate first" + return func(self, *args, **kwargs) + return inner + + def command_login(self, *args): + if not len(args) == 2: + return "90 Invalid Argument: login takes exactly two arguments" + + for cuser in self.server.config.users: + if cuser.username == args[0]: + if cuser.validatePasword(args[1]): + self.user = cuser + return f"10 Login Successful: logged in as {self.user.username}" + + return "99 Authentication Failed: username or password incorrect" + + def command_whoami(self, *args): + if args: + return "90 Invalid Argument: whoami takes no arguments" + if self.user: + return "11 Logged In: logged in as {self.user.username}" + return "12 Not Logged In" + + @requires_login + def command_vessels(self, *args): + return "20 OK: " + json.dumps([vessel.name for vessel in self.server.config.vessels]) + + @requires_login + def command_users(self, *args): + if not len(args) == 1: + return "90 Invalid Argument: users takes exactly one argument" + + vessels = list(filter(lambda v: v.name == args[0], self.server.config.vessels)) + + if not vessels: + return f"92 No Such Object: no vessel called {args[0]} found" + if len(vessels) > 1: + return f"98 Configuration Error: multiple vessels called {args[0]} found" + + vessel = vessels[0] + users = vessel.getUsers() + + result = [user for user in users.values()] + + return "20 OK: " + json.dumps(result) + + @requires_login + def command_user(self, *args): + if not len(args) == 2: + return "90 Invalid Argument: user takes exactly two arguments" + + vessels = list(filter(lambda v: v.name == args[0], self.server.config.vessels)) + + if not vessels: + return f"92 No Such Object: no vessel called {args[0]} found" + if len(vessels) > 1: + return f"98 Configuration Error: multiple vessels called {args[0]} found" + + vessel = vessels[0] + users = vessel.getUsers(username = args[1]) + + if not users: + return f"92 No Such Object: no user called {args[1]} found on vessel {args[0]}" + if len(users) > 1: + return f"98 Configuration Error: multiple users called {args[1]} found on vessel {args[0]}" + + return "20 OK: " + json.dumps(list(users.values())[0]) + + def handle(self): + self.user = False + + self.request.sendall("ReportMonster Server\n(c) Kumi Systems e.U. (https://kumi.systems/)\n\n> ".encode()) + + while True: + data = self.request.recv(1024) + if data: + plain = data.decode() + if not (parts := plain.split()): + response = "00 No Command Received" + + else: + parts = plain.split() + + command = parts[0] + args = parts[1:] + + COMMANDS = { + "login": self.command_login, + "user": self.command_user, + "users": self.command_users, + "vessels": self.command_vessels, + "w": self.command_whoami, + "whoami": self.command_whoami, + } + + if command in COMMANDS.keys(): + response = COMMANDS[command](*args) + else: + response = f"91 Invalid Command: don't know command {command}" + + self.request.sendall(response.encode()) + self.request.sendall("\n> ".encode()) + + +class ReportMonsterServer(socketserver.ThreadingMixIn, socketserver.TCPServer): + allow_reuse_address = True + + +if __name__ == "__main__": + HOST = "127.0.0.1" + PORT = 6789 + + CONFIG = MonsterConfig(pathlib.Path(__file__).resolve().parent / "settings.ini") + + server = ReportMonsterServer((HOST, PORT), ReportMonsterHandler) + server.config = CONFIG + + with server: + st = threading.Thread(target=server.serve_forever) + st.daemon = True + st.start() + + try: + while st.is_alive(): + time.sleep(1) + except KeyboardInterrupt: + print("Received Ctrl+C - stopping server...") + server.shutdown() + st.join() \ No newline at end of file diff --git a/settings.dist.ini b/settings.dist.ini index c1ffa85..21748b1 100644 --- a/settings.dist.ini +++ b/settings.dist.ini @@ -1,5 +1,5 @@ [MONSTER] -PyAdonis = /opt/pyadonis/ +PyAdonis = /opt/pyadonis/ [Vessel vessel1] Host = 10.12.13.14 @@ -9,9 +9,11 @@ Database = moodle ssh = 0 [Vessel vessel2] -Host = 192.168.95192.1 +Host = 192.168.192.1 Username = monster Password = jfaskldfjklasfh Database = moodle ssh = 1 +[User test] +Password = test \ No newline at end of file