mirror of
https://github.com/damp11113/PyserSSH.git
synced 2025-04-27 06:28:12 +00:00
new update 4.0
This commit is contained in:
parent
350796c441
commit
8742212952
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
dist/
|
||||||
|
src/PyserSSH.egg-info/
|
BIN
demo/opensource.png
Normal file
BIN
demo/opensource.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 78 KiB |
27
demo/private_key.pem
Normal file
27
demo/private_key.pem
Normal 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
124
demo/server.py
Normal 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
3
setup.cfg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[metadata]
|
||||||
|
description-file=README.md
|
||||||
|
license_files=LICENSE.rst
|
21
setup.py
Normal file
21
setup.py
Normal 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
30
src/PyserSSH/__init__.py
Normal 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
152
src/PyserSSH/account.py
Normal 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
|
303
src/PyserSSH/extensions/processbar.py
Normal file
303
src/PyserSSH/extensions/processbar.py
Normal 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
124
src/PyserSSH/interactive.py
Normal 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
303
src/PyserSSH/server.py
Normal 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
199
src/PyserSSH/system/SFTP.py
Normal 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
|
34
src/PyserSSH/system/info.py
Normal file
34
src/PyserSSH/system/info.py
Normal 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"
|
||||||
|
)
|
157
src/PyserSSH/system/inputsystem.py
Normal file
157
src/PyserSSH/system/inputsystem.py
Normal 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"])
|
89
src/PyserSSH/system/interface.py
Normal file
89
src/PyserSSH/system/interface.py
Normal 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()])
|
64
src/PyserSSH/system/syscom.py
Normal file
64
src/PyserSSH/system/syscom.py
Normal 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
|
31
src/PyserSSH/system/sysfunc.py
Normal file
31
src/PyserSSH/system/sysfunc.py
Normal 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
12
upload.bat
Normal 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
|
Loading…
x
Reference in New Issue
Block a user