From 87422129525195474ae7c13c7858b61dc8efbb0c Mon Sep 17 00:00:00 2001 From: damp11113 Date: Sun, 17 Mar 2024 19:49:54 +0700 Subject: [PATCH] new update 4.0 --- .gitignore | 2 + demo/opensource.png | Bin 0 -> 79942 bytes demo/private_key.pem | 27 +++ demo/server.py | 124 +++++++++++ setup.cfg | 3 + setup.py | 21 ++ src/PyserSSH/__init__.py | 30 +++ src/PyserSSH/account.py | 152 +++++++++++++ src/PyserSSH/extensions/processbar.py | 303 ++++++++++++++++++++++++++ src/PyserSSH/interactive.py | 124 +++++++++++ src/PyserSSH/server.py | 303 ++++++++++++++++++++++++++ src/PyserSSH/system/SFTP.py | 199 +++++++++++++++++ src/PyserSSH/system/info.py | 34 +++ src/PyserSSH/system/inputsystem.py | 157 +++++++++++++ src/PyserSSH/system/interface.py | 89 ++++++++ src/PyserSSH/system/syscom.py | 64 ++++++ src/PyserSSH/system/sysfunc.py | 31 +++ upload.bat | 12 + 18 files changed, 1675 insertions(+) create mode 100644 .gitignore create mode 100644 demo/opensource.png create mode 100644 demo/private_key.pem create mode 100644 demo/server.py create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 src/PyserSSH/__init__.py create mode 100644 src/PyserSSH/account.py create mode 100644 src/PyserSSH/extensions/processbar.py create mode 100644 src/PyserSSH/interactive.py create mode 100644 src/PyserSSH/server.py create mode 100644 src/PyserSSH/system/SFTP.py create mode 100644 src/PyserSSH/system/info.py create mode 100644 src/PyserSSH/system/inputsystem.py create mode 100644 src/PyserSSH/system/interface.py create mode 100644 src/PyserSSH/system/syscom.py create mode 100644 src/PyserSSH/system/sysfunc.py create mode 100644 upload.bat 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 0000000000000000000000000000000000000000..8f7ac9e97a94d66961b53e5a91d918530d5d2a76 GIT binary patch literal 79942 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±)vw6b6ukIVN}-2; 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

rEiR%R`9|e;iy}91n*>1bVCw4VFK(Q}JswX^+yM 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" # 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