diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7c86c3a --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +dist/ +src/PyserSSH.egg-info/ \ No newline at end of file diff --git a/demo/opensource.png b/demo/opensource.png new file mode 100644 index 0000000..8f7ac9e Binary files /dev/null and b/demo/opensource.png differ diff --git a/demo/private_key.pem b/demo/private_key.pem new file mode 100644 index 0000000..2a48623 --- /dev/null +++ b/demo/private_key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAq7UgQtL4Nv2s8rvaJjQryNxpsKpcSeDIsABnvry6Xkd3KhOi +K3c4dkYJjiAb4w4wfPiJ7sFL/PFP3f/slcpNHz18meWZkktia3rBX8uyJQ3soyNw +Vbxm6mOPntAqC4JBoPaYS4HABSYxJYY6yPU1i0UufvWg5pNRgeZIM8kQSyie4q1C +AEFG1T6sabJ5mOWH8Yw/zu3nTQpz2yIZYSVOsvJxBtaCEHCThhmQk2jPb88Ss0XT +a7uzaX/UIRktDz1FN6ooJbFqHsHxOsZJrC6YdZ3lo7DZYJU+jclG4jy95rGe7KpE +0p9cMAYNO0ya6toJM6GwFzJEk+HD0BTxdi7dKQIDAQABAoIBAFY1ciUa1xSE+LhG +KJjVyMXoJAhXAE73VMtI6M2S499B8kpl4R4BlY+MSm/ZHyc4kI+uGVKOKiCs53SG +cboi/+WXcV+zLw+MWbWsxDncg2ynORAPUu840FMN+aW6zeFJXLn8FSqT0lzDeBlm +80zCEEgES/viRw59GIcnn0igwlV5EzO6zhWzwdeMpBO4XFFDaiEY5idIBQf4jCEY +JcfQOrkPpfPgjLyQmFLyeojyaUVLIOOLUGMsSS8Hk7MJlgEdneEIXX7EhPqLDPyc +1f33WsnlTvbHLGWHE7lMG1LbK1ecsNwWWUFfoVQaQzYUQAVSuoqTYYkAYfGnQmV0 +nnsIUAECgYEA7xR8cqu/knx9gFeinYwLx+/BbUw1MAX0WSK4GOx0O7qgbDb+scb2 +x4aBCZnDE44KRl1/mbLXxb3wq8GN7W4owIHSud/8gZNqJM0uXbiJS7gMW0XnoJuk +D4hr0ADddPn/vGfQyUf0oUOFg2nP+H99GEuyYpHXr39R4Fh4UljXEkECgYEAt9wG +GRUhW6BoE3t2/mUcgkpXrljI7W2SDtHGgGzNb8Mlxcu/KUC4b6qdNwe83t0w3SaP ++34JHXIqnb2cuvigQj8pCoFxaMT6gH7x1zQWI1cORCA+Vfx/NZ2cCyXpebNOytxu +AwtAVo+r/QZlfs4OlG+TVwKBxYz9huCPFaAfQOkCgYA5iViZ0DN+cW9Sn8SG3dlH ++K84OoriT8yKVwyvEti2Nye8Y0/QQO3K/te3E8Yawqg+XuoCd0PuVtPAwggCB+zO +x2+LRBhkprF4wdhSvcJs8pImtSAVSt+kzVQE7vBc4n1lPibFCggZd0J+acyfJS9Z +1X3MswSRO7bcou3yA2dfAQKBgAHz3Dy39Lq8YV6TmRfqivr3Pyci2j9rQnnV0H3c +qfHd6LDJESanAU5uSW0kL+VOBA7VMgJBvGcLp1g1g0yZB1qswQrThRjPvrlOn9Lh +QrrtWcFvdjoDjHZNTjLwHCKmvNd6r9Bodi51KCZvwvQtzAnXhYEPDcHDVY3xJJPe +N3bBAoGADnEc8G2taL/tq7Skcw1G/cZYUp4CZw+ypCLd1xyus7Lnu9Wl6y3U0HEl +pjgzBGlwvTRkvC5ewz46WIWE+hlmOdSih81Cro48baXR2T1OD8jKQ2pXmHK5Z/wy +0V7t7eHd/k8CcXzIWIk6gmpOYhKkIVvQW5g7ssbwsfsk3qD++Fs= +-----END RSA PRIVATE KEY----- diff --git a/demo/server.py b/demo/server.py new file mode 100644 index 0000000..eece326 --- /dev/null +++ b/demo/server.py @@ -0,0 +1,124 @@ +import os +import socket +import time +import shlex +from damp11113 import TextFormatter +import cv2 +import traceback + +from PyserSSH import Server, AccountManager, Send, wait_input, wait_inputkey +from PyserSSH.system.info import system_banner +from PyserSSH.extensions.processbar import indeterminateStatus, LoadingProgress + +useraccount = AccountManager() +useraccount.add_account("admin", "") # create user without password + +ssh = Server(useraccount, system_commands=True, system_message=False) + +nonamewarning = """Connection Warning: +Unauthorized access or improper use of this system is prohibited. +Please ensure you have proper authorization before proceeding.""" + +Authorizedmessage = """You have successfully connected to the server. +Enjoy your session and remember to follow security protocols.""" + +@ssh.on_user("connect") +def connect(channel, client): + #print(client["windowsize"]) + if client['current_user'] == "": + warningmessage = nonamewarning + else: + warningmessage = Authorizedmessage + + + wm = f"""********************************************************************************************* +Hello {client['current_user']}, + +{warningmessage} + +{system_banner} +*********************************************************************************************""" + + for char in wm: + Send(channel, char, ln=False) + time.sleep(0.005) # Adjust the delay as needed + Send(channel, '\n') # Send newline after each line + +@ssh.on_user("error") +def error(channel, error, client): + if isinstance(error, socket.error): + pass + else: + Send(channel, traceback.format_exc()) + +@ssh.on_user("command") +def command(channel, command: str, client): + if command == "passtest": + user = wait_input(channel, "username: ") + password = wait_input(channel, "password: ", password=True) + Send(channel, f"username: {user} | password: {password}") + elif command == "colortest": + for i in range(0, 255, 5): + Send(channel, TextFormatter.format_text_truecolor(" ", background=f"{i};0;0"), ln=False) + Send(channel, "") + for i in range(0, 255, 5): + Send(channel, TextFormatter.format_text_truecolor(" ", background=f"0;{i};0"), ln=False) + Send(channel, "") + for i in range(0, 255, 5): + Send(channel, TextFormatter.format_text_truecolor(" ", background=f"0;0;{i}"), ln=False) + Send(channel, "") + + Send(channel, "TrueColors 24-Bit") + elif command == "keytest": + user = wait_inputkey(channel, "press any key", raw=True) + Send(channel, "") + Send(channel, f"key: {user}") + for i in range(10): + user = wait_inputkey(channel, "press any key", raw=True) + Send(channel, "") + Send(channel, f"key: {user}") + elif command.startswith("typing"): + args = shlex.split(command) + messages = args[1] + speed = float(args[2]) + for w in messages: + Send(channel, w, ln=False) + time.sleep(speed) + Send(channel, "") + elif command == "renimtest": + image = cv2.imread(r"opensource.png", cv2.IMREAD_COLOR) + image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) + + width, height = client['windowsize']["width"], client['windowsize']["height"] + + # resize image + resized = cv2.resize(image, (width, height)) + + # Scan all pixels + for y in range(0, height): + for x in range(0, width): + pixel_color = resized[y, x] + #PyserSSH.Send(channel, f"Pixel color at ({x}, {y}): {pixel_color}") + if pixel_color.tolist() != [0, 0, 0]: + Send(channel, TextFormatter.format_text_truecolor(" ", background=f"{pixel_color[0]};{pixel_color[1]};{pixel_color[2]}"), ln=False) + else: + Send(channel, " ", ln=False) + + Send(channel, "") + elif command == "errortest": + raise Exception("hello error") + elif command == "inloadtest": + loading = indeterminateStatus(client) + loading.start() + time.sleep(5) + loading.stop() + elif command == "loadtest": + l = LoadingProgress(client, total=100, color=True) + l.start() + for i in range(101): + l.current = i + l.desc = "loading..." + time.sleep(0.05) + l.stop() + +ssh.run(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'private_key.pem')) \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..8c99867 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,3 @@ +[metadata] +description-file=README.md +license_files=LICENSE.rst \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..a6aac67 --- /dev/null +++ b/setup.py @@ -0,0 +1,21 @@ +from setuptools import setup, find_packages + +with open('README.md', 'r', encoding='utf-8') as f: + long_description = f.read() + +setup( + name='PyserSSH', + version='4.0', + license='MIT', + author='damp11113', + author_email='damp51252@gmail.com', + packages=find_packages('src'), + package_dir={'': 'src'}, + url='https://github.com/damp11113/PyserSSH', + description="A easy ssh server", + long_description=long_description, + long_description_content_type='text/markdown', + install_requires=[ + "paramiko" + ] +) \ No newline at end of file diff --git a/src/PyserSSH/__init__.py b/src/PyserSSH/__init__.py new file mode 100644 index 0000000..982647d --- /dev/null +++ b/src/PyserSSH/__init__.py @@ -0,0 +1,30 @@ +""" +PyserSSH - A SSH server. For more info visit https://github.com/damp11113/PyserSSH +Copyright (C) 2023-2024 damp11113 (MIT) + +Visit https://github.com/damp11113/PyserSSH + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +from .interactive import * +from .server import Server +from .account import AccountManager diff --git a/src/PyserSSH/account.py b/src/PyserSSH/account.py new file mode 100644 index 0000000..c9f52a5 --- /dev/null +++ b/src/PyserSSH/account.py @@ -0,0 +1,152 @@ +""" +PyserSSH - A SSH server. For more info visit https://github.com/damp11113/PyserSSH +Copyright (C) 2023-2024 damp11113 (MIT) + +Visit https://github.com/damp11113/PyserSSH + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import pickle + +class AccountManager: + def __init__(self, anyuser=False, historylimit=10): + self.accounts = {} + self.anyuser = anyuser + self.historylimit = historylimit + + if self.anyuser: + print("history system can't work if 'anyuser' is enable") + + def validate_credentials(self, username, password): + if username in self.accounts and self.accounts[username]["password"] == password or self.anyuser: + return True + return False + + def get_permissions(self, username): + if username in self.accounts: + return self.accounts[username]["permissions"] + return [] + + def set_prompt(self, username, prompt=">"): + if username in self.accounts: + self.accounts[username]["prompt"] = prompt + + def get_prompt(self, username): + if username in self.accounts and "prompt" in self.accounts[username]: + return self.accounts[username]["prompt"] + return ">" # Default prompt if not set for the user + + def add_account(self, username, password, permissions={}): + self.accounts[username] = {"password": password, "permissions": permissions} + + def change_password(self, username, new_password): + if username in self.accounts: + self.accounts[username]["password"] = new_password + + def set_permissions(self, username, new_permissions): + if username in self.accounts: + self.accounts[username]["permissions"] = new_permissions + + def save_to_file(self, filename): + with open(filename, 'wb') as file: + pickle.dump(self.accounts, file) + + def load_from_file(self, filename): + try: + with open(filename, 'rb') as file: + self.accounts = pickle.load(file) + except FileNotFoundError: + print("File not found. No accounts loaded.") + except Exception as e: + print(f"An error occurred: {e}. No accounts loaded.") + + def set_user_sftp_allow(self, username, allow=True): + if username in self.accounts: + self.accounts[username]["sftp_allow"] = allow + + def get_user_sftp_allow(self, username): + if username in self.accounts and "sftp_allow" in self.accounts[username]: + if self.anyuser: + return True + return self.accounts[username]["sftp_allow"] + return True + + def set_user_sftp_readonly(self, username, readonly=False): + if username in self.accounts: + self.accounts[username]["sftp_readonly"] = readonly + + def get_user_sftp_readonly(self, username): + if username in self.accounts and "sftp_readonly" in self.accounts[username]: + return self.accounts[username]["sftp_readonly"] + return False + + def set_user_sftp_path(self, username, path="/"): + if username in self.accounts: + if path == "/": + self.accounts[username]["sftp_path"] = "" + else: + self.accounts[username]["sftp_path"] = path + + def get_user_sftp_path(self, username): + if username in self.accounts and "sftp_path" in self.accounts[username]: + return self.accounts[username]["sftp_path"] + return "" + + def add_history(self, username, command): + if not self.anyuser: + if username in self.accounts: + if "history" not in self.accounts[username]: + self.accounts[username]["history"] = [] # Initialize history list if it doesn't exist + + history_limit = self.historylimit if self.historylimit is not None else float('inf') + self.accounts[username]["history"].append(command) + self.accounts[username]["lastcommand"] = command + # Trim history to the specified limit + if self.historylimit != None: + if len(self.accounts[username]["history"]) > history_limit: + self.accounts[username]["history"] = self.accounts[username]["history"][-history_limit:] + + def clear_history(self, username): + if not self.anyuser: + if username in self.accounts: + self.accounts[username]["history"] = [] # Initialize history list if it doesn't exist + + def get_history(self, username, index, getall=False): + if not self.anyuser: + if username in self.accounts and "history" in self.accounts[username]: + history = self.accounts[username]["history"] + history.reverse() + if getall: + return history + else: + if index < len(history): + return history[index] + else: + return None # Index out of range + return None # User or history not found + + def get_lastcommand(self, username): + if not self.anyuser: + if username in self.accounts and "lastcommand" in self.accounts[username]: + command = self.accounts[username]["lastcommand"] + return command + return None # User or history not found \ No newline at end of file diff --git a/src/PyserSSH/extensions/processbar.py b/src/PyserSSH/extensions/processbar.py new file mode 100644 index 0000000..0c15f0b --- /dev/null +++ b/src/PyserSSH/extensions/processbar.py @@ -0,0 +1,303 @@ +""" +PyserSSH - A SSH server. For more info visit https://github.com/damp11113/PyserSSH +Copyright (C) 2023-2024 damp11113 (MIT) + +Visit https://github.com/damp11113/PyserSSH + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" +# this file is from damp11113-library + +from itertools import cycle, islice +import math +import time +from threading import Thread +from time import sleep + +from ..interactive import Print + +try: + from damp11113.utils import get_size_unit2, center_string, TextFormatter, insert_string +except: + raise ModuleNotFoundError("This extension is require damp11113-library") + +steps1 = ['[ ]', '[- ]', '[-- ]', '[---]', '[ --]', '[ -]'] +steps2 = ['[ ]', '[- ]', '[ - ]', '[ -]'] +steps3 = ['[ ]', '[- ]', '[-- ]', '[ --]', '[ -]', '[ ]', '[ -]', '[ --]', '[-- ]', '[- ]'] +steps4 = ['[ ]', '[- ]', '[ - ]', '[ -]', '[ ]', '[ -]', '[ - ]', '[- ]', '[ ]'] +steps5 = ['[ ]', '[ -]', '[ --]', '[---]', '[-- ]', '[- ]'] +steps6 = ['[ ]', '[ -]', '[ - ]', '[- ]'] + +class indeterminateStatus: + def __init__(self, client, desc="Loading...", end="[ ✔ ]", timeout=0.1, fail='[ ❌ ]', steps=None): + self.channel = client['channel'] + self.windowsize = client["windowsize"] + + self.desc = desc + self.end = end + self.timeout = timeout + self.faill = fail + + self._thread = Thread(target=self._animate, daemon=True) + if steps is None: + self.steps = steps1 + else: + self.steps = steps + self.done = False + self.fail = False + + def start(self): + self._thread.start() + return self + + def _animate(self): + for c in cycle(self.steps): + if self.done: + break + Print(self.channel, f"\r{c} {self.desc}" , end="") + sleep(self.timeout) + + def __enter__(self): + self.start() + + def stop(self): + self.done = True + cols = self.windowsize["width"] + Print(self.channel, "\r" + " " * cols, end="") + Print(self.channel, f"\r{self.end}") + + def stopfail(self): + self.done = True + self.fail = True + cols = self.windowsize["width"] + Print(self.channel, "\r" + " " * cols, end="") + Print(self.channel, f"\r{self.faill}") + + def __exit__(self, exc_type, exc_value, tb): + # handle exceptions with those variables ^ + self.stop() + +class LoadingProgress: + def __init__(self, client, total=100, totalbuffer=None, length=50, fill='█', fillbufferbar='█', desc="Loading...", status="", enabuinstatus=True, end="[ ✔ ]", timeout=0.1, fail='[ ❌ ]', steps=None, unit="it", barbackground="-", shortnum=False, buffer=False, shortunitsize=1000, currentshortnum=False, show=True, indeterminate=False, barcolor="red", bufferbarcolor="white",barbackgroundcolor="black", color=True): + """ + Simple loading progress bar python + @param client: from ssh client request + @param total: change all total + @param desc: change description + @param status: change progress status + @param end: change success progress + @param timeout: change speed + @param fail: change error stop + @param steps: change steps animation + @param unit: change unit + @param buffer: enable buffer progress (experiment) + @param show: show progress bar + @param indeterminate: indeterminate mode + @param barcolor: change bar color + @param bufferbarcolor: change buffer bar color + @param barbackgroundcolor: change background color + @param color: enable colorful + """ + self.channel = client["channel"] + self.windowsize = client["windowsize"] + + self.desc = desc + self.end = end + self.timeout = timeout + self.faill = fail + self.total = total + self.length = length + self.fill = fill + self.enbuinstatus = enabuinstatus + self.status = status + self.barbackground = barbackground + self.unit = unit + self.shortnum = shortnum + self.shortunitsize = shortunitsize + self.currentshortnum = currentshortnum + self.printed = show + self.indeterminate = indeterminate + self.barcolor = barcolor + self.barbackgroundcolor = barbackgroundcolor + self.enabuffer = buffer + self.bufferbarcolor = bufferbarcolor + self.fillbufferbar = fillbufferbar + self.totalbuffer = totalbuffer + self.enacolor = color + + self._thread = Thread(target=self._animate, daemon=True) + + if steps is None: + self.steps = steps1 + else: + self.steps = steps + + if self.totalbuffer is None: + self.totalbuffer = self.total + + self.currentpercent = 0 + self.currentbufferpercent = 0 + self.current = 0 + self.currentbuffer = 0 + self.startime = 0 + self.done = False + self.fail = False + self.currentprint = "" + + def start(self): + self._thread.start() + self.startime = time.perf_counter() + return self + + def update(self, i): + self.current += i + + def updatebuffer(self, i): + self.currentbuffer += i + + def _animate(self): + for c in cycle(self.steps): + if self.done: + break + + if not self.indeterminate: + if self.total != 0 or math.trunc(float(self.currentpercent)) > 100: + if self.enabuffer: + self.currentpercent = ("{0:.1f}").format(100 * (self.current / float(self.total))) + + filled_length = int(self.length * self.current // self.total) + + if self.enacolor: + bar = TextFormatter.format_text(self.fill * filled_length, self.barcolor) + else: + bar = self.fill * filled_length + + self.currentbufferpercent = ("{0:.1f}").format( + 100 * (self.currentbuffer / float(self.totalbuffer))) + + if float(self.currentbufferpercent) >= 100.0: + self.currentbufferpercent = 100 + + filled_length_buffer = int(self.length * self.currentbuffer // self.totalbuffer) + + if filled_length_buffer >= self.length: + filled_length_buffer = self.length + + if self.enacolor: + bufferbar = TextFormatter.format_text(self.fillbufferbar * filled_length_buffer, + self.bufferbarcolor) + else: + bufferbar = self.fillbufferbar * filled_length_buffer + + bar = insert_string(bufferbar, bar) + + if self.enacolor: + bar += TextFormatter.format_text(self.barbackground * (self.length - filled_length_buffer), + self.barbackgroundcolor) + else: + bar += self.barbackground * (self.length - filled_length_buffer) + else: + self.currentpercent = ("{0:.1f}").format(100 * (self.current / float(self.total))) + filled_length = int(self.length * self.current // self.total) + if self.enacolor: + bar = TextFormatter.format_text(self.fill * filled_length, self.barcolor) + + bar += TextFormatter.format_text(self.barbackground * (self.length - filled_length), + self.barbackgroundcolor) + else: + bar = self.fill * filled_length + if self.enacolor: + bar = TextFormatter.format_text(bar, self.barcolor) + bar += self.barbackground * (self.length - filled_length) + + + if self.enbuinstatus: + elapsed_time = time.perf_counter() - self.startime + speed = self.current / elapsed_time if elapsed_time > 0 else 0 + remaining = self.total - self.current + eta_seconds = remaining / speed if speed > 0 else 0 + elapsed_formatted = time.strftime('%H:%M:%S', time.gmtime(elapsed_time)) + eta_formatted = time.strftime('%H:%M:%S', time.gmtime(eta_seconds)) + if self.shortnum: + stotal = get_size_unit2(self.total, '', False, self.shortunitsize, False, '') + scurrent = get_size_unit2(self.current, '', False, self.shortunitsize, self.currentshortnum, '') + else: + stotal = self.total + scurrent = self.current + + if math.trunc(float(self.currentpercent)) > 100: + elapsed_time = time.perf_counter() - self.startime + elapsed_formatted = time.strftime('%H:%M:%S', time.gmtime(elapsed_time)) + + bar = center_string(self.barbackground * self.length, TextFormatter.format_text("Indeterminate", self.barcolor)) + + self.currentprint = f"{c} {self.desc} | --%|{bar}| {scurrent}/{stotal} | {elapsed_formatted} | {get_size_unit2(speed, self.unit, self.shortunitsize)} | {self.status}" + + else: + self.currentprint = f"{c} {self.desc} | {math.trunc(float(self.currentpercent))}%|{bar}| {scurrent}/{stotal} | {elapsed_formatted}<{eta_formatted} | {get_size_unit2(speed, self.unit, self.shortunitsize)} | {self.status}" + else: + if self.shortnum: + stotal = get_size_unit2(self.total, '', False, self.shortunitsize, False, '') + scurrent = get_size_unit2(self.current, '', False, self.shortunitsize, self.currentshortnum, '') + else: + stotal = self.total + scurrent = self.current + + self.currentprint = f"{c} {self.desc} | {math.trunc(float(self.currentpercent))}%|{bar}| {scurrent}/{stotal} | {self.status}" + else: + elapsed_time = time.perf_counter() - self.startime + elapsed_formatted = time.strftime('%H:%M:%S', time.gmtime(elapsed_time)) + + bar = center_string(self.barbackground * self.length, TextFormatter.format_text("Indeterminate", self.barcolor)) + + self.currentprint = f"{c} {self.desc} | --%|{bar}| {elapsed_formatted} | {self.status}" + else: + elapsed_time = time.perf_counter() - self.startime + elapsed_formatted = time.strftime('%H:%M:%S', time.gmtime(elapsed_time)) + + bar = center_string(self.barbackground * self.length, TextFormatter.format_text("Indeterminate", self.barcolor)) + + self.currentprint = f"{c} {self.desc} | --%|{bar}| {elapsed_formatted} | {self.status}" + + if self.printed: + Print(self.channel, f"\r{self.currentprint}", end="") + + sleep(self.timeout) + + def __enter__(self): + self.start() + + def stop(self): + self.done = True + cols = self.windowsize["width"] + Print(self.channel, "\r" + " " * cols, end="") + Print(self.channel, f"\r{self.end}") + + def stopfail(self): + self.done = True + self.fail = True + cols = self.windowsize["width"] + Print(self.channel, "\r" + " " * cols, end="") + Print(self.channel, f"\r{self.faill}") + + def __exit__(self, exc_type, exc_value, tb): + # handle exceptions with those variables ^ + self.stop() \ No newline at end of file diff --git a/src/PyserSSH/interactive.py b/src/PyserSSH/interactive.py new file mode 100644 index 0000000..a777620 --- /dev/null +++ b/src/PyserSSH/interactive.py @@ -0,0 +1,124 @@ +""" +PyserSSH - A SSH server. For more info visit https://github.com/damp11113/PyserSSH +Copyright (C) 2023-2024 damp11113 (MIT) + +Visit https://github.com/damp11113/PyserSSH + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +from .system.sysfunc import replace_enter_with_crlf + +def Send(channel, string, ln=True): + if ln: + channel.send(replace_enter_with_crlf(string + "\n")) + else: + channel.send(replace_enter_with_crlf(string)) + +def Print(channel, string, start="", end="\n"): + channel.send(replace_enter_with_crlf(start + string + end)) + +def Clear(client): + channel = client["channel"] + sx, sy = client["windowsize"]["width"], client["windowsize"]["height"] + + for x in range(sx): + for y in range(sy): + Send(channel, '\b \b', ln=False) # Send newline after each line + +def wait_input(channel, prompt="", defaultvalue=None, cursor_scroll=False, echo=True, password=False, passwordmask=b"*", noabort=False): + channel.send(replace_enter_with_crlf(prompt)) + + buffer = bytearray() + cursor_position = 0 + + try: + while True: + byte = channel.recv(1) + + if not byte or byte == b'\x04': + raise EOFError() + elif byte == b'\x03' and not noabort: + break + elif byte == b'\t': + pass + elif byte == b'\x7f' or byte == b'\x08': # Backspace + if cursor_position > 0: + # Move cursor back, erase character, move cursor back again + channel.sendall(b'\b \b') + buffer = buffer[:cursor_position - 1] + buffer[cursor_position:] + cursor_position -= 1 + elif byte == b'\x1b' and channel.recv(1) == b'[': # Arrow keys + arrow_key = channel.recv(1) + if cursor_scroll: + if arrow_key == b'C': # Right arrow key + if cursor_position < len(buffer): + channel.sendall(b'\x1b[C') + cursor_position += 1 + elif arrow_key == b'D': # Left arrow key + if cursor_position > 0: + channel.sendall(b'\x1b[D') + cursor_position -= 1 + elif byte in (b'\r', b'\n'): # Enter key + break + else: # Regular character + buffer = buffer[:cursor_position] + byte + buffer[cursor_position:] + cursor_position += 1 + if echo or password: + if password: + channel.sendall(passwordmask) + else: + channel.sendall(byte) + + channel.sendall(b'\r\n') + + except Exception: + raise + + output = buffer.decode('utf-8') + + # Return default value if specified and no input given + if defaultvalue is not None and not output.strip(): + return defaultvalue + else: + return output + +def wait_inputkey(channel, prompt="", raw=False): + if prompt != "": + channel.send(replace_enter_with_crlf(prompt)) + + try: + byte = channel.recv(10) + + if not raw: + if not byte or byte == b'\x04': + raise EOFError() + + elif byte == b'\t': + pass + + return byte.decode('utf-8') + + else: + return byte + + except Exception: + raise \ No newline at end of file diff --git a/src/PyserSSH/server.py b/src/PyserSSH/server.py new file mode 100644 index 0000000..52a46ba --- /dev/null +++ b/src/PyserSSH/server.py @@ -0,0 +1,303 @@ +""" +PyserSSH - A SSH server. For more info visit https://github.com/damp11113/PyserSSH +Copyright (C) 2023-2024 damp11113 (MIT) + +Visit https://github.com/damp11113/PyserSSH + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import os +import time +import paramiko +import socket +import threading +from functools import wraps +import logging + +from .system.SFTP import SSHSFTPServer +from .system.interface import Sinterface +from .interactive import * +from .system.inputsystem import expect +from .system.info import system_banner + +try: + os.environ["pyserssh_systemmessage"] +except: + os.environ["pyserssh_systemmessage"] = "YES" + +if os.environ["pyserssh_systemmessage"] == "YES": + print(system_banner) + +#paramiko.sftp_file.SFTPFile.MAX_REQUEST_SIZE = pow(2, 22) + +sftpclient = ["WinSCP", "Xplore"] + +logger = logging.getLogger("PyserSSH") +logger.disabled = True + +class Server: + def __init__(self, accounts, system_message=True, timeout=0, disable_scroll_with_arrow=True, sftp=True, sftproot=os.getcwd(), system_commands=False, compression=True, usexternalauth=False, history=True): + """ + A simple SSH server + """ + self._event_handlers = {} + self.sysmess = system_message + self.client_handlers = {} # Dictionary to store event handlers for each client + self.current_users = {} # Dictionary to store current_user for each connected client + self.accounts = accounts + self.timeout = timeout + self.disable_scroll_with_arrow = disable_scroll_with_arrow + self.sftproot = sftproot + self.sftpena = sftp + self.enasyscom = system_commands + self.compressena = compression + self.usexternalauth = usexternalauth + self.history = history + + self.system_banner = system_banner + + if self.enasyscom: + print("\033[33m!!Warning!! System commands is enable! \033[0m") + + def on_user(self, event_name): + def decorator(func): + @wraps(func) + def wrapper(channel, *args, **kwargs): + # Ignore the third argument + filtered_args = args[:2] + args[3:] + return func(channel, *filtered_args, **kwargs) + self._event_handlers[event_name] = wrapper + return wrapper + return decorator + + def handle_client_disconnection(self, peername, current_user): + if peername in self.client_handlers: + del self.client_handlers[peername] + logger.info(f"User {current_user} disconnected") + + def _handle_event(self, event_name, *args, **kwargs): + handler = self._event_handlers.get(event_name) + if handler: + handler(*args, **kwargs) + if event_name == "disconnected": + self.handle_client_disconnection(*args, **kwargs) + + def handle_client(self, client, addr): + bh_session = paramiko.Transport(client) + bh_session.add_server_key(self.private_key) + + if self.sftpena: + SSHSFTPServer.ROOT = self.sftproot + SSHSFTPServer.ACCOUNT = self.accounts + SSHSFTPServer.CLIENTHANDELES = self.client_handlers + bh_session.set_subsystem_handler('sftp', paramiko.SFTPServer, SSHSFTPServer) + + if self.compressena: + bh_session.use_compression(True) + else: + bh_session.use_compression(False) + + bh_session.default_window_size = 2147483647 + bh_session.packetizer.REKEY_BYTES = pow(2, 40) + bh_session.packetizer.REKEY_PACKETS = pow(2, 40) + + server = Sinterface(self) + bh_session.start_server(server=server) + + logger.info(bh_session.remote_version) + + channel = bh_session.accept() + + if self.timeout != 0: + channel.settimeout(self.timeout) + + if channel is None: + logger.warning("no channel") + + try: + logger.info("user authenticated") + client_address = channel.getpeername() # Get client's address to identify the user + if client_address not in self.client_handlers: + # Create a new event handler for this client if it doesn't exist + self.client_handlers[client_address] = { + "event_handlers": {}, + "current_user": None, + "channel": channel, # Associate the channel with the client handler, + "last_activity_time": None, + "connecttype": None, + "last_login_time": None, + "windowsize": {} + } + client_handler = self.client_handlers[client_address] + client_handler["current_user"] = server.current_user + client_handler["channel"] = channel # Update the channel attribute for the client handler + client_handler["last_activity_time"] = time.time() + client_handler["last_login_time"] = time.time() + + peername = channel.getpeername() + + + #byte = channel.recv(1) + #if byte == b'\x00': + + #if not any(bh_session.remote_version.split("-")[2].startswith(prefix) for prefix in sftpclient): + if not channel.out_window_size == bh_session.default_window_size: + if self.sysmess: + channel.sendall(replace_enter_with_crlf(self.system_banner)) + channel.sendall(replace_enter_with_crlf("\n")) + + while self.client_handlers[channel.getpeername()]["windowsize"] == {}: + pass + + self._handle_event("connect", channel, self.client_handlers[channel.getpeername()]) + + client_handler["connecttype"] = "ssh" + try: + channel.send(replace_enter_with_crlf(self.accounts.get_prompt(self.client_handlers[channel.getpeername()]["current_user"]) + " ").encode('utf-8')) + while True: + expect(self, channel, peername) + except KeyboardInterrupt: + channel.close() + bh_session.close() + except Exception as e: + logger.error(e) + finally: + channel.close() + else: + if self.sftpena: + if self.accounts.get_user_sftp_allow(self.client_handlers[channel.getpeername()]["current_user"]): + client_handler["connecttype"] = "sftp" + self._handle_event("connectsftp", channel, self.client_handlers[channel.getpeername()]) + else: + del self.client_handlers[peername] + channel.close() + else: + del self.client_handlers[peername] + channel.close() + except: + raise + + def stop_server(self): + logger.info("Stopping the server...") + try: + for client_handler in self.client_handlers.values(): + channel = client_handler.get("channel") + if channel: + channel.close() + self.server.close() + logger.info("Server stopped.") + except Exception as e: + logger.error(f"Error occurred while stopping the server: {e}") + + def _start_listening_thread(self): + try: + self.server.listen(10) + logger.info("Start Listening for connections...") + while True: + client, addr = self.server.accept() + client_thread = threading.Thread(target=self.handle_client, args=(client, addr)) + client_thread.start() + + except Exception as e: + logger.error(e) + + def run(self, private_key_path, host="0.0.0.0", port=2222): + self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True) + self.server.bind((host, port)) + self.private_key = paramiko.RSAKey(filename=private_key_path) + + client_thread = threading.Thread(target=self._start_listening_thread) + client_thread.start() + + def kickbyusername(self, username, reason=None): + for peername, client_handler in list(self.client_handlers.items()): + if client_handler["current_user"] == username: + channel = client_handler.get("channel") + if reason is None: + if channel: + channel.close() + del self.client_handlers[peername] + logger.info(f"User '{username}' has been kicked.") + else: + if channel: + Send(channel, f"You have been disconnected for {reason}") + channel.close() + del self.client_handlers[peername] + logger.info(f"User '{username}' has been kicked by reason {reason}.") + + def kickbypeername(self, peername, reason=None): + client_handler = self.client_handlers.get(peername) + if client_handler: + channel = client_handler.get("channel") + if reason is None: + if channel: + channel.close() + del self.client_handlers[peername] + logger.info(f"peername '{peername}' has been kicked.") + else: + if channel: + Send(channel, f"You have been disconnected for {reason}") + channel.close() + del self.client_handlers[peername] + logger.info(f"peername '{peername}' has been kicked by reason {reason}.") + + def kickall(self, reason=None): + for peername, client_handler in self.client_handlers.items(): + channel = client_handler.get("channel") + if reason is None: + if channel: + channel.close() + else: + if channel: + Send(channel, f"You have been disconnected for {reason}") + channel.close() + + if reason is None: + self.client_handlers.clear() + logger.info("All users have been kicked.") + else: + logger.info(f"All users have been kicked by reason {reason}.") + + def broadcast(self, message): + for client_handler in self.client_handlers.values(): + channel = client_handler.get("channel") + if channel: + try: + # Send the message to the client + Send(channel, message) + except Exception as e: + logger.error(f"Error occurred while broadcasting message: {e}") + + def sendto(self, username, message): + for client_handler in self.client_handlers.values(): + if client_handler.get("current_user") == username: + channel = client_handler.get("channel") + if channel: + try: + # Send the message to the specific client + Send(channel, message) + except Exception as e: + logger.error(f"Error occurred while sending message to {username}: {e}") + break + else: + logger.warning(f"User '{username}' not found.") \ No newline at end of file diff --git a/src/PyserSSH/system/SFTP.py b/src/PyserSSH/system/SFTP.py new file mode 100644 index 0000000..de125cd --- /dev/null +++ b/src/PyserSSH/system/SFTP.py @@ -0,0 +1,199 @@ +""" +PyserSSH - A SSH server. For more info visit https://github.com/damp11113/PyserSSH +Copyright (C) 2023-2024 damp11113 (MIT) + +Visit https://github.com/damp11113/PyserSSH + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import os +import paramiko + +class SSHSFTPHandle(paramiko.SFTPHandle): + def stat(self): + try: + return paramiko.SFTPAttributes.from_stat(os.fstat(self.readfile.fileno())) + except OSError as e: + return paramiko.SFTPServer.convert_errno(e.errno) + + def chattr(self, attr): + # python doesn't have equivalents to fchown or fchmod, so we have to + # use the stored filename + try: + paramiko.SFTPServer.set_file_attr(self.filename, attr) + return paramiko.SFTP_OK + except OSError as e: + return paramiko.SFTPServer.convert_errno(e.errno) + +class SSHSFTPServer(paramiko.SFTPServerInterface): + ROOT = None + ACCOUNT = None + CLIENTHANDELES = None + + def _realpath(self, path): + return self.ROOT + self.canonicalize(path) + + def list_folder(self, path): + path = self._realpath(path) + try: + out = [] + flist = os.listdir(path) + for fname in flist: + attr = paramiko.SFTPAttributes.from_stat(os.stat(os.path.join(path, fname))) + attr.filename = fname + out.append(attr) + return out + except OSError as e: + return paramiko.SFTPServer.convert_errno(e.errno) + + def stat(self, path): + path = self._realpath(path) + try: + return paramiko.SFTPAttributes.from_stat(os.stat(path)) + except OSError as e: + return paramiko.SFTPServer.convert_errno(e.errno) + + def lstat(self, path): + path = self._realpath(path) + try: + return paramiko.SFTPAttributes.from_stat(os.lstat(path)) + except OSError as e: + return paramiko.SFTPServer.convert_errno(e.errno) + + def open(self, path, flags, attr): + path = self._realpath(path) + try: + binary_flag = getattr(os, 'O_BINARY', 0) + flags |= binary_flag + mode = getattr(attr, 'st_mode', None) + if mode is not None: + fd = os.open(path, flags, mode) + else: + # os.open() defaults to 0777 which is + # an odd default mode for files + fd = os.open(path, flags, 0o666) + except OSError as e: + return paramiko.SFTPServer.convert_errno(e.errno) + if (flags & os.O_CREAT) and (attr is not None): + attr._flags &= ~attr.FLAG_PERMISSIONS + paramiko.SFTPServer.set_file_attr(path, attr) + if flags & os.O_WRONLY: + if flags & os.O_APPEND: + fstr = 'ab' + else: + fstr = 'wb' + elif flags & os.O_RDWR: + if flags & os.O_APPEND: + fstr = 'a+b' + else: + fstr = 'r+b' + else: + # O_RDONLY (== 0) + fstr = 'rb' + try: + f = os.fdopen(fd, fstr) + except OSError as e: + return paramiko.SFTPServer.convert_errno(e.errno) + fobj = SSHSFTPHandle(flags) + fobj.filename = path + fobj.readfile = f + fobj.writefile = f + return fobj + + def remove(self, path): + path = self._realpath(path) + try: + os.remove(path) + except OSError as e: + return paramiko.SFTPServer.convert_errno(e.errno) + return paramiko.SFTP_OK + + def rename(self, oldpath, newpath): + oldpath = self._realpath(oldpath) + newpath = self._realpath(newpath) + try: + os.rename(oldpath, newpath) + except OSError as e: + return paramiko.SFTPServer.convert_errno(e.errno) + return paramiko.SFTP_OK + + def mkdir(self, path, attr): + path = self._realpath(path) + try: + os.mkdir(path) + if attr is not None: + paramiko.SFTPServer.set_file_attr(path, attr) + except OSError as e: + return paramiko.SFTPServer.convert_errno(e.errno) + return paramiko.SFTP_OK + + def rmdir(self, path): + path = self._realpath(path) + try: + os.rmdir(path) + except OSError as e: + return paramiko.SFTPServer.convert_errno(e.errno) + return paramiko.SFTP_OK + + def chattr(self, path, attr): + path = self._realpath(path) + try: + paramiko.SFTPServer.set_file_attr(path, attr) + except OSError as e: + return paramiko.SFTPServer.convert_errno(e.errno) + return paramiko.SFTP_OK + + def symlink(self, target_path, path): + path = self._realpath(path) + if (len(target_path) > 0) and (target_path[0] == '/'): + # absolute symlink + target_path = os.path.join(self.ROOT, target_path[1:]) + if target_path[:2] == '//': + # bug in os.path.join + target_path = target_path[1:] + else: + # compute relative to path + abspath = os.path.join(os.path.dirname(path), target_path) + if abspath[:len(self.ROOT)] != self.ROOT: + # this symlink isn't going to work anyway -- just break it immediately + target_path = '' + try: + os.symlink(target_path, path) + except OSError as e: + return paramiko.SFTPServer.convert_errno(e.errno) + return paramiko.SFTP_OK + + def readlink(self, path): + path = self._realpath(path) + try: + symlink = os.readlink(path) + except OSError as e: + return paramiko.SFTPServer.convert_errno(e.errno) + + if os.path.isabs(symlink): + if symlink[:len(self.ROOT)] == self.ROOT: + symlink = symlink[len(self.ROOT):] + if (len(symlink) == 0) or (symlink[0] != '/'): + symlink = '/' + symlink + else: + symlink = '' + return symlink diff --git a/src/PyserSSH/system/info.py b/src/PyserSSH/system/info.py new file mode 100644 index 0000000..9ee469c --- /dev/null +++ b/src/PyserSSH/system/info.py @@ -0,0 +1,34 @@ +""" +PyserSSH - A SSH server. For more info visit https://github.com/damp11113/PyserSSH +Copyright (C) 2023-2024 damp11113 (MIT) + +Visit https://github.com/damp11113/PyserSSH + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +version = "4.0" + +system_banner = ( + f"\033[36mPyserSSH V{version} \033[0m\n" + #"\033[33m!!Warning!! This is Testing Version of PyserSSH \033[0m\n" + "\033[35mUse Putty and WinSCP (SFTP) for best experience \033[0m" +) diff --git a/src/PyserSSH/system/inputsystem.py b/src/PyserSSH/system/inputsystem.py new file mode 100644 index 0000000..5213275 --- /dev/null +++ b/src/PyserSSH/system/inputsystem.py @@ -0,0 +1,157 @@ +""" +PyserSSH - A SSH server. For more info visit https://github.com/damp11113/PyserSSH +Copyright (C) 2023-2024 damp11113 (MIT) + +Visit https://github.com/damp11113/PyserSSH + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import time +import logging + +from .sysfunc import replace_enter_with_crlf +from .syscom import systemcommand + +logger = logging.getLogger("PyserSSH") +logger.disabled = True + +def expect(self, chan, peername, echo=True): + buffer = bytearray() + cursor_position = 0 + history_index_position = 0 # Initialize history index position outside the loop + currentuser = self.client_handlers[chan.getpeername()] + try: + while True: + byte = chan.recv(1) + self._handle_event("onrawtype", chan, byte, self.client_handlers[chan.getpeername()]) + + if self.timeout != 0: + self.client_handlers[chan.getpeername()]["last_activity_time"] = time.time() + + if not byte or byte == b'\x04': + raise EOFError() + elif byte == b'\x03': + pass + elif byte == b'\t': + pass + elif byte == b'\x7f' or byte == b'\x08': + if cursor_position > 0: + buffer = buffer[:cursor_position - 1] + buffer[cursor_position:] + cursor_position -= 1 + chan.sendall(b"\b \b") + elif byte == b"\x1b" and chan.recv(1) == b'[': + arrow_key = chan.recv(1) + if not self.disable_scroll_with_arrow: + if arrow_key == b'C': + # Right arrow key, move cursor right if not at the end + if cursor_position < len(buffer): + chan.sendall(b'\x1b[C') + cursor_position += 1 + elif arrow_key == b'D': + # Left arrow key, move cursor left if not at the beginning + if cursor_position > 0: + chan.sendall(b'\x1b[D') + cursor_position -= 1 + elif self.history: + if arrow_key == b'A': + if history_index_position == 0: + command = self.accounts.get_lastcommand(currentuser["current_user"]) + else: + command = self.accounts.get_history(currentuser["current_user"], history_index_position) + + # Clear the buffer + for i in range(cursor_position): + chan.send(b"\b \b") + + # Update buffer and cursor position with the new command + buffer = bytearray(command.encode('utf-8')) + cursor_position = len(buffer) + + # Print the updated buffer + chan.sendall(buffer) + + history_index_position += 1 + + if arrow_key == b'B': + if history_index_position != -1: + if history_index_position == 0: + command = self.accounts.get_lastcommand(currentuser["current_user"]) + else: + command = self.accounts.get_history(currentuser["current_user"], history_index_position) + + # Clear the buffer + for i in range(cursor_position): + chan.send(b"\b \b") + + # Update buffer and cursor position with the new command + buffer = bytearray(command.encode('utf-8')) + cursor_position = len(buffer) + + # Print the updated buffer + chan.sendall(buffer) + else: + history_index_position = 0 + for i in range(cursor_position): + chan.send(b"\b \b") + + buffer.clear() + cursor_position = 0 + + history_index_position -= 1 + + elif byte in (b'\r', b'\n'): + break + else: + history_index_position = -1 + buffer = buffer[:cursor_position] + byte + buffer[cursor_position:] + cursor_position += 1 + self._handle_event("ontype", chan, byte, self.client_handlers[chan.getpeername()]) + if echo: + chan.sendall(byte) + + if echo: + chan.sendall(b'\r\n') + + command = str(buffer.decode('utf-8')) + + try: + if self.enasyscom: + systemcommand(currentuser, command) + + self._handle_event("command", chan, command, currentuser) + except Exception as e: + self._handle_event("error", chan, e, currentuser) + + if self.history and command.strip() != "" and self.accounts.get_lastcommand(currentuser["current_user"]) != command: + self.accounts.add_history(currentuser["current_user"], command) + + try: + chan.send(replace_enter_with_crlf(self.accounts.get_prompt(currentuser["current_user"]) + " ").encode('utf-8')) + except: + logger.error("Send error") + + except Exception as e: + logger.error(str(e)) + finally: + if not byte: + logger.info(f"{peername} is disconnected") + self._handle_event("disconnected", peername, self.client_handlers[peername]["current_user"]) \ No newline at end of file diff --git a/src/PyserSSH/system/interface.py b/src/PyserSSH/system/interface.py new file mode 100644 index 0000000..53833c0 --- /dev/null +++ b/src/PyserSSH/system/interface.py @@ -0,0 +1,89 @@ +""" +PyserSSH - A SSH server. For more info visit https://github.com/damp11113/PyserSSH +Copyright (C) 2023-2024 damp11113 (MIT) + +Visit https://github.com/damp11113/PyserSSH + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import paramiko + +class Sinterface(paramiko.ServerInterface): + def __init__(self, serverself): + self.current_user = None + self.serverself = serverself + + def check_channel_request(self, kind, chanid): + if kind == 'session': + return paramiko.OPEN_SUCCEEDED + return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED + + def check_auth_password(self, username, password): + data = { + "username": username, + "password": password, + } + + if self.serverself.accounts.validate_credentials(username, password) and not self.serverself.usexternalauth: + self.current_user = username # Store the current user upon successful authentication + return paramiko.AUTH_SUCCESSFUL + else: + if self.serverself._handle_event("auth", data): + return paramiko.AUTH_SUCCESSFUL + else: + return paramiko.AUTH_FAILED + + def check_channel_pty_request(self, channel, term, width, height, pixelwidth, pixelheight, modes): + data = { + "term": term, + "width": width, + "height": height, + "pixelwidth": pixelwidth, + "pixelheight": pixelheight, + "modes": modes + } + data2 = { + "width": width, + "height": height, + "pixelwidth": pixelwidth, + "pixelheight": pixelheight, + } + self.serverself.client_handlers[channel.getpeername()]["windowsize"] = data2 + self.serverself._handle_event("connectpty", channel, data, self.serverself.client_handlers[channel.getpeername()]) + + return True + + def check_channel_shell_request(self, channel): + return True + + def check_channel_x11_request(self, channel, single_connection, auth_protocol, auth_cookie, screen_number): + return True + + def check_channel_window_change_request(self, channel, width: int, height: int, pixelwidth: int, pixelheight: int): + data = { + "width": width, + "height": height, + "pixelwidth": pixelwidth, + "pixelheight": pixelheight + } + self.serverself.client_handlers[channel.getpeername()]["windowsize"] = data + self.serverself._handle_event("resized", channel, data, self.serverself.client_handlers[channel.getpeername()]) diff --git a/src/PyserSSH/system/syscom.py b/src/PyserSSH/system/syscom.py new file mode 100644 index 0000000..e61687b --- /dev/null +++ b/src/PyserSSH/system/syscom.py @@ -0,0 +1,64 @@ +""" +PyserSSH - A SSH server. For more info visit https://github.com/damp11113/PyserSSH +Copyright (C) 2023-2024 damp11113 (MIT) + +Visit https://github.com/damp11113/PyserSSH + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +from ..interactive import * +from .info import version + +try: + from damp11113.info import pyofetch + from damp11113.utils import TextFormatter + damp11113lib = True +except: + damp11113lib = False + +def systemcommand(client, command): + channel = client["channel"] + + if command == "info": + if damp11113lib: + Send(channel, "Please wait...", ln=False) + pyf = pyofetch().info(f"{TextFormatter.format_text('PyserSSH Version', color='yellow')}: {TextFormatter.format_text(version, color='cyan')}") + Send(channel, " \r", ln=False) + for i in pyf: + Send(channel, i) + else: + Send(channel, "damp11113-library not available for use this command") + elif command == "whoami": + Send(channel, client["current_user"]) + elif command == "exit": + channel.close() + elif command == "clear": + Clear(client) + elif command == "fullscreentest": + Clear(client) + sx, sy = client["windowsize"]["width"], client["windowsize"]["height"] + + for x in range(sx): + for y in range(sy): + Send(channel, 'H', ln=False) # Send newline after each line + else: + return False \ No newline at end of file diff --git a/src/PyserSSH/system/sysfunc.py b/src/PyserSSH/system/sysfunc.py new file mode 100644 index 0000000..fbc8bde --- /dev/null +++ b/src/PyserSSH/system/sysfunc.py @@ -0,0 +1,31 @@ +""" +PyserSSH - A SSH server. For more info visit https://github.com/damp11113/PyserSSH +Copyright (C) 2023-2024 damp11113 (MIT) + +Visit https://github.com/damp11113/PyserSSH + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +def replace_enter_with_crlf(input_string): + if '\n' in input_string: + input_string = input_string.replace('\n', '\r\n') + return input_string \ No newline at end of file diff --git a/upload.bat b/upload.bat new file mode 100644 index 0000000..9717ff5 --- /dev/null +++ b/upload.bat @@ -0,0 +1,12 @@ +@echo off + +title change urllib3 to 1.26.15 +pip install urllib3==1.26.15 + +title building dist +python setup.py sdist + +title uploading to pypi +twine upload -r pypi dist/* + +pause \ No newline at end of file