new update 4.0

This commit is contained in:
dharm pimsen 2024-03-17 19:49:54 +07:00
parent 350796c441
commit 8742212952
18 changed files with 1675 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
dist/
src/PyserSSH.egg-info/

BIN
demo/opensource.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

27
demo/private_key.pem Normal file
View File

@ -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-----

124
demo/server.py Normal file
View File

@ -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'))

3
setup.cfg Normal file
View File

@ -0,0 +1,3 @@
[metadata]
description-file=README.md
license_files=LICENSE.rst

21
setup.py Normal file
View File

@ -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"
]
)

30
src/PyserSSH/__init__.py Normal file
View File

@ -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

152
src/PyserSSH/account.py Normal file
View File

@ -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

View File

@ -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()

124
src/PyserSSH/interactive.py Normal file
View File

@ -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

303
src/PyserSSH/server.py Normal file
View File

@ -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.")

199
src/PyserSSH/system/SFTP.py Normal file
View File

@ -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 = '<error>'
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 = '<error>'
return symlink

View File

@ -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"
)

View File

@ -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"])

View File

@ -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()])

View File

@ -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

View File

@ -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

12
upload.bat Normal file
View File

@ -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