mirror of
https://github.com/damp11113/PyserSSH.git
synced 2025-04-27 22:48:11 +00:00
Compare commits
17 Commits
Author | SHA1 | Date | |
---|---|---|---|
b9e4bb5887 | |||
3a751972c1 | |||
a3f1b50303 | |||
64bb2b0616 | |||
78a6459d26 | |||
1b15383ca7 | |||
f5cb7ead49 | |||
7eda693f78 | |||
019836cb00 | |||
0d6c58b71a | |||
31276827c4 | |||
b6455ce6a3 | |||
f578fc57d5 | |||
c4b5314fda | |||
98361c33f2 | |||
058daf9f04 | |||
f33be3bca1 |
42
README.md
42
README.md
@ -1,50 +1,60 @@
|
||||
# What is PyserSSH
|
||||
|
||||
PyserSSH is a library for remote control your code with ssh client. The aim is to provide a scriptable SSH server which can be made to behave like any SSH-enabled device.
|
||||
This library will be **Pyserminal** (Python Server Terminal) as it supports multiple protocols such as ssh telnet rlogin and mores...
|
||||
|
||||
PyserSSH is a free and open-source Python library designed to facilitate the creation of customizable SSH terminal servers. Initially developed for research purposes to address the lack of suitable SSH server libraries in Python, PyserSSH provides a flexible and user-friendly solution for implementing SSH servers, making it easier for developers to handle user interactions and command processing.
|
||||
|
||||
The project was started by a solo developer to create a more accessible and flexible tool for managing SSH connections and commands. It offers a simplified API compared to other libraries, such as Paramiko, SSHim, and Twisted, which are either outdated or complex for new users.
|
||||
|
||||
This project is part from [damp11113-library](https://github.com/damp11113/damp11113-library)
|
||||
|
||||
This Server use port **2222** for default port
|
||||
## Some smail PyserSSH history
|
||||
PyserSSH version [1.0](https://github.com/DPSoftware-Foundation/PyserSSH/releases/download/Legacy/PyserSSH10.py) (real filename is "test277.py") was created in 2023/9/3 for experimental purposes only. Because I couldn't find the best ssh server library for python and I started this project only for research. But I have time to develop this research into a real library for use. In software or server.
|
||||
|
||||
Read full history from [docs](https://damp11113.xyz/PyserSSHDocs/history.html)
|
||||
|
||||
# Install
|
||||
Install from pypi
|
||||
```bash
|
||||
pip install PyserSSH
|
||||
```
|
||||
Install from github
|
||||
Install with [openRemoDesk](https://github.com/DPSoftware-Foundation/openRemoDesk) protocol
|
||||
```bash
|
||||
pip install PyserSSH[RemoDesk]
|
||||
```
|
||||
Install from Github
|
||||
```bash
|
||||
pip install git+https://github.com/damp11113/PyserSSH.git
|
||||
```
|
||||
Install from DPCloudev Git
|
||||
```bash
|
||||
pip install git+https://git.damp11113.xyz/DPSoftware-Foundation/PyserSSH.git
|
||||
```
|
||||
|
||||
# Quick Example
|
||||
This Server use port **2222** for default port
|
||||
```py
|
||||
import os
|
||||
|
||||
from PyserSSH import Server, Send, AccountManager
|
||||
|
||||
useraccount = AccountManager()
|
||||
useraccount.add_account("admin", "") # create user without password
|
||||
from PyserSSH import Server, AccountManager
|
||||
|
||||
useraccount = AccountManager(allow_guest=True)
|
||||
ssh = Server(useraccount)
|
||||
|
||||
@ssh.on_user("command")
|
||||
def command(client, command: str):
|
||||
if command == "hello":
|
||||
Send(client, "world!")
|
||||
client.send("world!")
|
||||
|
||||
ssh.run(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'private_key.pem'))
|
||||
ssh.run("your private key file")
|
||||
```
|
||||
This example you can connect with `ssh admin@localhost -p 2222` and press enter on login
|
||||
If you input `hello` the response is `world`
|
||||
|
||||
# Demo
|
||||
https://github.com/damp11113/PyserSSH/assets/64675096/49bef3e2-3b15-4b64-b88e-3ca84a955de7
|
||||
> [!WARNING]
|
||||
> For use in product please **generate new private key**! If you still use this demo private key maybe your product getting **hacked**! up to 90%. Please don't use this demo private key for real product.
|
||||
|
||||
See [server.py](https://github.com/damp11113/PyserSSH/blob/main/demo/server.py)
|
||||
https://github.com/damp11113/PyserSSH/assets/64675096/49bef3e2-3b15-4b64-b88e-3ca84a955de7
|
||||
|
||||
I intend to leaked private key because that key i generated new. I recommend to generate new key if you want to use on your host because that key is for demo only.
|
||||
why i talk about this? because when i push private key into this repo in next 5 min++ i getting new email from GitGuardian. in that email say "
|
||||
GitGuardian has detected the following RSA Private Key exposed within your GitHub account" i dont knows what is GitGuardian and i not install this app into my account.
|
||||
|
||||
|
||||
|
||||
|
157
demo/Save Your Tears lyrics.srt
Normal file
157
demo/Save Your Tears lyrics.srt
Normal file
@ -0,0 +1,157 @@
|
||||
0
|
||||
00:00:00,000 --> 00:00:09,200
|
||||
(Intro)
|
||||
|
||||
1
|
||||
00:00:09,200 --> 00:00:13,100
|
||||
I saw you dancing in a crowded room
|
||||
|
||||
2
|
||||
00:00:13,200 --> 00:00:17,200
|
||||
You look so happy when
|
||||
I'm not with you
|
||||
|
||||
3
|
||||
00:00:17,300 --> 00:00:21,300
|
||||
But then you saw me, caught
|
||||
you by surprise
|
||||
|
||||
4
|
||||
00:00:21,400 --> 00:00:26,200
|
||||
A single teardrop falling
|
||||
from your eye
|
||||
|
||||
5
|
||||
00:00:26,300 --> 00:00:31,400
|
||||
I don't know why I run away
|
||||
|
||||
6
|
||||
00:00:34,400 --> 00:00:40,900
|
||||
I'll make you cry when I run away
|
||||
|
||||
7
|
||||
00:00:41,700 --> 00:00:45,950
|
||||
You could've asked me why
|
||||
I broke your heart
|
||||
|
||||
8
|
||||
00:00:46,000 --> 00:00:49,800
|
||||
You could've told me
|
||||
that you fell apart
|
||||
|
||||
9
|
||||
00:00:49,900 --> 00:00:53,700
|
||||
But you walked past me
|
||||
like I wasn't there
|
||||
|
||||
10
|
||||
00:00:53,800 --> 00:00:58,450
|
||||
And just pretended like
|
||||
you didn't care
|
||||
|
||||
11
|
||||
00:00:58,500 --> 00:01:03,000
|
||||
I don't know why I run away
|
||||
|
||||
12
|
||||
00:01:06,400 --> 00:01:11,300
|
||||
I'll make you cry when I run away
|
||||
|
||||
13
|
||||
00:01:14,700 --> 00:01:18,600
|
||||
Take me back 'cause I wanna stay
|
||||
|
||||
14
|
||||
00:01:18,700 --> 00:01:21,600
|
||||
Save your tears for another
|
||||
|
||||
15
|
||||
00:01:21,700 --> 00:01:28,900
|
||||
Save your tears for another day
|
||||
|
||||
16
|
||||
00:01:29,800 --> 00:01:34,700
|
||||
Save your tears for another day
|
||||
|
||||
17
|
||||
00:01:37,000 --> 00:01:42,900
|
||||
So, I made you think that
|
||||
I would always stay
|
||||
|
||||
18
|
||||
00:01:43,000 --> 00:01:46,600
|
||||
I said some things that
|
||||
I should never say
|
||||
|
||||
19
|
||||
00:01:46,700 --> 00:01:50,600
|
||||
Yeah, I broke your heart like
|
||||
someone did to mine
|
||||
|
||||
20
|
||||
00:01:50,700 --> 00:01:55,500
|
||||
And now you won't love
|
||||
me for a second time
|
||||
|
||||
21
|
||||
00:01:55,600 --> 00:02:00,400
|
||||
I don't know why I run away, oh, girl
|
||||
|
||||
22
|
||||
00:02:03,300 --> 00:02:08,800
|
||||
Said I make you cry when I run away
|
||||
|
||||
23
|
||||
00:02:11,500 --> 00:02:15,600
|
||||
Girl, take me back 'cause I wanna stay
|
||||
|
||||
24
|
||||
00:02:15,700 --> 00:02:19,200
|
||||
Save your tears for another
|
||||
|
||||
25
|
||||
00:02:19,300 --> 00:02:23,600
|
||||
I realize that I'm much too late
|
||||
|
||||
26
|
||||
00:02:23,700 --> 00:02:26,800
|
||||
And you deserve someone better
|
||||
|
||||
27
|
||||
00:02:26,900 --> 00:02:32,000
|
||||
Save your tears for another
|
||||
day (Ooh, yeah)
|
||||
|
||||
28
|
||||
00:02:34,900 --> 00:02:40,900
|
||||
Save your tears for another day (Yeah)
|
||||
|
||||
29
|
||||
00:02:46,100 --> 00:02:52,300
|
||||
I don't know why I run away
|
||||
|
||||
30
|
||||
00:02:52,700 --> 00:02:57,400
|
||||
I'll make you cry when I run away
|
||||
|
||||
31
|
||||
00:02:59,400 --> 00:03:06,650
|
||||
Save your tears for another
|
||||
day, ooh, girl (Ah)
|
||||
|
||||
32
|
||||
00:03:06,700 --> 00:03:13,600
|
||||
I said save your tears
|
||||
for another day (Ah)
|
||||
|
||||
33
|
||||
00:03:15,700 --> 00:03:23,200
|
||||
Save your tears for another day (Ah)
|
||||
|
||||
34
|
||||
00:03:23,700 --> 00:03:33,300
|
||||
Save your tears for another day (Ah)
|
||||
|
||||
35
|
||||
00:03:34,300 --> 00:03:43,300
|
||||
by DPSoftware Foundation
|
477
demo/demo1.py
Normal file
477
demo/demo1.py
Normal file
@ -0,0 +1,477 @@
|
||||
import os
|
||||
os.environ["damp11113_load_all_module"] = "NO"
|
||||
|
||||
import socket
|
||||
import time
|
||||
import cv2
|
||||
import traceback
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
import numpy as np
|
||||
import logging
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(format='[{asctime}] [{levelname}] {name}: {message}', datefmt='%Y-%m-%d %H:%M:%S', style='{', level=logging.DEBUG)
|
||||
|
||||
from PyserSSH import Server, AccountManager
|
||||
from PyserSSH.interactive import Send, wait_input, wait_inputkey, wait_choose, Clear, wait_inputmouse
|
||||
from PyserSSH.system.info import version, Flag_TH
|
||||
from PyserSSH.extensions.processbar import indeterminateStatus, LoadingProgress
|
||||
from PyserSSH.extensions.dialog import MenuDialog, TextDialog, TextInputDialog
|
||||
from PyserSSH.extensions.moredisplay import clickable_url, Send_karaoke_effect
|
||||
from PyserSSH.extensions.moreinteractive import ShowCursor
|
||||
from PyserSSH.extensions.remodesk import RemoDesk
|
||||
from PyserSSH.extensions.XHandler import XHandler
|
||||
from PyserSSH.system.clientype import Client
|
||||
from PyserSSH.system.RemoteStatus import remotestatus
|
||||
from PyserSSH.utils.ServerManager import ServerManager
|
||||
|
||||
class TextFormatter:
|
||||
RESET = "\033[0m"
|
||||
TEXT_COLORS = {
|
||||
"black": "\033[30m",
|
||||
"red": "\033[31m",
|
||||
"green": "\033[32m",
|
||||
"yellow": "\033[33m",
|
||||
"blue": "\033[34m",
|
||||
"magenta": "\033[35m",
|
||||
"cyan": "\033[36m",
|
||||
"white": "\033[37m"
|
||||
}
|
||||
TEXT_COLOR_LEVELS = {
|
||||
"light": "\033[1;{}m", # Light color prefix
|
||||
"dark": "\033[2;{}m" # Dark color prefix
|
||||
}
|
||||
BACKGROUND_COLORS = {
|
||||
"black": "\033[40m",
|
||||
"red": "\033[41m",
|
||||
"green": "\033[42m",
|
||||
"yellow": "\033[43m",
|
||||
"blue": "\033[44m",
|
||||
"magenta": "\033[45m",
|
||||
"cyan": "\033[46m",
|
||||
"white": "\033[47m"
|
||||
}
|
||||
TEXT_ATTRIBUTES = {
|
||||
"bold": "\033[1m",
|
||||
"italic": "\033[3m",
|
||||
"underline": "\033[4m",
|
||||
"blink": "\033[5m",
|
||||
"reverse": "\033[7m",
|
||||
"strikethrough": "\033[9m"
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def format_text_truecolor(text, color=None, background=None, attributes=None, target_text=''):
|
||||
formatted_text = ""
|
||||
start_index = text.find(target_text)
|
||||
end_index = start_index + len(target_text) if start_index != -1 else len(text)
|
||||
|
||||
if color:
|
||||
formatted_text += f"\033[38;2;{color}m"
|
||||
|
||||
if background:
|
||||
formatted_text += f"\033[48;2;{background}m"
|
||||
|
||||
if attributes in TextFormatter.TEXT_ATTRIBUTES:
|
||||
formatted_text += TextFormatter.TEXT_ATTRIBUTES[attributes]
|
||||
|
||||
if target_text == "":
|
||||
formatted_text += text + TextFormatter.RESET
|
||||
else:
|
||||
formatted_text += text[:start_index] + text[start_index:end_index] + TextFormatter.RESET + text[end_index:]
|
||||
|
||||
return formatted_text
|
||||
|
||||
useraccount = AccountManager(allow_guest=True, autoload=True, autosave=True)
|
||||
|
||||
if not os.path.isfile("autosave_session.ses"):
|
||||
useraccount.add_account("admin", "", sudo=True) # create user without password
|
||||
useraccount.add_account("test", "test") # create user without password
|
||||
useraccount.add_account("demo")
|
||||
useraccount.add_account("remote", "12345", permissions=["remote_desktop"])
|
||||
useraccount.set_user_enable_inputsystem_echo("remote", False)
|
||||
useraccount.set_user_sftp_allow("admin", True)
|
||||
|
||||
XH = XHandler()
|
||||
ssh = Server(useraccount,
|
||||
system_commands=True,
|
||||
system_message=False,
|
||||
sftp=True,
|
||||
enable_preauth_banner=True,
|
||||
XHandler=XH)
|
||||
|
||||
remotedesktopserver = RemoDesk()
|
||||
|
||||
servername = "PyserSSH"
|
||||
|
||||
loading = ["PyserSSH", "openRemoDesk", "XHandler", "RemoteStatus"]
|
||||
|
||||
@ssh.on_user("pre-shell")
|
||||
def guestauth(client):
|
||||
if client.get_name() == "remote":
|
||||
return
|
||||
|
||||
if not useraccount.has_user(client.get_name()):
|
||||
while True:
|
||||
Clear(client)
|
||||
Send(client, f"You are currently logged in as a guest. To access, please login or register.\nYour current account: {client.get_name()}\n")
|
||||
method = wait_choose(client, ["Login", "Register", "Exit"], prompt="Action: ")
|
||||
Clear(client)
|
||||
if method == 0: # login
|
||||
username = wait_input(client, "Username: ", noabort=True)
|
||||
|
||||
if not username:
|
||||
Send(client, "Please Enter username")
|
||||
wait_inputkey(client, "Press any key to continue...")
|
||||
continue
|
||||
|
||||
password = wait_input(client, "Password: ", password=True, noabort=True)
|
||||
|
||||
Send(client, "Please wait...")
|
||||
if not useraccount.has_user(username):
|
||||
Send(client, f"Username isn't exist. Please try again")
|
||||
wait_inputkey(client, "Press any key to continue...")
|
||||
continue
|
||||
|
||||
if not useraccount.validate_credentials(username, password):
|
||||
Send(client, f"Password incorrect. Please try again")
|
||||
wait_inputkey(client, "Press any key to continue...")
|
||||
continue
|
||||
|
||||
Clear(client)
|
||||
client.switch_user(username)
|
||||
break
|
||||
elif method == 1: # register
|
||||
username = wait_input(client, "Please choose a username: ", noabort=True)
|
||||
if not username:
|
||||
Send(client, "Please Enter username")
|
||||
wait_inputkey(client, "Press any key to continue...")
|
||||
continue
|
||||
|
||||
if useraccount.has_user(username):
|
||||
Send(client, f"Username is exist. Please try again")
|
||||
wait_inputkey(client, "Press any key to continue...")
|
||||
continue
|
||||
|
||||
password = wait_input(client, "Password: ", password=True, noabort=True)
|
||||
|
||||
if not password:
|
||||
Send(client, "Please Enter password")
|
||||
wait_inputkey(client, "Press any key to continue...")
|
||||
continue
|
||||
|
||||
confirmpassword = wait_input(client, "Confirm Password: ", password=True, noabort=True)
|
||||
|
||||
if not password:
|
||||
Send(client, "Please Enter confirm password")
|
||||
wait_inputkey(client, "Press any key to continue...")
|
||||
continue
|
||||
|
||||
if password != confirmpassword:
|
||||
Send(client, "Password do not matching the confirm password. Please try again.")
|
||||
wait_inputkey(client, "Press any key to continue...")
|
||||
continue
|
||||
|
||||
Send(client, "Please wait...")
|
||||
useraccount.add_account(username, password, ["user"])
|
||||
client.switch_user(username)
|
||||
Clear(client)
|
||||
break
|
||||
else:
|
||||
client.close()
|
||||
|
||||
@ssh.on_user("connect")
|
||||
def connect(client):
|
||||
if client.get_name() == "remote":
|
||||
return
|
||||
|
||||
client.set_prompt(client["current_user"] + "@" + servername + ":~$")
|
||||
|
||||
wm = f"""{Flag_TH()}{'–'*50}
|
||||
Hello {client['current_user']},
|
||||
|
||||
This is testing server of PyserSSH v{version}.
|
||||
|
||||
Visit: {clickable_url("https://damp11113.xyz", "DPCloudev")}
|
||||
{'–'*50}"""
|
||||
|
||||
for i in loading:
|
||||
P = indeterminateStatus(client, f"Starting {i}", f"[ OK ] Started {i}")
|
||||
P.start()
|
||||
|
||||
time.sleep(len(i) / 20)
|
||||
|
||||
P.stop()
|
||||
|
||||
Di1 = TextDialog(client, "Welcome!\n to PyserSSH test server", "PyserSSH Extension")
|
||||
Di1.render()
|
||||
|
||||
for char in wm:
|
||||
Send(client, char, ln=False)
|
||||
#time.sleep(0.005) # Adjust the delay as needed
|
||||
Send(client, '\n') # Send newline after each line
|
||||
|
||||
@ssh.on_user("authbanner")
|
||||
def banner(tmp):
|
||||
return "Hello World!\n", "en"
|
||||
|
||||
@ssh.on_user("error")
|
||||
def error(client, error):
|
||||
if isinstance(error, socket.error):
|
||||
pass
|
||||
else:
|
||||
Send(client, traceback.format_exc())
|
||||
|
||||
@XH.command(name="startremotedesktop", category="Remote", permissions=["remote_desktop"])
|
||||
def remotedesktop(client):
|
||||
remotedesktopserver.handle_new_client(client)
|
||||
|
||||
@XH.command(name="passtest", category="Test Function")
|
||||
def xh_passtest(client):
|
||||
user = wait_input(client, "username: ")
|
||||
password = wait_input(client, "password: ", password=True)
|
||||
Send(client, f"username: {user} | password: {password}")
|
||||
|
||||
@XH.command(name="colortest", category="Test Function")
|
||||
def xh_colortest(client):
|
||||
for i in range(0, 255, 5):
|
||||
Send(client, TextFormatter.format_text_truecolor(" ", background=f"{i};0;0"), ln=False)
|
||||
Send(client, "")
|
||||
for i in range(0, 255, 5):
|
||||
Send(client, TextFormatter.format_text_truecolor(" ", background=f"0;{i};0"), ln=False)
|
||||
Send(client, "")
|
||||
for i in range(0, 255, 5):
|
||||
Send(client, TextFormatter.format_text_truecolor(" ", background=f"0;0;{i}"), ln=False)
|
||||
Send(client, "")
|
||||
Send(client, "TrueColors 24-Bit")
|
||||
|
||||
@XH.command(name="keytest", category="Test Function")
|
||||
def xh_keytest(client: Client):
|
||||
user = wait_inputkey(client, "press any key", raw=True, timeout=1)
|
||||
Send(client, "")
|
||||
Send(client, f"key: {user}")
|
||||
for i in range(10):
|
||||
user = wait_inputkey(client, "press any key", raw=True, timeout=1)
|
||||
Send(client, "")
|
||||
Send(client, f"key: {user}")
|
||||
|
||||
@XH.command(name="typing")
|
||||
def xh_typing(client: Client, messages, speed = 1):
|
||||
for w in messages:
|
||||
Send(client, w, ln=False)
|
||||
time.sleep(float(speed))
|
||||
Send(client, "")
|
||||
|
||||
@XH.command(name="renimtest")
|
||||
def xh_renimtest(client: Client):
|
||||
Clear(client)
|
||||
image = cv2.imread("opensource.png", cv2.IMREAD_COLOR)
|
||||
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
||||
|
||||
width, height = client['windowsize']["width"] - 5, client['windowsize']["height"] - 5
|
||||
|
||||
# resize image
|
||||
resized = cv2.resize(image, (width, height))
|
||||
t = ""
|
||||
|
||||
# Scan all pixels
|
||||
for y in range(0, height):
|
||||
for x in range(0, width):
|
||||
pixel_color = resized[y, x]
|
||||
if pixel_color.tolist() != [0, 0, 0]:
|
||||
t += TextFormatter.format_text_truecolor(" ",
|
||||
background=f"{pixel_color[0]};{pixel_color[1]};{pixel_color[2]}")
|
||||
else:
|
||||
t += " "
|
||||
|
||||
Send(client, t, ln=False)
|
||||
Send(client, "")
|
||||
t = ""
|
||||
|
||||
@XH.command(name="errortest", category="Test Function")
|
||||
def xh_errortest(client: Client):
|
||||
raise Exception("hello error")
|
||||
|
||||
@XH.command(name="inloadtest", category="Test Function")
|
||||
def xh_inloadtest(client: Client):
|
||||
loading = indeterminateStatus(client)
|
||||
loading.start()
|
||||
time.sleep(5)
|
||||
loading.stop()
|
||||
|
||||
@XH.command(name="loadtest", category="Test Function")
|
||||
def xh_loadtest(client: Client):
|
||||
l = LoadingProgress(client, total=100, color=True)
|
||||
l.start()
|
||||
for i in range(101):
|
||||
l.current = i
|
||||
l.status = f"loading {i}"
|
||||
time.sleep(0.05)
|
||||
l.stop()
|
||||
|
||||
@XH.command(name="dialogtest", category="Test Function")
|
||||
def xh_dialogtest(client: Client):
|
||||
Di1 = TextDialog(client, "Hello Dialog!", "PyserSSH Extension")
|
||||
Di1.render()
|
||||
|
||||
@XH.command(name="dialogtest2", category="Test Function")
|
||||
def xh_dialogtest2(client: Client):
|
||||
Di2 = MenuDialog(client, ["H1", "H2", "H3"], "PyserSSH Extension", "Hello world")
|
||||
Di2.render()
|
||||
Send(client, f"selected index: {Di2.output()}")
|
||||
|
||||
@XH.command(name="dialogtest3", category="Test Function")
|
||||
def xh_dialogtest3(client: Client):
|
||||
Di3 = TextInputDialog(client, "PyserSSH Extension")
|
||||
Di3.render()
|
||||
Send(client, f"input: {Di3.output()}")
|
||||
|
||||
@XH.command(name="passdialogtest3", category="Test Function")
|
||||
def xh_passdialogtest3(client: Client):
|
||||
Di3 = TextInputDialog(client, "PyserSSH Extension", inputtitle="Password Here", password=True)
|
||||
Di3.render()
|
||||
Send(client, f"password: {Di3.output()}")
|
||||
|
||||
@XH.command(name="choosetest", category="Test Function")
|
||||
def xh_choosetest(client: Client):
|
||||
mylist = ["H1", "H2", "H3"]
|
||||
cindex = wait_choose(client, mylist, "select: ")
|
||||
Send(client, f"selected: {mylist[cindex]}")
|
||||
|
||||
@XH.command(name="vieweb")
|
||||
def xh_vieweb(client: Client, url: str):
|
||||
loading = indeterminateStatus(client, desc=f"requesting {url}...")
|
||||
loading.start()
|
||||
try:
|
||||
content = requests.get(url).content
|
||||
except:
|
||||
loading.stopfail()
|
||||
return
|
||||
loading.stop()
|
||||
loading = indeterminateStatus(client, desc=f"parsing html {url}...")
|
||||
loading.start()
|
||||
try:
|
||||
soup = BeautifulSoup(content, 'html.parser')
|
||||
# Extract only the text content
|
||||
text_content = soup.get_text()
|
||||
except:
|
||||
loading.stopfail()
|
||||
return
|
||||
loading.stop()
|
||||
Di1 = TextDialog(client, text_content, url)
|
||||
Di1.render()
|
||||
|
||||
@XH.command(name="shutdown")
|
||||
def xh_shutdown(client: Client, at: str):
|
||||
if at == "now":
|
||||
ssh.stop_server()
|
||||
|
||||
@XH.command(name="mouseinput", category="Test Function")
|
||||
def xh_mouseinput(client: Client):
|
||||
for i in range(10):
|
||||
button, x, y = wait_inputmouse(client)
|
||||
if button == 0:
|
||||
Send(client, "Left Button")
|
||||
elif button == 1:
|
||||
Send(client, "Middle Button")
|
||||
elif button == 2:
|
||||
Send(client, "Right Button")
|
||||
elif button == 3:
|
||||
Send(client, "Button Up")
|
||||
|
||||
Send(client, f"Current POS: X {x} | Y {y} with button {button}")
|
||||
|
||||
@XH.command(name="karaoke")
|
||||
def xh_karaoke(client: Client):
|
||||
ShowCursor(client, False)
|
||||
Send_karaoke_effect(client, "Python can print like karaoke!")
|
||||
ShowCursor(client)
|
||||
|
||||
R1 = 1
|
||||
R2 = 2
|
||||
K2 = 5
|
||||
|
||||
theta_spacing = 0.07
|
||||
phi_spacing = 0.02
|
||||
|
||||
illumination = np.fromiter(".,-~:;=!*#$@", dtype="<U1")
|
||||
|
||||
def render_frame(A: float, B: float, screen_size) -> np.ndarray:
|
||||
K1 = screen_size * K2 * 3 / (8 * (R1 + R2))
|
||||
"""
|
||||
Returns a frame of the spinning 3D donut.
|
||||
Based on the pseudocode from: https://www.a1k0n.net/2011/07/20/donut-math.html
|
||||
"""
|
||||
cos_A = np.cos(A)
|
||||
sin_A = np.sin(A)
|
||||
cos_B = np.cos(B)
|
||||
sin_B = np.sin(B)
|
||||
|
||||
output = np.full((screen_size, screen_size), " ") # (40, 40)
|
||||
zbuffer = np.zeros((screen_size, screen_size)) # (40, 40)
|
||||
|
||||
cos_phi = np.cos(phi := np.arange(0, 2 * np.pi, phi_spacing)) # (315,)
|
||||
sin_phi = np.sin(phi) # (315,)
|
||||
cos_theta = np.cos(theta := np.arange(0, 2 * np.pi, theta_spacing)) # (90,)
|
||||
sin_theta = np.sin(theta) # (90,)
|
||||
circle_x = R2 + R1 * cos_theta # (90,)
|
||||
circle_y = R1 * sin_theta # (90,)
|
||||
|
||||
x = (np.outer(cos_B * cos_phi + sin_A * sin_B * sin_phi, circle_x) - circle_y * cos_A * sin_B).T # (90, 315)
|
||||
y = (np.outer(sin_B * cos_phi - sin_A * cos_B * sin_phi, circle_x) + circle_y * cos_A * cos_B).T # (90, 315)
|
||||
z = ((K2 + cos_A * np.outer(sin_phi, circle_x)) + circle_y * sin_A).T # (90, 315)
|
||||
ooz = np.reciprocal(z) # Calculates 1/z
|
||||
xp = (screen_size / 2 + K1 * ooz * x).astype(int) # (90, 315)
|
||||
yp = (screen_size / 2 - K1 * ooz * y).astype(int) # (90, 315)
|
||||
L1 = (((np.outer(cos_phi, cos_theta) * sin_B) - cos_A * np.outer(sin_phi, cos_theta)) - sin_A * sin_theta) # (315, 90)
|
||||
L2 = cos_B * (cos_A * sin_theta - np.outer(sin_phi, cos_theta * sin_A)) # (315, 90)
|
||||
L = np.around(((L1 + L2) * 8)).astype(int).T # (90, 315)
|
||||
mask_L = L >= 0 # (90, 315)
|
||||
chars = illumination[L] # (90, 315)
|
||||
|
||||
for i in range(90):
|
||||
mask = mask_L[i] & (ooz[i] > zbuffer[xp[i], yp[i]]) # (315,)
|
||||
|
||||
zbuffer[xp[i], yp[i]] = np.where(mask, ooz[i], zbuffer[xp[i], yp[i]])
|
||||
output[xp[i], yp[i]] = np.where(mask, chars[i], output[xp[i], yp[i]])
|
||||
|
||||
return output
|
||||
|
||||
@XH.command()
|
||||
def donut(client, screen_size=40):
|
||||
screen_size = int(screen_size)
|
||||
|
||||
A = 1
|
||||
B = 1
|
||||
|
||||
for _ in range(screen_size * screen_size):
|
||||
A += theta_spacing
|
||||
B += phi_spacing
|
||||
Clear(client)
|
||||
array = render_frame(A, B, screen_size)
|
||||
for row in array:
|
||||
Send(client, " ".join(row))
|
||||
Send(client, "\n")
|
||||
|
||||
if wait_inputkey(client, raw=True, timeout=0.01) == "\x03": break
|
||||
|
||||
|
||||
@XH.command(name="status")
|
||||
def xh_status(client: Client):
|
||||
remotestatus(ssh, client.channel, True)
|
||||
|
||||
#@ssh.on_user("command")
|
||||
#def command(client: Client, command: str):
|
||||
|
||||
#ssh.run(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'private_key.pem'))
|
||||
|
||||
manager = ServerManager()
|
||||
|
||||
# Add servers to the manager
|
||||
manager.add_server("ssh", ssh, os.path.join(os.path.dirname(os.path.realpath(__file__)), 'private_key.pem'))
|
||||
manager.add_server("telnet", ssh, "", protocol="telnet")
|
||||
|
||||
# Start a specific server
|
||||
manager.start_server("ssh")
|
||||
manager.start_server("telnet")
|
29
demo/demo2.py
Normal file
29
demo/demo2.py
Normal file
@ -0,0 +1,29 @@
|
||||
import os
|
||||
import time
|
||||
from damp11113 import SRTParser
|
||||
|
||||
from PyserSSH import Server, Send, AccountManager
|
||||
from PyserSSH.extensions.XHandler import XHandler
|
||||
from PyserSSH.extensions.moredisplay import Send_karaoke_effect, ShowCursor
|
||||
|
||||
accountmanager = AccountManager()
|
||||
accountmanager.add_account("admin", "")
|
||||
|
||||
XH = XHandler()
|
||||
server = Server(accountmanager, XHandler=XH)
|
||||
|
||||
@XH.command()
|
||||
def karaoke(client):
|
||||
ShowCursor(client, False)
|
||||
subtitle = SRTParser("Save Your Tears lyrics.srt", removeln=True)
|
||||
|
||||
for sub in subtitle:
|
||||
delay = sub["duration"] / len(sub["text"])
|
||||
Send_karaoke_effect(client, sub["text"], delay)
|
||||
|
||||
if sub["next_text_duration"] is not None:
|
||||
time.sleep(sub["next_text_duration"])
|
||||
|
||||
ShowCursor(client)
|
||||
|
||||
server.run(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'private_key.pem'))
|
@ -1,27 +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=
|
||||
MIIEpQIBAAKCAQEAwNfkia91HNrpyqlHwjYrVKDV5SkDt5P27MxKZDjwOokGBX7E
|
||||
g5cMXb1wxQeCm+zptg680qIXHfSaaOi1E/DAutaTIQa3GI+gDMphlWMxrEWFuZOB
|
||||
ylvTuFAxLB8xKcuBjelQX4TYlcgA1WgyeI6LFPNdJPekVHnzkLCZnW+y05PkT6a0
|
||||
QY1Eoa6DY2TtY8w4NZmnyCy1ZPYV5qLKN/P7aVSU52AD8u25St1WprvxpM4TtZiG
|
||||
2O9X1Unx+wtco2P8G1M4qcuWqPDdITn4n19DcR7rhuACjUo2poFTlnl9lfEsW11R
|
||||
5sDfYlgc3n8a4Iw49Ea4GaLkSEMluOfB9eOLUQIDAQABAoIBAAeZmpVTN7uFjyLg
|
||||
YrEZ6cGXPcbJw9k8zhr3/tM4q+hf/7+WBuWkEtCGR5xO7Ev73XFs3u6IL9QLkKL4
|
||||
z4YefgypqeO/0YB4zJckdLhqpTRZCxOiEhfpCuI1MDLrycgQD/uJSenHIQgKI/+a
|
||||
cH7Ffgq7Kp0V22vu4HVVLcCsJxvlIxFd92xCKFl8zRHBdyKikfvZAEidbMu9xdsW
|
||||
S9DzFCveCGrE8g6HWQyXiCpq2xb4b2C37O+0iZRtYfJQSCrnG99Y/KfWIVbb+3gU
|
||||
5WbIlYm57TKzMGgKc3LWtGCWxfB/NNP5wOxR+4y78oWDzTibrT5OZDsX2S+mbgNB
|
||||
wAo/0U8CgYEAxHAOrlz9Ae2kYfyUgx9JTonElIFlDmDVdYcRW8Go7xpeMZ+XG2sR
|
||||
f/za6t6jiCxI9FSD5gl4nDyOVhx5zRpu2QZvZBHICaWDwEmZC+d3suYtQY/ixR3K
|
||||
3sdDKK6wzOtta+OBVNPQWAW2rmTr/J1JobguflM0NBm+YZC02gQyL9sCgYEA+1DU
|
||||
llDGDaU08WQNTLRgW+1RAbzsBTFd+DhvbYM8+mgmlFzHKHJP3jCpwLZmqdBzLl0R
|
||||
wUZBwpZ5MnkiQV0e9AW4/tnqBw8n9pf+NgNqcssw8MEMXHPbLNwr7OVS/LG8VNOm
|
||||
LbuLjxq8O8wfbS87eBj2D18c1x4voEIw1AWYn0MCgYEAnPBF2moyPMMmjJ5l7Ggn
|
||||
ghaxNlA2c4lLoOz7IkqTdAul65FsARzGS3GxWOnsztNKqeGHy1YPxQrgUM3JReLz
|
||||
YnIwtks6fPJ+Uza5jngr+oLI71NMQl1uAhRChJMkb2M79XE6l5HuJxTRgXzhyN3E
|
||||
wO5MPuKsl19l6b7ZrkCh8/cCgYEAjIL6+TgcI9D0suo/zV0kawFaw1//jj+1zGyx
|
||||
UEeKNm848saUy3ZuVUpb/tV8vQFBBPEgVjGT3toG1UOI9Ya9Ia55anQoNt4wd90v
|
||||
Ur/CKoCU0mb9JEvahVBsdr0ZExPEuqDDTtqHAvHtwHk2MPOxikpaeOmy1EuaUT3w
|
||||
0vp2BMUCgYEAsLL592l8pclhxk2b0lmgvhPLOmZuQ7QkcnMMyYCeUr9Kt95VN40J
|
||||
N/LK9LIbf/l9CUN4eO1JqCJkAiMIW2Gvumw3g+TMj+nqcfsufSHJCG1EZNYMUftG
|
||||
aL7KtccPyFwotMD/P+OaAeJimwuC5247hCep1SSf1A41gbdmutiirM4=
|
||||
-----END RSA PRIVATE KEY-----
|
||||
|
197
demo/server.py
197
demo/server.py
@ -1,197 +0,0 @@
|
||||
import os
|
||||
import socket
|
||||
import time
|
||||
import shlex
|
||||
from damp11113 import TextFormatter, sort_files, allfiles
|
||||
import cv2
|
||||
import traceback
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from PyserSSH import Server, AccountManager, Send, wait_input, wait_inputkey, wait_choose, Clear, Title
|
||||
from PyserSSH.system.info import system_banner
|
||||
from PyserSSH.extensions.processbar import indeterminateStatus, LoadingProgress
|
||||
from PyserSSH.extensions.dialog import MenuDialog, TextDialog, TextInputDialog
|
||||
from PyserSSH.extensions.moredisplay import clickable_url
|
||||
|
||||
useraccount = AccountManager()
|
||||
useraccount.add_account("admin", "") # create user without password
|
||||
useraccount.add_account("test", "test") # 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."""
|
||||
|
||||
loading = ["PyserSSH", "Extensions"]
|
||||
|
||||
@ssh.on_user("connect")
|
||||
def connect(client):
|
||||
Title(client, "PyserSSH")
|
||||
#print(client["windowsize"])
|
||||
if client['current_user'] == "":
|
||||
warningmessage = nonamewarning
|
||||
else:
|
||||
warningmessage = Authorizedmessage
|
||||
|
||||
wm = f"""*********************************************************************************************
|
||||
Hello {client['current_user']},
|
||||
|
||||
{warningmessage}
|
||||
|
||||
Visit: {clickable_url("https://damp11113.xyz", "DPCloudev")}
|
||||
|
||||
{system_banner}
|
||||
*********************************************************************************************"""
|
||||
|
||||
if client['current_user'] != "test":
|
||||
for i in loading:
|
||||
P = indeterminateStatus(client, f"Starting {i}", f"[ OK ] Started {i}")
|
||||
P.start()
|
||||
|
||||
time.sleep(len(i) / 20)
|
||||
|
||||
P.stop()
|
||||
|
||||
Di1 = TextDialog(client, "PyserSSH Extension", "Welcome!\n to PyserSSH test server")
|
||||
Di1.render()
|
||||
|
||||
for char in wm:
|
||||
Send(client, char, ln=False)
|
||||
# time.sleep(0.005) # Adjust the delay as needed
|
||||
Send(client, '\n') # Send newline after each line
|
||||
|
||||
@ssh.on_user("error")
|
||||
def error(client, error):
|
||||
if isinstance(error, socket.error):
|
||||
pass
|
||||
else:
|
||||
Send(client, traceback.format_exc())
|
||||
|
||||
|
||||
#@ssh.on_user("onrawtype")
|
||||
#def onrawtype(client, key):
|
||||
# print(key)
|
||||
|
||||
@ssh.on_user("command")
|
||||
def command(client, command: str):
|
||||
if command == "passtest":
|
||||
user = wait_input(client, "username: ")
|
||||
password = wait_input(client, "password: ", password=True)
|
||||
Send(client, f"username: {user} | password: {password}")
|
||||
elif command == "colortest":
|
||||
for i in range(0, 255, 5):
|
||||
Send(client, TextFormatter.format_text_truecolor(" ", background=f"{i};0;0"), ln=False)
|
||||
Send(client, "")
|
||||
for i in range(0, 255, 5):
|
||||
Send(client, TextFormatter.format_text_truecolor(" ", background=f"0;{i};0"), ln=False)
|
||||
Send(client, "")
|
||||
for i in range(0, 255, 5):
|
||||
Send(client, TextFormatter.format_text_truecolor(" ", background=f"0;0;{i}"), ln=False)
|
||||
Send(client, "")
|
||||
Send(client, "TrueColors 24-Bit")
|
||||
elif command == "keytest":
|
||||
user = wait_inputkey(client, "press any key", raw=True)
|
||||
Send(client, "")
|
||||
Send(client, f"key: {user}")
|
||||
for i in range(10):
|
||||
user = wait_inputkey(client, "press any key", raw=True)
|
||||
Send(client, "")
|
||||
Send(client, f"key: {user}")
|
||||
elif command.startswith("typing"):
|
||||
args = shlex.split(command)
|
||||
messages = args[1]
|
||||
speed = float(args[2])
|
||||
for w in messages:
|
||||
Send(client, w, ln=False)
|
||||
time.sleep(speed)
|
||||
Send(client, "")
|
||||
elif command.startswith("renimtest"):
|
||||
args = shlex.split(command)
|
||||
Clear(client)
|
||||
image = cv2.imread(f"opensource.png", cv2.IMREAD_COLOR)
|
||||
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
||||
|
||||
width, height = client['windowsize']["width"]-5, client['windowsize']["height"]-5
|
||||
|
||||
# resize image
|
||||
resized = cv2.resize(image, (width, height))
|
||||
t = ""
|
||||
|
||||
# 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]:
|
||||
t += TextFormatter.format_text_truecolor(" ", background=f"{pixel_color[0]};{pixel_color[1]};{pixel_color[2]}")
|
||||
else:
|
||||
t += " "
|
||||
|
||||
Send(client, t, ln=False)
|
||||
Send(client, "")
|
||||
t = ""
|
||||
|
||||
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.status = f"loading {i}"
|
||||
time.sleep(0.05)
|
||||
l.stop()
|
||||
elif command == "dialogtest":
|
||||
Di1 = TextDialog(client, "PyserSSH Extension", "Hello Dialog!")
|
||||
Di1.render()
|
||||
elif command == "dialogtest2":
|
||||
Di2 = MenuDialog(client, ["H1", "H2", "H3"], "PyserSSH Extension", "Hello world")
|
||||
Di2.render()
|
||||
Send(client, f"selected index: {Di2.output()}")
|
||||
elif command == "dialogtest3":
|
||||
Di3 = TextInputDialog(client, "PyserSSH Extension")
|
||||
Di3.render()
|
||||
Send(client, f"input: {Di3.output()}")
|
||||
elif command == "passdialogtest3":
|
||||
Di3 = TextInputDialog(client, "PyserSSH Extension", inputtitle="Password Here", password=True)
|
||||
Di3.render()
|
||||
Send(client, f"password: {Di3.output()}")
|
||||
elif command == "choosetest":
|
||||
cindex = wait_choose(client, ["H1", "H2", "H3"], "select: ")
|
||||
Send(client, f"selected index: {cindex}")
|
||||
elif command.startswith("vieweb"):
|
||||
args = shlex.split(command)
|
||||
url = args[1]
|
||||
loading = indeterminateStatus(client, desc=f"requesting {url}...")
|
||||
loading.start()
|
||||
try:
|
||||
content = requests.get(url).content
|
||||
except:
|
||||
loading.stopfail()
|
||||
return
|
||||
loading.stop()
|
||||
loading = indeterminateStatus(client, desc=f"parsing html {url}...")
|
||||
loading.start()
|
||||
try:
|
||||
soup = BeautifulSoup(content, 'html.parser')
|
||||
# Extract only the text content
|
||||
text_content = soup.get_text()
|
||||
except:
|
||||
loading.stopfail()
|
||||
return
|
||||
loading.stop()
|
||||
Di1 = TextDialog(client, url, text_content)
|
||||
Di1.render()
|
||||
|
||||
|
||||
ssh.run(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'private_key.pem'))
|
41
setup.py
41
setup.py
@ -1,21 +1,44 @@
|
||||
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.3',
|
||||
version='5.1.4',
|
||||
license='MIT',
|
||||
author='damp11113',
|
||||
author_email='damp51252@gmail.com',
|
||||
author='DPSoftware Foundation',
|
||||
author_email='contact@damp11113.xyz',
|
||||
packages=find_packages('src'),
|
||||
package_dir={'': 'src'},
|
||||
url='https://github.com/damp11113/PyserSSH',
|
||||
description="python scriptable ssh server library. based on Paramiko",
|
||||
long_description=long_description,
|
||||
long_description=open('README.md', 'r', encoding='utf-8').read(),
|
||||
long_description_content_type='text/markdown',
|
||||
keywords="SSH server",
|
||||
python_requires='>=3.6',
|
||||
classifiers=[
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Intended Audience :: Developers",
|
||||
"Intended Audience :: System Administrators",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Topic :: Communications",
|
||||
"Topic :: Internet",
|
||||
"Topic :: Internet :: File Transfer Protocol (FTP)",
|
||||
"Topic :: Software Development",
|
||||
"Topic :: Terminals"
|
||||
],
|
||||
install_requires=[
|
||||
"paramiko"
|
||||
]
|
||||
"paramiko",
|
||||
"psutil"
|
||||
],
|
||||
extras_require={
|
||||
'RemoDesk': [
|
||||
"mouse",
|
||||
"keyboard",
|
||||
"Brotli",
|
||||
"pillow",
|
||||
"numpy",
|
||||
"opencv-python"
|
||||
],
|
||||
}
|
||||
)
|
@ -1,8 +1,8 @@
|
||||
"""
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/damp11113/PyserSSH
|
||||
Copyright (C) 2023-2024 damp11113 (MIT)
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
Copyright (C) 2023-present DPSoftware Foundation (MIT)
|
||||
|
||||
Visit https://github.com/damp11113/PyserSSH
|
||||
Visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
|
||||
MIT License
|
||||
|
||||
@ -42,27 +42,35 @@ import logging
|
||||
from .interactive import *
|
||||
from .server import Server
|
||||
from .account import AccountManager
|
||||
from .system.info import system_banner
|
||||
from .system.info import system_banner, version
|
||||
|
||||
if os.name == 'nt':
|
||||
import ctypes
|
||||
kernel32 = ctypes.windll.kernel32
|
||||
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
|
||||
|
||||
try:
|
||||
os.environ["pyserssh_systemmessage"]
|
||||
except:
|
||||
os.environ["pyserssh_systemmessage"] = "YES"
|
||||
|
||||
try:
|
||||
os.environ["pyserssh_enable_damp11113"]
|
||||
except:
|
||||
os.environ["pyserssh_enable_damp11113"] = "YES"
|
||||
|
||||
try:
|
||||
os.environ["pyserssh_log"]
|
||||
except:
|
||||
os.environ["pyserssh_log"] = "NO"
|
||||
|
||||
if os.environ["pyserssh_log"] == "NO":
|
||||
logging.basicConfig(level=logging.CRITICAL)
|
||||
logger = logging.getLogger("PyserSSH")
|
||||
logger.disabled = True
|
||||
#logger.disabled = False
|
||||
|
||||
if os.environ["pyserssh_systemmessage"] == "YES":
|
||||
print(system_banner)
|
||||
|
||||
__author__ = "damp11113"
|
||||
__url__ = "https://github.com/DPSoftware-Foundation/PyserSSH"
|
||||
__copyright__ = "2023-present"
|
||||
__license__ = "MIT"
|
||||
__version__ = version
|
||||
__department__ = "DPSoftware"
|
||||
__organization__ = "DOPFoundation"
|
@ -1,8 +1,8 @@
|
||||
"""
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/damp11113/PyserSSH
|
||||
Copyright (C) 2023-2024 damp11113 (MIT)
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
Copyright (C) 2023-present DPSoftware Foundation (MIT)
|
||||
|
||||
Visit https://github.com/damp11113/PyserSSH
|
||||
Visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
|
||||
MIT License
|
||||
|
||||
@ -24,179 +24,309 @@ 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 logging
|
||||
import os
|
||||
import pickle
|
||||
import time
|
||||
import atexit
|
||||
import threading
|
||||
import hashlib
|
||||
|
||||
logger = logging.getLogger("PyserSSH.Account")
|
||||
|
||||
class AccountManager:
|
||||
def __init__(self, anyuser=False, historylimit=10, autosave=False, autosavedelay=60, autoload=False, autoloadfile="autosave_session.ses"):
|
||||
def __init__(self, allow_guest=False, historylimit=10, autosave=False, autosavedelay=60, autoload=False, autofile="autosave_session.ses"):
|
||||
self.accounts = {}
|
||||
self.anyuser = anyuser
|
||||
self.allow_guest = allow_guest
|
||||
self.historylimit = historylimit
|
||||
self.autosavedelay = autosavedelay
|
||||
|
||||
self.__autosavework = False
|
||||
self.autosave = autosave
|
||||
self.__autosaveworknexttime = 0
|
||||
|
||||
if self.anyuser:
|
||||
print("history system can't work if 'anyuser' is enable")
|
||||
self.__autofile = autofile
|
||||
|
||||
if autoload:
|
||||
self.load(autoloadfile)
|
||||
self.load(self.__autofile)
|
||||
|
||||
if autosave:
|
||||
self.__autosavethread = threading.Thread(target=self.__autosave)
|
||||
if self.autosave:
|
||||
logger.info("starting autosave")
|
||||
self.__autosavethread = threading.Thread(target=self.__autosave, daemon=True)
|
||||
self.__autosavethread.start()
|
||||
atexit.register(self.__saveexit)
|
||||
|
||||
def __autosave(self):
|
||||
self.save("autosave_session.ses")
|
||||
self.save(self.__autofile)
|
||||
self.__autosaveworknexttime = time.time() + self.autosavedelay
|
||||
self.__autosavework = True
|
||||
while self.__autosavework:
|
||||
if int(self.__autosaveworknexttime) == int(time.time()):
|
||||
self.save("autosave_session.ses")
|
||||
self.save(self.__autofile)
|
||||
self.__autosaveworknexttime = time.time() + self.autosavedelay
|
||||
|
||||
time.sleep(1) # fix cpu load
|
||||
|
||||
def __auto_save(func):
|
||||
def wrapper(self, *args, **kwargs):
|
||||
result = func(self, *args, **kwargs)
|
||||
if self.autosave:
|
||||
self.save(self.__autofile, False)
|
||||
return result
|
||||
return wrapper
|
||||
|
||||
def __saveexit(self):
|
||||
self.__autosavework = False
|
||||
self.save("autosave_session.ses")
|
||||
self.save(self.__autofile)
|
||||
self.__autosavethread.join()
|
||||
|
||||
def validate_credentials(self, username, password):
|
||||
if username in self.accounts and self.accounts[username]["password"] == password or self.anyuser:
|
||||
def validate_credentials(self, username, password=None, public_key=None):
|
||||
if self.allow_guest and not self.has_user(username):
|
||||
return True
|
||||
|
||||
allowed_auth_list = str(self.accounts[username].get("allowed_auth", "")).split(",")
|
||||
|
||||
# Check password authentication
|
||||
if password is not None and "password" in allowed_auth_list:
|
||||
stored_password = self.accounts[username].get("password", "")
|
||||
return stored_password == hashlib.md5(password.encode()).hexdigest()
|
||||
|
||||
# Check public key authentication
|
||||
if public_key is not None and "publickey" in allowed_auth_list:
|
||||
stored_public_key = self.accounts[username].get("public_key", "")
|
||||
return stored_public_key == public_key
|
||||
|
||||
# Check if 'none' authentication is allowed
|
||||
if "none" in allowed_auth_list:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def has_user(self, username):
|
||||
return username in self.accounts
|
||||
|
||||
def list_users(self):
|
||||
return list(self.accounts.keys())
|
||||
|
||||
def get_allowed_auths(self, username):
|
||||
if self.has_user(username) and "allowed_auth" in self.accounts[username]:
|
||||
return self.accounts[username]["allowed_auth"]
|
||||
return "none"
|
||||
|
||||
def get_permissions(self, username):
|
||||
if username in self.accounts:
|
||||
if self.has_user(username):
|
||||
return self.accounts[username]["permissions"]
|
||||
return []
|
||||
|
||||
@__auto_save
|
||||
def set_prompt(self, username, prompt=">"):
|
||||
if username in self.accounts:
|
||||
if self.has_user(username):
|
||||
self.accounts[username]["prompt"] = prompt
|
||||
|
||||
def get_prompt(self, username):
|
||||
if username in self.accounts and "prompt" in self.accounts[username]:
|
||||
if self.has_user(username) 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}
|
||||
@__auto_save
|
||||
def add_account(self, username, password=None, public_key=None, permissions:list=None, sudo=False):
|
||||
if not self.has_user(username):
|
||||
allowedlist = []
|
||||
accountkey = {}
|
||||
|
||||
if permissions is None:
|
||||
permissions = []
|
||||
|
||||
if password != None:
|
||||
allowedlist.append("password")
|
||||
accountkey["password"] = hashlib.md5(password.encode()).hexdigest()
|
||||
|
||||
if public_key != None:
|
||||
allowedlist.append("publickey")
|
||||
accountkey["public_key"] = public_key
|
||||
|
||||
if password is None and public_key is None:
|
||||
allowedlist.append("none")
|
||||
|
||||
accountkey["permissions"] = permissions
|
||||
accountkey["allowed_auth"] = ",".join(allowedlist)
|
||||
|
||||
self.accounts[username] = accountkey
|
||||
|
||||
if sudo:
|
||||
if self.has_sudo_user():
|
||||
raise Exception(f"sudo user is exist")
|
||||
|
||||
self.accounts[username]["sudo"] = sudo
|
||||
else:
|
||||
raise Exception(f"{username} is exist")
|
||||
|
||||
def has_sudo_user(self):
|
||||
return any(account.get("sudo", False) for account in self.accounts.values())
|
||||
|
||||
def is_user_has_sudo(self, username):
|
||||
if self.has_user(username) and "sudo" in self.accounts[username]:
|
||||
return self.accounts[username]["sudo"]
|
||||
return False
|
||||
|
||||
@__auto_save
|
||||
def remove_account(self, username):
|
||||
if self.has_user(username):
|
||||
del self.accounts[username]
|
||||
|
||||
@__auto_save
|
||||
def change_password(self, username, new_password):
|
||||
if username in self.accounts:
|
||||
if self.has_user(username):
|
||||
self.accounts[username]["password"] = new_password
|
||||
|
||||
@__auto_save
|
||||
def set_permissions(self, username, new_permissions):
|
||||
if username in self.accounts:
|
||||
if self.has_user(username):
|
||||
self.accounts[username]["permissions"] = new_permissions
|
||||
|
||||
def save(self, filename="session.ssh"):
|
||||
with open(filename, 'wb') as file:
|
||||
pickle.dump(self.accounts, file)
|
||||
def save(self, filename="session.ses", keep_log=True):
|
||||
if keep_log:
|
||||
logger.info(f"saving session to {filename}")
|
||||
try:
|
||||
with open(filename, 'wb') as file:
|
||||
pickle.dump(self.accounts, file)
|
||||
|
||||
if keep_log:
|
||||
logger.info(f"saved session to {filename}")
|
||||
except Exception as e:
|
||||
if keep_log:
|
||||
logger.error(f"save session failed: {e}")
|
||||
|
||||
def load(self, filename):
|
||||
logger.info(f"loading session from {filename}")
|
||||
try:
|
||||
with open(filename, 'rb') as file:
|
||||
self.accounts = pickle.load(file)
|
||||
logger.info(f"loaded session")
|
||||
except FileNotFoundError:
|
||||
print("File not found. No accounts loaded.")
|
||||
logger.error("can't load session: file not found.")
|
||||
except Exception as e:
|
||||
print(f"An error occurred: {e}. No accounts loaded.")
|
||||
logger.error(f"can't load session: {e}")
|
||||
|
||||
@__auto_save
|
||||
def set_user_sftp_allow(self, username, allow=True):
|
||||
if username in self.accounts:
|
||||
if self.has_user(username):
|
||||
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
|
||||
if self.has_user(username) and "sftp_allow" in self.accounts[username]:
|
||||
return self.accounts[username]["sftp_allow"]
|
||||
return True
|
||||
return False
|
||||
|
||||
@__auto_save
|
||||
def set_user_sftp_readonly(self, username, readonly=False):
|
||||
if username in self.accounts:
|
||||
if self.has_user(username):
|
||||
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]:
|
||||
if self.has_user(username) 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:
|
||||
@__auto_save
|
||||
def set_user_sftp_root_path(self, username, path="/"):
|
||||
if self.has_user(username):
|
||||
if path == "/":
|
||||
self.accounts[username]["sftp_path"] = ""
|
||||
self.accounts[username]["sftp_root_path"] = os.getcwd()
|
||||
else:
|
||||
self.accounts[username]["sftp_path"] = path
|
||||
self.accounts[username]["sftp_root_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 get_user_sftp_root_path(self, username):
|
||||
if self.has_user(username) and "sftp_root_path" in self.accounts[username]:
|
||||
return self.accounts[username]["sftp_root_path"]
|
||||
return os.getcwd()
|
||||
|
||||
@__auto_save
|
||||
def set_user_enable_inputsystem(self, username, enable=True):
|
||||
if self.has_user(username):
|
||||
self.accounts[username]["inputsystem"] = enable
|
||||
|
||||
def get_user_enable_inputsystem(self, username):
|
||||
if self.has_user(username) and "inputsystem" in self.accounts[username]:
|
||||
return self.accounts[username]["inputsystem"]
|
||||
return True
|
||||
|
||||
@__auto_save
|
||||
def set_user_enable_inputsystem_echo(self, username, echo=True):
|
||||
if self.has_user(username):
|
||||
self.accounts[username]["inputsystem_echo"] = echo
|
||||
|
||||
def get_user_enable_inputsystem_echo(self, username):
|
||||
if self.has_user(username) and "inputsystem_echo" in self.accounts[username]:
|
||||
return self.accounts[username]["inputsystem_echo"]
|
||||
return True
|
||||
|
||||
@__auto_save
|
||||
def set_banner(self, username, banner):
|
||||
if self.has_user(username):
|
||||
self.accounts[username]["banner"] = banner
|
||||
|
||||
def get_banner(self, username):
|
||||
if self.has_user(username) and "banner" in self.accounts[username]:
|
||||
return self.accounts[username]["banner"]
|
||||
return None
|
||||
|
||||
def get_user_timeout(self, username):
|
||||
if username in self.accounts and "timeout" in self.accounts[username]:
|
||||
if self.has_user(username) and "timeout" in self.accounts[username]:
|
||||
return self.accounts[username]["timeout"]
|
||||
return None
|
||||
|
||||
@__auto_save
|
||||
def set_user_timeout(self, username, timeout=None):
|
||||
if username in self.accounts:
|
||||
if self.has_user(username):
|
||||
self.accounts[username]["timeout"] = timeout
|
||||
|
||||
def get_user_last_login(self, username):
|
||||
if username in self.accounts and "lastlogin" in self.accounts[username]:
|
||||
if self.has_user(username) and "lastlogin" in self.accounts[username]:
|
||||
return self.accounts[username]["lastlogin"]
|
||||
return None
|
||||
|
||||
@__auto_save
|
||||
def set_user_last_login(self, username, ip, timelogin=time.time()):
|
||||
if username in self.accounts:
|
||||
if self.has_user(username):
|
||||
self.accounts[username]["lastlogin"] = {
|
||||
"ip": ip,
|
||||
"time": timelogin
|
||||
}
|
||||
|
||||
@__auto_save
|
||||
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:
|
||||
if self.has_user(username):
|
||||
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:]
|
||||
|
||||
@__auto_save
|
||||
def clear_history(self, username):
|
||||
if self.has_user(username):
|
||||
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
|
||||
if self.has_user(username) 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:
|
||||
if index < len(history):
|
||||
return history[index]
|
||||
else:
|
||||
return None # Index out of range
|
||||
return None # User or history not found
|
||||
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
|
||||
if self.has_user(username) and "lastcommand" in self.accounts[username]:
|
||||
command = self.accounts[username]["lastcommand"]
|
||||
return command
|
||||
return None # User or history not found
|
||||
|
@ -1,8 +1,8 @@
|
||||
"""
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/damp11113/PyserSSH
|
||||
Copyright (C) 2023-2024 damp11113 (MIT)
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
Copyright (C) 2023-present DPSoftware Foundation (MIT)
|
||||
|
||||
Visit https://github.com/damp11113/PyserSSH
|
||||
Visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
|
||||
MIT License
|
||||
|
||||
@ -30,16 +30,38 @@ import shlex
|
||||
|
||||
from ..interactive import Send
|
||||
|
||||
def are_permissions_met(permission_list, permission_require):
|
||||
return set(permission_require).issubset(set(permission_list))
|
||||
|
||||
class XHandler:
|
||||
def __init__(self, enablehelp=True, showusageonworng=True):
|
||||
"""
|
||||
Initializes the command handler with optional settings for help messages and usage.
|
||||
|
||||
Parameters:
|
||||
enablehelp (bool): Whether help messages are enabled.
|
||||
showusageonworng (bool): Whether usage information is shown on wrong usage.
|
||||
"""
|
||||
self.handlers = {}
|
||||
self.categories = {}
|
||||
self.enablehelp = enablehelp
|
||||
self.showusageonworng = showusageonworng
|
||||
|
||||
self.serverself = None
|
||||
self.commandnotfound = None
|
||||
|
||||
def command(self, category=None, name=None, aliases=None):
|
||||
def command(self, category=None, name=None, aliases=None, permissions: list = None):
|
||||
"""
|
||||
Decorator to register a function as a command with optional category, name, aliases, and permissions.
|
||||
|
||||
Parameters:
|
||||
category (str): The category under which the command falls (default: None).
|
||||
name (str): The name of the command (default: None).
|
||||
aliases (list): A list of command aliases (default: None).
|
||||
permissions (list): A list of permissions required to execute the command (default: None).
|
||||
|
||||
Returns:
|
||||
function: The wrapped function.
|
||||
"""
|
||||
def decorator(func):
|
||||
nonlocal name, category
|
||||
if name is None:
|
||||
@ -48,21 +70,32 @@ class XHandler:
|
||||
command_description = func.__doc__ # Read the docstring
|
||||
parameters = inspect.signature(func).parameters
|
||||
command_args = []
|
||||
has_args = False
|
||||
has_kwargs = False
|
||||
|
||||
for param in list(parameters.values())[1:]: # Exclude first parameter (client)
|
||||
if param.default != inspect.Parameter.empty: # Check if parameter has default value
|
||||
if param.kind == inspect.Parameter.VAR_POSITIONAL:
|
||||
has_args = True
|
||||
elif param.kind == inspect.Parameter.VAR_KEYWORD:
|
||||
has_kwargs = True
|
||||
elif param.default != inspect.Parameter.empty: # Check if parameter has default value
|
||||
if param.annotation == bool:
|
||||
command_args.append(f"-{param.name}")
|
||||
command_args.append(f"--{param.name}")
|
||||
else:
|
||||
command_args.append((f"{param.name}", param.default))
|
||||
else:
|
||||
command_args.append(param.name)
|
||||
|
||||
if category is None:
|
||||
category = 'No Category'
|
||||
if category not in self.categories:
|
||||
self.categories[category] = {}
|
||||
self.categories[category][command_name] = {
|
||||
'description': command_description.strip() if command_description else "",
|
||||
'args': command_args
|
||||
'args': command_args,
|
||||
"permissions": permissions,
|
||||
'has_args': has_args,
|
||||
'has_kwargs': has_kwargs
|
||||
}
|
||||
self.handlers[command_name] = func
|
||||
if aliases:
|
||||
@ -73,9 +106,20 @@ class XHandler:
|
||||
return decorator
|
||||
|
||||
def call(self, client, command_string):
|
||||
"""
|
||||
Processes a command string, validates arguments, and calls the corresponding function.
|
||||
|
||||
Parameters:
|
||||
client (object): The client sending the command.
|
||||
command_string (str): The command string to be executed.
|
||||
|
||||
Returns:
|
||||
Any: The result of the command function, or an error message if invalid.
|
||||
"""
|
||||
tokens = shlex.split(command_string)
|
||||
command_name = tokens[0]
|
||||
args = tokens[1:]
|
||||
|
||||
if command_name == "help" and self.enablehelp:
|
||||
if args:
|
||||
Send(client, self.get_help_command_info(args[0]))
|
||||
@ -85,58 +129,68 @@ class XHandler:
|
||||
else:
|
||||
if command_name in self.handlers:
|
||||
command_func = self.handlers[command_name]
|
||||
command_info = self.get_command_info(command_name)
|
||||
if command_info and command_info.get('permissions'):
|
||||
if not are_permissions_met(self.serverself.accounts.get_permissions(client.get_name()), command_info.get('permissions')) or not self.serverself.accounts.is_user_has_sudo(client.get_name()):
|
||||
Send(client, f"Permission denied. You do not have permission to execute '{command_name}'.")
|
||||
return
|
||||
|
||||
command_args = inspect.signature(command_func).parameters
|
||||
if len(args) % 2 != 0 and not args[0].startswith("--"):
|
||||
if self.showusageonworng:
|
||||
Send(client, self.get_help_command_info(command_name))
|
||||
else:
|
||||
Send(client, f"Invalid number of arguments for command '{command_name}'.")
|
||||
return
|
||||
# Parse arguments
|
||||
final_args = {}
|
||||
for i in range(0, len(args), 2):
|
||||
if args[i].startswith("--"):
|
||||
arg_name = args[i].lstrip('--')
|
||||
final_kwargs = {}
|
||||
i = 0
|
||||
|
||||
while i < len(args):
|
||||
arg = args[i]
|
||||
if arg.startswith('-'):
|
||||
arg_name = arg.lstrip('-')
|
||||
if arg_name not in command_args:
|
||||
if self.showusageonworng:
|
||||
Send(client, self.get_help_command_info(command_name))
|
||||
else:
|
||||
Send(client, f"Invalid flag '{arg_name}' for command '{command_name}'.")
|
||||
Send(client, f"Invalid flag '{arg_name}' for command '{command_name}'.")
|
||||
return
|
||||
try:
|
||||
args[i + 1]
|
||||
except:
|
||||
pass
|
||||
if command_args[arg_name].annotation == bool:
|
||||
final_args[arg_name] = True
|
||||
i += 1
|
||||
else:
|
||||
if self.showusageonworng:
|
||||
Send(client, self.get_help_command_info(command_name))
|
||||
if i + 1 < len(args):
|
||||
final_args[arg_name] = args[i + 1]
|
||||
i += 2
|
||||
else:
|
||||
Send(client, f"value '{args[i + 1]}' not available for '{arg_name}' flag for command '{command_name}'.")
|
||||
return
|
||||
final_args[arg_name] = True
|
||||
if self.showusageonworng:
|
||||
Send(client, self.get_help_command_info(command_name))
|
||||
Send(client, f"Missing value for flag '{arg_name}' for command '{command_name}'.")
|
||||
return
|
||||
else:
|
||||
arg_name = args[i].lstrip('-')
|
||||
if arg_name not in command_args:
|
||||
if self.showusageonworng:
|
||||
Send(client, self.get_help_command_info(command_name))
|
||||
if command_info['has_args']:
|
||||
final_args.setdefault('args', []).append(arg)
|
||||
elif command_info['has_kwargs']:
|
||||
final_kwargs[arg] = args[i + 1] if i + 1 < len(args) else None
|
||||
i += 1
|
||||
else:
|
||||
if len(final_args) + 1 < len(command_args):
|
||||
param = list(command_args.values())[len(final_args) + 1]
|
||||
final_args[param.name] = arg
|
||||
else:
|
||||
Send(client, f"Invalid argument '{arg_name}' for command '{command_name}'.")
|
||||
return
|
||||
arg_value = args[i + 1]
|
||||
final_args[arg_name] = arg_value
|
||||
# Match parsed arguments to function parameters
|
||||
final_args_list = []
|
||||
if self.showusageonworng:
|
||||
Send(client, self.get_help_command_info(command_name))
|
||||
Send(client, f"Unexpected argument '{arg}' for command '{command_name}'.")
|
||||
return
|
||||
i += 1
|
||||
|
||||
# Check for required positional arguments
|
||||
for param in list(command_args.values())[1:]: # Skip client argument
|
||||
if param.name in final_args:
|
||||
final_args_list.append(final_args[param.name])
|
||||
elif param.default != inspect.Parameter.empty:
|
||||
final_args_list.append(param.default)
|
||||
else:
|
||||
if param.name not in final_args and param.default == inspect.Parameter.empty:
|
||||
if self.showusageonworng:
|
||||
Send(client, self.get_help_command_info(command_name))
|
||||
else:
|
||||
Send(client, f"Missing required argument '{param.name}' for command '{command_name}'")
|
||||
Send(client, f"Missing required argument '{param.name}' for command '{command_name}'")
|
||||
return
|
||||
|
||||
final_args_list = [final_args.get(param.name, param.default) for param in list(command_args.values())[1:]]
|
||||
|
||||
if command_info['has_kwargs']:
|
||||
final_args_list.append(final_kwargs)
|
||||
|
||||
return command_func(client, *final_args_list)
|
||||
else:
|
||||
if self.commandnotfound:
|
||||
@ -147,6 +201,15 @@ class XHandler:
|
||||
return
|
||||
|
||||
def get_command_info(self, command_name):
|
||||
"""
|
||||
Retrieves information about a specific command, including its description, arguments, and permissions.
|
||||
|
||||
Parameters:
|
||||
command_name (str): The name of the command.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing command details such as name, description, args, and permissions.
|
||||
"""
|
||||
found_command = None
|
||||
for category, commands in self.categories.items():
|
||||
if command_name in commands:
|
||||
@ -165,10 +228,22 @@ class XHandler:
|
||||
'name': command_name,
|
||||
'description': found_command['description'].strip() if found_command['description'] else "",
|
||||
'args': found_command['args'],
|
||||
'category': category
|
||||
'category': category,
|
||||
'permissions': found_command['permissions'],
|
||||
'has_args': found_command['has_args'],
|
||||
'has_kwargs': found_command['has_kwargs']
|
||||
}
|
||||
|
||||
def get_help_command_info(self, command):
|
||||
"""
|
||||
Generates a detailed help message for a specific command.
|
||||
|
||||
Parameters:
|
||||
command (str): The name of the command.
|
||||
|
||||
Returns:
|
||||
str: The formatted help message for the command.
|
||||
"""
|
||||
command_info = self.get_command_info(command)
|
||||
aliases = command_info.get('aliases', [])
|
||||
help_message = f"{command_info['name']}"
|
||||
@ -185,9 +260,19 @@ class XHandler:
|
||||
help_message += f" [-{arg[0]} {arg[1]}]"
|
||||
else:
|
||||
help_message += f" <{arg}>"
|
||||
if command_info['has_args']:
|
||||
help_message += " [<args>...]"
|
||||
if command_info['has_kwargs']:
|
||||
help_message += " [--<key>=<value>...]"
|
||||
return help_message
|
||||
|
||||
def get_help_message(self):
|
||||
"""
|
||||
Generates a general help message listing all categories and their associated commands.
|
||||
|
||||
Returns:
|
||||
str: The formatted help message containing all commands and categories.
|
||||
"""
|
||||
help_message = ""
|
||||
for category, commands in self.categories.items():
|
||||
help_message += f"{category}:\n"
|
||||
@ -199,7 +284,15 @@ class XHandler:
|
||||
return help_message
|
||||
|
||||
def get_all_commands(self):
|
||||
"""
|
||||
Retrieves all registered commands, grouped by category.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary where each key is a category name and the value is a
|
||||
dictionary of commands within that category.
|
||||
"""
|
||||
all_commands = {}
|
||||
for category, commands in self.categories.items():
|
||||
all_commands[category] = commands
|
||||
return all_commands
|
||||
return all_commands
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
"""
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/damp11113/PyserSSH
|
||||
Copyright (C) 2023-2024 damp11113 (MIT)
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
Copyright (C) 2023-present DPSoftware Foundation (MIT)
|
||||
|
||||
Visit https://github.com/damp11113/PyserSSH
|
||||
Visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
|
||||
MIT License
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
"""
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/damp11113/PyserSSH
|
||||
Copyright (C) 2023-2024 damp11113 (MIT)
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
Copyright (C) 2023-present DPSoftware Foundation (MIT)
|
||||
|
||||
Visit https://github.com/damp11113/PyserSSH
|
||||
Visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
|
||||
MIT License
|
||||
|
||||
@ -30,8 +30,22 @@ import re
|
||||
from ..interactive import Clear, Send, wait_inputkey
|
||||
from ..system.sysfunc import text_centered_screen
|
||||
|
||||
|
||||
class TextDialog:
|
||||
def __init__(self, client, title="", content=""):
|
||||
"""
|
||||
A dialog that displays a simple text message with an optional title.
|
||||
|
||||
Args:
|
||||
client (Client): The client to display the dialog to.
|
||||
content (str, optional): The content to be displayed in the dialog. Defaults to an empty string.
|
||||
title (str, optional): The title of the dialog. Defaults to an empty string.
|
||||
|
||||
Methods:
|
||||
render(): Renders the dialog, displaying the title and content in the center of the screen.
|
||||
waituserenter(): Waits for the user to press the 'enter' key to close the dialog.
|
||||
"""
|
||||
|
||||
def __init__(self, client, content="", title=""):
|
||||
self.client = client
|
||||
|
||||
self.windowsize = client["windowsize"]
|
||||
@ -39,11 +53,15 @@ class TextDialog:
|
||||
self.content = content
|
||||
|
||||
def render(self):
|
||||
"""
|
||||
Renders the dialog by displaying the title, content, and waiting for the user's input.
|
||||
"""
|
||||
Clear(self.client)
|
||||
Send(self.client, self.title)
|
||||
Send(self.client, "-" * self.windowsize["width"])
|
||||
|
||||
generatedwindow = text_centered_screen(self.content, self.windowsize["width"], self.windowsize["height"]-3, " ")
|
||||
generatedwindow = text_centered_screen(self.content, self.windowsize["width"], self.windowsize["height"] - 3,
|
||||
" ")
|
||||
|
||||
Send(self.client, generatedwindow)
|
||||
|
||||
@ -52,13 +70,32 @@ class TextDialog:
|
||||
self.waituserenter()
|
||||
|
||||
def waituserenter(self):
|
||||
"""
|
||||
Waits for the user to press the 'enter' key to close the dialog.
|
||||
"""
|
||||
while True:
|
||||
if wait_inputkey(self.client, raw=True) == b'\r':
|
||||
Clear(self.client)
|
||||
break
|
||||
pass
|
||||
|
||||
|
||||
class MenuDialog:
|
||||
"""
|
||||
A menu dialog that allows the user to choose from a list of options, with navigation and selection using arrow keys.
|
||||
|
||||
Args:
|
||||
client (Client): The client to display the menu to.
|
||||
choose (list): A list of options to be displayed.
|
||||
title (str, optional): The title of the menu.
|
||||
desc (str, optional): A description to display above the menu options.
|
||||
|
||||
Methods:
|
||||
render(): Renders the menu dialog and waits for user input.
|
||||
_waituserinput(): Handles user input for selecting options or canceling.
|
||||
output(): Returns the selected option index or `None` if canceled.
|
||||
"""
|
||||
|
||||
def __init__(self, client, choose: list, title="", desc=""):
|
||||
self.client = client
|
||||
|
||||
@ -67,9 +104,12 @@ class MenuDialog:
|
||||
self.desc = desc
|
||||
self.contentallindex = len(choose) - 1
|
||||
self.selectedindex = 0
|
||||
self.selectstatus = 0 # 0 none 1 selected 2 cancel
|
||||
self.selectstatus = 0 # 0 none, 1 selected, 2 canceled
|
||||
|
||||
def render(self):
|
||||
"""
|
||||
Renders the menu dialog, displaying the options and allowing the user to navigate and select an option.
|
||||
"""
|
||||
tempcontentlist = self.choose.copy()
|
||||
|
||||
Clear(self.client)
|
||||
@ -90,7 +130,8 @@ class MenuDialog:
|
||||
f"{exported}"
|
||||
)
|
||||
|
||||
generatedwindow = text_centered_screen(contenttoshow, self.client["windowsize"]["width"], self.client["windowsize"]["height"]-3, " ")
|
||||
generatedwindow = text_centered_screen(contenttoshow, self.client["windowsize"]["width"],
|
||||
self.client["windowsize"]["height"] - 3, " ")
|
||||
|
||||
Send(self.client, generatedwindow)
|
||||
|
||||
@ -99,6 +140,9 @@ class MenuDialog:
|
||||
self._waituserinput()
|
||||
|
||||
def _waituserinput(self):
|
||||
"""
|
||||
Waits for user input and updates the selection based on key presses.
|
||||
"""
|
||||
keyinput = wait_inputkey(self.client, raw=True)
|
||||
|
||||
if keyinput == b'\r': # Enter key
|
||||
@ -124,12 +168,31 @@ class MenuDialog:
|
||||
self.render()
|
||||
|
||||
def output(self):
|
||||
"""
|
||||
Returns the selected option index or `None` if the action was canceled.
|
||||
"""
|
||||
if self.selectstatus == 2:
|
||||
return None
|
||||
elif self.selectstatus == 1:
|
||||
return self.selectedindex
|
||||
|
||||
|
||||
class TextInputDialog:
|
||||
"""
|
||||
A text input dialog that allows the user to input text with optional password masking.
|
||||
|
||||
Args:
|
||||
client (Client): The client to display the dialog to.
|
||||
title (str, optional): The title of the input dialog.
|
||||
inputtitle (str, optional): The prompt text for the user input.
|
||||
password (bool, optional): If `True`, the input will be masked as a password. Defaults to `False`.
|
||||
|
||||
Methods:
|
||||
render(): Renders the input dialog, displaying the prompt and capturing user input.
|
||||
_waituserinput(): Handles user input, including text input and special keys.
|
||||
output(): Returns the input text if selected, or `None` if canceled.
|
||||
"""
|
||||
|
||||
def __init__(self, client, title="", inputtitle="Input Here", password=False):
|
||||
self.client = client
|
||||
|
||||
@ -137,27 +200,31 @@ class TextInputDialog:
|
||||
self.inputtitle = inputtitle
|
||||
self.ispassword = password
|
||||
|
||||
self.inputstatus = 0 # 0 none 1 selected 2 cancel
|
||||
self.inputstatus = 0 # 0 none, 1 selected, 2 canceled
|
||||
self.buffer = bytearray()
|
||||
self.cursor_position = 0
|
||||
|
||||
def render(self):
|
||||
"""
|
||||
Renders the text input dialog and waits for user input.
|
||||
"""
|
||||
Clear(self.client)
|
||||
Send(self.client, self.title)
|
||||
Send(self.client, "-" * self.client["windowsize"]["width"])
|
||||
|
||||
if self.ispassword:
|
||||
texts = (
|
||||
f"{self.inputtitle}\n\n"
|
||||
"> " + ("*" * len(self.buffer.decode('utf-8')))
|
||||
f"{self.inputtitle}\n\n"
|
||||
"> " + ("*" * len(self.buffer.decode('utf-8')))
|
||||
)
|
||||
else:
|
||||
texts = (
|
||||
f"{self.inputtitle}\n\n"
|
||||
"> " + self.buffer.decode('utf-8')
|
||||
f"{self.inputtitle}\n\n"
|
||||
"> " + self.buffer.decode('utf-8')
|
||||
)
|
||||
|
||||
generatedwindow = text_centered_screen(texts, self.client["windowsize"]["width"], self.client["windowsize"]["height"]-3, " ")
|
||||
generatedwindow = text_centered_screen(texts, self.client["windowsize"]["width"],
|
||||
self.client["windowsize"]["height"] - 3, " ")
|
||||
|
||||
Send(self.client, generatedwindow)
|
||||
|
||||
@ -166,6 +233,9 @@ class TextInputDialog:
|
||||
self._waituserinput()
|
||||
|
||||
def _waituserinput(self):
|
||||
"""
|
||||
Waits for the user to input text or special commands (backspace, cancel, enter).
|
||||
"""
|
||||
keyinput = wait_inputkey(self.client, raw=True)
|
||||
|
||||
if keyinput == b'\r': # Enter key
|
||||
@ -182,7 +252,7 @@ class TextInputDialog:
|
||||
self.buffer = self.buffer[:self.cursor_position - 1] + self.buffer[self.cursor_position:]
|
||||
self.cursor_position -= 1
|
||||
elif bool(re.compile(b'\x1b\[[0-9;]*[mGK]').search(keyinput)):
|
||||
pass
|
||||
pass # Ignore ANSI escape codes
|
||||
else: # Regular character
|
||||
self.buffer = self.buffer[:self.cursor_position] + keyinput + self.buffer[self.cursor_position:]
|
||||
self.cursor_position += 1
|
||||
@ -197,6 +267,9 @@ class TextInputDialog:
|
||||
self.render()
|
||||
|
||||
def output(self):
|
||||
"""
|
||||
Returns the input text if the input was selected, or `None` if canceled.
|
||||
"""
|
||||
if self.inputstatus == 2:
|
||||
return None
|
||||
elif self.inputstatus == 1:
|
||||
|
@ -1,8 +1,8 @@
|
||||
"""
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/damp11113/PyserSSH
|
||||
Copyright (C) 2023-2024 damp11113 (MIT)
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
Copyright (C) 2023-present DPSoftware Foundation (MIT)
|
||||
|
||||
Visit https://github.com/damp11113/PyserSSH
|
||||
Visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
|
||||
MIT License
|
||||
|
||||
@ -24,68 +24,57 @@ 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
|
||||
|
||||
from ..interactive import Send
|
||||
|
||||
def clickable_url(url, link_text=""):
|
||||
"""
|
||||
Creates a clickable URL in a terminal client with optional link text.
|
||||
|
||||
Args:
|
||||
url (str): The URL to be linked.
|
||||
link_text (str, optional): The text to be displayed for the link. Defaults to an empty string, which will display the URL itself.
|
||||
|
||||
Returns:
|
||||
str: A terminal escape sequence that makes the URL clickable with the provided link text.
|
||||
"""
|
||||
return f"\033]8;;{url}\033\\{link_text}\033]8;;\033\\"
|
||||
|
||||
class BasicTextFormatter:
|
||||
RESET = "\033[0m"
|
||||
TEXT_COLORS = {
|
||||
"black": "\033[30m",
|
||||
"red": "\033[31m",
|
||||
"green": "\033[32m",
|
||||
"yellow": "\033[33m",
|
||||
"blue": "\033[34m",
|
||||
"magenta": "\033[35m",
|
||||
"cyan": "\033[36m",
|
||||
"white": "\033[37m"
|
||||
}
|
||||
TEXT_COLOR_LEVELS = {
|
||||
"light": "\033[1;{}m", # Light color prefix
|
||||
"dark": "\033[2;{}m" # Dark color prefix
|
||||
}
|
||||
BACKGROUND_COLORS = {
|
||||
"black": "\033[40m",
|
||||
"red": "\033[41m",
|
||||
"green": "\033[42m",
|
||||
"yellow": "\033[43m",
|
||||
"blue": "\033[44m",
|
||||
"magenta": "\033[45m",
|
||||
"cyan": "\033[46m",
|
||||
"white": "\033[47m"
|
||||
}
|
||||
TEXT_ATTRIBUTES = {
|
||||
"bold": "\033[1m",
|
||||
"italic": "\033[3m",
|
||||
"underline": "\033[4m",
|
||||
"blink": "\033[5m",
|
||||
"reverse": "\033[7m",
|
||||
"strikethrough": "\033[9m"
|
||||
}
|
||||
def Send_karaoke_effect(client, text, delay=0.1, ln=True):
|
||||
"""
|
||||
Sends a text with a 'karaoke' effect where the text is printed one character at a time,
|
||||
with the remaining text dimmed until it is printed.
|
||||
|
||||
@staticmethod
|
||||
def format_text(text, color=None, color_level=None, background=None, attributes=None, target_text=''):
|
||||
formatted_text = ""
|
||||
start_index = text.find(target_text)
|
||||
end_index = start_index + len(target_text) if start_index != -1 else len(text)
|
||||
Args:
|
||||
client (Client): The client to send the text to.
|
||||
text (str): The text to be printed with the karaoke effect.
|
||||
delay (float, optional): The delay in seconds between printing each character. Defaults to 0.1.
|
||||
ln (bool, optional): Whether to send a newline after the text is finished. Defaults to True.
|
||||
|
||||
if color in BasicTextFormatter.TEXT_COLORS:
|
||||
if color_level in BasicTextFormatter.TEXT_COLOR_LEVELS:
|
||||
color_code = BasicTextFormatter.TEXT_COLORS[color]
|
||||
color_format = BasicTextFormatter.TEXT_COLOR_LEVELS[color_level].format(color_code)
|
||||
formatted_text += color_format
|
||||
else:
|
||||
formatted_text += BasicTextFormatter.TEXT_COLORS[color]
|
||||
This function simulates a typing effect by printing the text character by character,
|
||||
while dimming the unprinted characters.
|
||||
"""
|
||||
printed_text = ""
|
||||
for i, char in enumerate(text):
|
||||
# Print the already printed text normally
|
||||
Send(client, printed_text + char, ln=False)
|
||||
|
||||
if background in BasicTextFormatter.BACKGROUND_COLORS:
|
||||
formatted_text += BasicTextFormatter.BACKGROUND_COLORS[background]
|
||||
# Calculate the unprinted text and dim it
|
||||
not_printed_text = text[i + 1:]
|
||||
dimmed_text = ''.join([f"\033[2m{char}\033[0m" for char in not_printed_text])
|
||||
|
||||
if attributes in BasicTextFormatter.TEXT_ATTRIBUTES:
|
||||
formatted_text += BasicTextFormatter.TEXT_ATTRIBUTES[attributes]
|
||||
# Print the dimmed text for the remaining characters
|
||||
Send(client, dimmed_text, ln=False)
|
||||
|
||||
if target_text == "":
|
||||
formatted_text += text + BasicTextFormatter.RESET
|
||||
else:
|
||||
formatted_text += text[:start_index] + text[start_index:end_index] + BasicTextFormatter.RESET + text[end_index:]
|
||||
# Wait before printing the next character
|
||||
time.sleep(delay)
|
||||
|
||||
return formatted_text
|
||||
# Clear the line to update the text in the next iteration
|
||||
Send(client, '\r', ln=False)
|
||||
|
||||
# Update the printed_text to include the current character
|
||||
printed_text += char
|
||||
|
||||
if ln:
|
||||
Send(client, "") # Send a newline after the entire text is printed
|
||||
|
50
src/PyserSSH/extensions/moreinteractive.py
Normal file
50
src/PyserSSH/extensions/moreinteractive.py
Normal file
@ -0,0 +1,50 @@
|
||||
"""
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
Copyright (C) 2023-present DPSoftware Foundation (MIT)
|
||||
|
||||
Visit https://github.com/DPSoftware-Foundation/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 Send
|
||||
|
||||
def ShowCursor(client, show=True):
|
||||
"""
|
||||
Shows or hides the cursor for a specific client.
|
||||
|
||||
Args:
|
||||
client (Client): The client to show/hide the cursor for.
|
||||
show (bool, optional): A flag to determine whether to show or hide the cursor. Defaults to True (show cursor).
|
||||
"""
|
||||
if show:
|
||||
Send(client, "\033[?25h", ln=False) # Show cursor
|
||||
else:
|
||||
Send(client, "\033[?25l", ln=False) # Hide cursor
|
||||
|
||||
def SendBell(client):
|
||||
"""
|
||||
Sends a bell character (alert) to a client.
|
||||
|
||||
Args:
|
||||
client (Client): The client to send the bell character to.
|
||||
"""
|
||||
Send(client, "\x07", ln=False) # Bell character (alert)
|
@ -1,8 +1,8 @@
|
||||
"""
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/damp11113/PyserSSH
|
||||
Copyright (C) 2023-2024 damp11113 (MIT)
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
Copyright (C) 2023-present DPSoftware Foundation (MIT)
|
||||
|
||||
Visit https://github.com/damp11113/PyserSSH
|
||||
Visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
|
||||
MIT License
|
||||
|
||||
@ -37,22 +37,119 @@ from ..system.sysfunc import replace_enter_with_crlf
|
||||
def Print(channel, string, start="", end="\n"):
|
||||
channel.send(replace_enter_with_crlf(start + string + end))
|
||||
|
||||
try:
|
||||
from damp11113.utils import get_size_unit2, center_string, TextFormatter, insert_string
|
||||
except:
|
||||
raise ModuleNotFoundError("This extension is require damp11113-library")
|
||||
def get_size_unit2(number, unitp, persec=True, unitsize=1024, decimal=True, space=" "):
|
||||
for unit in ['', 'K', 'M', 'G', 'T', 'P']:
|
||||
if number < unitsize:
|
||||
if decimal:
|
||||
num = f"{number:.2f}"
|
||||
else:
|
||||
num = int(number)
|
||||
|
||||
steps1 = ['[ ]', '[- ]', '[-- ]', '[---]', '[ --]', '[ -]']
|
||||
steps2 = ['[ ]', '[- ]', '[ - ]', '[ -]']
|
||||
steps3 = ['[ ]', '[- ]', '[-- ]', '[ --]', '[ -]', '[ ]', '[ -]', '[ --]', '[-- ]', '[- ]']
|
||||
steps4 = ['[ ]', '[- ]', '[ - ]', '[ -]', '[ ]', '[ -]', '[ - ]', '[- ]', '[ ]']
|
||||
steps5 = ['[ ]', '[ -]', '[ --]', '[---]', '[-- ]', '[- ]']
|
||||
steps6 = ['[ ]', '[ -]', '[ - ]', '[- ]']
|
||||
if persec:
|
||||
return f"{num}{space}{unit}{unitp}/s"
|
||||
else:
|
||||
return f"{num}{space}{unit}{unitp}"
|
||||
number /= unitsize
|
||||
|
||||
def center_string(main_string, replacement_string):
|
||||
# Find the center index of the main string
|
||||
center_index = len(main_string) // 2
|
||||
|
||||
# Calculate the start and end indices for replacing
|
||||
start_index = center_index - len(replacement_string) // 2
|
||||
end_index = start_index + len(replacement_string)
|
||||
|
||||
# Replace the substring at the center
|
||||
new_string = main_string[:start_index] + replacement_string + main_string[end_index:]
|
||||
|
||||
return new_string
|
||||
|
||||
class TextFormatter:
|
||||
RESET = "\033[0m"
|
||||
TEXT_COLORS = {
|
||||
"black": "\033[30m",
|
||||
"red": "\033[31m",
|
||||
"green": "\033[32m",
|
||||
"yellow": "\033[33m",
|
||||
"blue": "\033[34m",
|
||||
"magenta": "\033[35m",
|
||||
"cyan": "\033[36m",
|
||||
"white": "\033[37m"
|
||||
}
|
||||
TEXT_COLOR_LEVELS = {
|
||||
"light": "\033[1;{}m", # Light color prefix
|
||||
"dark": "\033[2;{}m" # Dark color prefix
|
||||
}
|
||||
BACKGROUND_COLORS = {
|
||||
"black": "\033[40m",
|
||||
"red": "\033[41m",
|
||||
"green": "\033[42m",
|
||||
"yellow": "\033[43m",
|
||||
"blue": "\033[44m",
|
||||
"magenta": "\033[45m",
|
||||
"cyan": "\033[46m",
|
||||
"white": "\033[47m"
|
||||
}
|
||||
TEXT_ATTRIBUTES = {
|
||||
"bold": "\033[1m",
|
||||
"italic": "\033[3m",
|
||||
"underline": "\033[4m",
|
||||
"blink": "\033[5m",
|
||||
"reverse": "\033[7m",
|
||||
"strikethrough": "\033[9m"
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def format_text(text, color=None, color_level=None, background=None, attributes=None, target_text=''):
|
||||
formatted_text = ""
|
||||
start_index = text.find(target_text)
|
||||
end_index = start_index + len(target_text) if start_index != -1 else len(text)
|
||||
|
||||
if color in TextFormatter.TEXT_COLORS:
|
||||
if color_level in TextFormatter.TEXT_COLOR_LEVELS:
|
||||
color_code = TextFormatter.TEXT_COLORS[color]
|
||||
color_format = TextFormatter.TEXT_COLOR_LEVELS[color_level].format(color_code)
|
||||
formatted_text += color_format
|
||||
else:
|
||||
formatted_text += TextFormatter.TEXT_COLORS[color]
|
||||
|
||||
if background in TextFormatter.BACKGROUND_COLORS:
|
||||
formatted_text += TextFormatter.BACKGROUND_COLORS[background]
|
||||
|
||||
if attributes in TextFormatter.TEXT_ATTRIBUTES:
|
||||
formatted_text += TextFormatter.TEXT_ATTRIBUTES[attributes]
|
||||
|
||||
if target_text == "":
|
||||
formatted_text += text + TextFormatter.RESET
|
||||
else:
|
||||
formatted_text += text[:start_index] + text[start_index:end_index] + TextFormatter.RESET + text[end_index:]
|
||||
|
||||
return formatted_text
|
||||
|
||||
def insert_string(base, inserted, position=0):
|
||||
return base[:position] + inserted + base[position + len(inserted):]
|
||||
|
||||
class Steps:
|
||||
steps1 = ['[ ]', '[- ]', '[-- ]', '[---]', '[ --]', '[ -]']
|
||||
steps2 = ['[ ]', '[- ]', '[ - ]', '[ -]']
|
||||
steps3 = ['[ ]', '[- ]', '[-- ]', '[ --]', '[ -]', '[ ]', '[ -]', '[ --]', '[-- ]', '[- ]']
|
||||
steps4 = ['[ ]', '[- ]', '[ - ]', '[ -]', '[ ]', '[ -]', '[ - ]', '[- ]', '[ ]']
|
||||
steps5 = ['[ ]', '[ -]', '[ --]', '[---]', '[-- ]', '[- ]']
|
||||
steps6 = ['[ ]', '[ -]', '[ - ]', '[- ]']
|
||||
expand_contract = ['[ ]', '[= ]', '[== ]', '[=== ]', '[====]', '[ ===]', '[ ==]', '[ =]', '[ ]']
|
||||
rotating_dots = ['. ', '.. ', '... ', '.... ', '.....', ' ....', ' ...', ' ..', ' .', ' ']
|
||||
bouncing_ball = ['o ', ' o ', ' o ', ' o ', ' o ', ' o', ' o ', ' o ', ' o ', ' o ', 'o ']
|
||||
left_right_dots = ['[ ]', '[. ]', '[.. ]', '[... ]', '[....]', '[ ...]', '[ ..]', '[ .]', '[ ]']
|
||||
expanding_square = ['[ ]', '[■]', '[■■]', '[■■■]', '[■■■■]', '[■■■]', '[■■]', '[■]', '[ ]']
|
||||
spinner = ['|', '/', '-', '\\', '|', '/', '-', '\\']
|
||||
zigzag = ['/ ', ' / ', ' / ', ' /', ' / ', ' / ', '/ ', '\\ ', ' \\ ', ' \\ ', ' \\', ' \\ ', ' \\ ', '\\ ']
|
||||
arrows = ['← ', '←← ', '←←←', '←← ', '← ', '→ ', '→→ ', '→→→', '→→ ', '→ ']
|
||||
snake = ['[> ]', '[=> ]', '[==> ]', '[===> ]', '[====>]', '[ ===>]', '[ ==>]', '[ =>]', '[ >]']
|
||||
loading_bar = ['[ ]', '[= ]', '[== ]', '[=== ]', '[==== ]', '[===== ]', '[====== ]', '[======= ]', '[======== ]', '[========= ]', '[==========]']
|
||||
|
||||
class indeterminateStatus:
|
||||
def __init__(self, client, desc="Loading...", end="[ OK ]", timeout=0.1, fail='[FAILED]', steps=None):
|
||||
self.client = client
|
||||
|
||||
self.desc = desc
|
||||
self.end = end
|
||||
self.timeout = timeout
|
||||
@ -60,13 +157,14 @@ class indeterminateStatus:
|
||||
|
||||
self._thread = Thread(target=self._animate, daemon=True)
|
||||
if steps is None:
|
||||
self.steps = steps1
|
||||
self.steps = Steps.steps1
|
||||
else:
|
||||
self.steps = steps
|
||||
self.done = False
|
||||
self.fail = False
|
||||
|
||||
def start(self):
|
||||
"""Start progress bar"""
|
||||
self._thread.start()
|
||||
return self
|
||||
|
||||
@ -81,12 +179,14 @@ class indeterminateStatus:
|
||||
self.start()
|
||||
|
||||
def stop(self):
|
||||
"""stop progress"""
|
||||
self.done = True
|
||||
cols = self.client["windowsize"]["width"]
|
||||
Print(self.client['channel'], "\r" + " " * cols, end="")
|
||||
Print(self.client['channel'], f"\r{self.end}")
|
||||
|
||||
def stopfail(self):
|
||||
"""stop progress with error or fail"""
|
||||
self.done = True
|
||||
self.fail = True
|
||||
cols = self.client["windowsize"]["width"]
|
||||
@ -147,7 +247,7 @@ class LoadingProgress:
|
||||
self._thread = Thread(target=self._animate, daemon=True)
|
||||
|
||||
if steps is None:
|
||||
self.steps = steps1
|
||||
self.steps = Steps.steps1
|
||||
else:
|
||||
self.steps = steps
|
||||
|
||||
@ -164,14 +264,17 @@ class LoadingProgress:
|
||||
self.currentprint = ""
|
||||
|
||||
def start(self):
|
||||
"""Start progress bar"""
|
||||
self._thread.start()
|
||||
self.startime = time.perf_counter()
|
||||
return self
|
||||
|
||||
def update(self, i):
|
||||
def update(self, i=1):
|
||||
"""update progress"""
|
||||
self.current += i
|
||||
|
||||
def updatebuffer(self, i):
|
||||
def updatebuffer(self, i=1):
|
||||
"""update buffer progress"""
|
||||
self.currentbuffer += i
|
||||
|
||||
def _animate(self):
|
||||
@ -287,12 +390,14 @@ class LoadingProgress:
|
||||
self.start()
|
||||
|
||||
def stop(self):
|
||||
"""stop progress"""
|
||||
self.done = True
|
||||
cols = self.client["windowsize"]["width"]
|
||||
Print(self.client["channel"], "\r" + " " * cols, end="")
|
||||
Print(self.client["channel"], f"\r{self.end}")
|
||||
|
||||
def stopfail(self):
|
||||
"""stop progress with error or fail"""
|
||||
self.done = True
|
||||
self.fail = True
|
||||
cols = self.client["windowsize"]["width"]
|
||||
|
294
src/PyserSSH/extensions/remodesk.py
Normal file
294
src/PyserSSH/extensions/remodesk.py
Normal file
@ -0,0 +1,294 @@
|
||||
"""
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
Copyright (C) 2023-present DPSoftware Foundation (MIT)
|
||||
|
||||
Visit https://github.com/DPSoftware-Foundation/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 socket
|
||||
import threading
|
||||
import brotli
|
||||
import numpy as np
|
||||
import cv2
|
||||
from PIL import ImageGrab
|
||||
import struct
|
||||
import queue
|
||||
import pickle
|
||||
import mouse
|
||||
import keyboard
|
||||
import logging
|
||||
|
||||
from ..system.clientype import Client
|
||||
|
||||
logger = logging.getLogger("PyserSSH.Ext.RemoDeskSSH")
|
||||
|
||||
class Protocol:
|
||||
def __init__(self, server):
|
||||
self.listclient = []
|
||||
self.first = True
|
||||
self.running = False
|
||||
self.server = server
|
||||
self.buffer = queue.Queue(maxsize=10)
|
||||
|
||||
def _handle_client(self):
|
||||
try:
|
||||
while self.running:
|
||||
data2send = self.buffer.get()
|
||||
|
||||
for iclient in self.listclient:
|
||||
try:
|
||||
iclient[2].sendall(data2send)
|
||||
except Exception as e:
|
||||
iclient[2].close()
|
||||
self.listclient.remove(iclient)
|
||||
|
||||
if not self.listclient:
|
||||
self.running = False
|
||||
self.first = True
|
||||
logger.info("No clients connected. Server is standby")
|
||||
break
|
||||
|
||||
except socket.error:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.error(f"Error in handle_client: {e}")
|
||||
|
||||
def _handle_client_commands(self, client, id):
|
||||
try:
|
||||
while True:
|
||||
client_socket = client.get_subchannel(id)
|
||||
|
||||
try:
|
||||
# Receive the length of the data
|
||||
data_length = self._receive_exact(client_socket, 4)
|
||||
if not data_length:
|
||||
break
|
||||
|
||||
commandmetadata = struct.unpack('!I', data_length)
|
||||
command_data = self._receive_exact(client_socket, commandmetadata[0])
|
||||
command = pickle.loads(command_data)
|
||||
|
||||
if command:
|
||||
self.handle_commands(command, client)
|
||||
|
||||
except socket.error:
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"Error in handle_client_commands: {e}")
|
||||
|
||||
def handle_commands(self, command, client):
|
||||
pass
|
||||
|
||||
def _receive_exact(self, socket, n):
|
||||
"""Helper function to receive exactly n bytes."""
|
||||
data = b''
|
||||
while len(data) < n:
|
||||
packet = socket.recv(n - len(data))
|
||||
if not packet:
|
||||
return None
|
||||
data += packet
|
||||
return data
|
||||
|
||||
def init(self, client):
|
||||
pass
|
||||
|
||||
def handle_new_client(self, client: Client, directchannel=None):
|
||||
if directchannel:
|
||||
id = directchannel.get_id()
|
||||
channel = directchannel
|
||||
else:
|
||||
logger.info("waiting remote channel")
|
||||
id, channel = client.open_new_subchannel(5)
|
||||
if id == None or channel == None:
|
||||
logger.info("client is not connect in 5 sec")
|
||||
return
|
||||
|
||||
self.listclient.append([client, id, channel])
|
||||
|
||||
if self.first:
|
||||
self.running = True
|
||||
handle_client_thread = threading.Thread(target=self._handle_client, daemon=True)
|
||||
handle_client_thread.start()
|
||||
|
||||
self.init(client)
|
||||
|
||||
self.first = False
|
||||
|
||||
command_thread = threading.Thread(target=self._handle_client_commands, args=(client, id), daemon=True)
|
||||
command_thread.start()
|
||||
|
||||
class RemoDesk(Protocol):
|
||||
def __init__(self, server=None, quality=50, compression=50, format="jpeg", resolution: set[int, int] = None, activity_threshold=None, second_compress=True):
|
||||
"""
|
||||
Args:
|
||||
server: ssh server
|
||||
quality: quality of remote
|
||||
compression: percent of compression 0-100 %
|
||||
format: jpeg, webp, avif
|
||||
resolution: resolution of remote
|
||||
"""
|
||||
|
||||
super().__init__(server)
|
||||
|
||||
self.quality = quality
|
||||
self.compression = compression
|
||||
self.format = format
|
||||
self.resolution = resolution
|
||||
self.threshold = activity_threshold
|
||||
self.compress2 = second_compress
|
||||
self.screensize = ()
|
||||
self.previous_frame = None
|
||||
|
||||
def _capture_screen(self):
|
||||
try:
|
||||
screenshot = ImageGrab.grab()
|
||||
self.screensize = screenshot.size
|
||||
img_np = np.array(screenshot)
|
||||
img_bgr = cv2.cvtColor(img_np, cv2.COLOR_RGB2BGR)
|
||||
return img_bgr
|
||||
except:
|
||||
return b""
|
||||
|
||||
def _detect_activity(self, current_frame):
|
||||
if self.threshold:
|
||||
if self.previous_frame is None:
|
||||
self.previous_frame = current_frame
|
||||
return False # No previous frame to compare to
|
||||
|
||||
# Compute the absolute difference between the current frame and the previous frame
|
||||
diff = cv2.absdiff(current_frame, self.previous_frame)
|
||||
|
||||
# Convert the difference to grayscale
|
||||
gray_diff = cv2.cvtColor(diff, cv2.COLOR_BGR2GRAY)
|
||||
|
||||
# Apply a threshold to get a binary image
|
||||
_, thresh = cv2.threshold(gray_diff, self.threshold, 255, cv2.THRESH_BINARY)
|
||||
|
||||
# Calculate the number of non-zero pixels in the thresholded image
|
||||
non_zero_count = np.count_nonzero(thresh)
|
||||
|
||||
# Update the previous frame
|
||||
self.previous_frame = current_frame
|
||||
|
||||
# If there are enough non-zero pixels, we consider it as activity
|
||||
return non_zero_count > 500 # You can adjust the threshold as needed
|
||||
else:
|
||||
return True
|
||||
|
||||
def _imagenc(self, image):
|
||||
if self.format == "webp":
|
||||
retval, buffer = cv2.imencode('.webp', image, [int(cv2.IMWRITE_WEBP_QUALITY), self.quality])
|
||||
elif self.format == "jpeg":
|
||||
retval, buffer = cv2.imencode('.jpeg', image, [int(cv2.IMWRITE_JPEG_QUALITY), self.quality])
|
||||
elif self.format == "avif":
|
||||
retval, buffer = cv2.imencode('.avif', image, [int(cv2.IMWRITE_AVIF_QUALITY), self.quality])
|
||||
|
||||
else:
|
||||
raise TypeError(f"{self.format} is not supported")
|
||||
|
||||
if not retval:
|
||||
raise ValueError("image encoding failed.")
|
||||
|
||||
return np.array(buffer).tobytes()
|
||||
|
||||
def _translate_coordinates(self, x, y):
|
||||
if self.resolution:
|
||||
translated_x = int(x * (self.screensize[0] / self.resolution[0]))
|
||||
translated_y = int(y * (self.screensize[1] / self.resolution[1]))
|
||||
else:
|
||||
translated_x = int(x * (self.screensize[0] / 1920))
|
||||
translated_y = int(y * (self.screensize[1] / 1090))
|
||||
return translated_x, translated_y
|
||||
|
||||
def _convert_quality(self, quality):
|
||||
brotli_quality = int(quality / 100 * 11)
|
||||
lgwin = int(10 + (quality / 100 * (24 - 10)))
|
||||
|
||||
return brotli_quality, lgwin
|
||||
|
||||
def _capture(self):
|
||||
while self.running:
|
||||
screen_image = self._capture_screen()
|
||||
|
||||
if self._detect_activity(screen_image):
|
||||
if self.resolution:
|
||||
screen_image = cv2.resize(screen_image, self.resolution, interpolation=cv2.INTER_NEAREST)
|
||||
else:
|
||||
self.resolution = self.screensize
|
||||
|
||||
data = self._imagenc(screen_image)
|
||||
|
||||
if self.compress2:
|
||||
bquality, lgwin = self._convert_quality(self.compression)
|
||||
data = brotli.compress(data, quality=bquality, lgwin=lgwin)
|
||||
|
||||
data_length = struct.pack('!III', len(data), self.resolution[0], self.resolution[1])
|
||||
data2send = data_length + data
|
||||
|
||||
print(f"Sending data length: {len(data2send)}")
|
||||
self.buffer.put(data2send)
|
||||
|
||||
def handle_commands(self, command, client):
|
||||
action = command["action"]
|
||||
data = command["data"]
|
||||
|
||||
if action == "move_mouse":
|
||||
x, y = data["x"], data["y"]
|
||||
rx, ry = self._translate_coordinates(x, y)
|
||||
mouse.move(rx, ry)
|
||||
|
||||
elif action == "click_mouse":
|
||||
button = data["button"]
|
||||
state = data["state"]
|
||||
|
||||
if button == 1:
|
||||
if state == "down":
|
||||
mouse.press()
|
||||
else:
|
||||
mouse.release()
|
||||
elif button == 2:
|
||||
if state == "down":
|
||||
mouse.press(mouse.MIDDLE)
|
||||
else:
|
||||
mouse.release(mouse.MIDDLE)
|
||||
elif button == 3:
|
||||
if state == "down":
|
||||
mouse.press(mouse.RIGHT)
|
||||
else:
|
||||
mouse.release(mouse.RIGHT)
|
||||
# elif button == 4:
|
||||
# mouse.wheel()
|
||||
# elif button == 5:
|
||||
# mouse.wheel(-1)
|
||||
elif action == "keyboard":
|
||||
key = data["key"]
|
||||
state = data["state"]
|
||||
|
||||
if state == "down":
|
||||
keyboard.press(key)
|
||||
else:
|
||||
keyboard.release(key)
|
||||
|
||||
def init(self, client):
|
||||
capture_thread = threading.Thread(target=self._capture, daemon=True)
|
||||
capture_thread.start()
|
140
src/PyserSSH/extensions/serverutils.py
Normal file
140
src/PyserSSH/extensions/serverutils.py
Normal file
@ -0,0 +1,140 @@
|
||||
"""
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
Copyright (C) 2023-present DPSoftware Foundation (MIT)
|
||||
|
||||
Visit https://github.com/DPSoftware-Foundation/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 logging
|
||||
|
||||
from ..interactive import Send
|
||||
|
||||
logger = logging.getLogger("PyserSSH.Ext.ServerUtils")
|
||||
|
||||
def kickbyusername(server, username, reason=None):
|
||||
"""
|
||||
Kicks a user from the server by their username.
|
||||
|
||||
Args:
|
||||
server (Server): The server object where clients are connected.
|
||||
username (str): The username of the client to be kicked.
|
||||
reason (str, optional): The reason for kicking the user. If None, no reason is provided.
|
||||
"""
|
||||
for peername, client_handler in list(server.client_handlers.items()):
|
||||
if client_handler["current_user"] == username:
|
||||
channel = client_handler.get("channel")
|
||||
server._handle_event("disconnected", channel.getpeername(), server.client_handlers[channel.getpeername()]["current_user"])
|
||||
if reason is None:
|
||||
if channel:
|
||||
channel.close()
|
||||
logger.info(f"User '{username}' has been kicked.")
|
||||
else:
|
||||
if channel:
|
||||
Send(channel, f"You have been disconnected for {reason}")
|
||||
channel.close()
|
||||
logger.info(f"User '{username}' has been kicked by reason {reason}.")
|
||||
|
||||
def kickbypeername(server, peername, reason=None):
|
||||
"""
|
||||
Kicks a user from the server by their peername.
|
||||
|
||||
Args:
|
||||
server (Server): The server object where clients are connected.
|
||||
peername (str): The peername of the client to be kicked.
|
||||
reason (str, optional): The reason for kicking the user. If None, no reason is provided.
|
||||
"""
|
||||
client_handler = server.client_handlers.get(peername)
|
||||
if client_handler:
|
||||
channel = client_handler.get("channel")
|
||||
server._handle_event("disconnected", channel.getpeername(), server.client_handlers[channel.getpeername()]["current_user"])
|
||||
if reason is None:
|
||||
if channel:
|
||||
channel.close()
|
||||
logger.info(f"peername '{peername}' has been kicked.")
|
||||
else:
|
||||
if channel:
|
||||
Send(channel, f"You have been disconnected for {reason}")
|
||||
channel.close()
|
||||
logger.info(f"peername '{peername}' has been kicked by reason {reason}.")
|
||||
|
||||
def kickall(server, reason=None):
|
||||
"""
|
||||
Kicks all users from the server.
|
||||
|
||||
Args:
|
||||
server (Server): The server object where clients are connected.
|
||||
reason (str, optional): The reason for kicking all users. If None, no reason is provided.
|
||||
"""
|
||||
for peername, client_handler in server.client_handlers.items():
|
||||
channel = client_handler.get("channel")
|
||||
server._handle_event("disconnected", channel.getpeername(), server.client_handlers[channel.getpeername()]["current_user"])
|
||||
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:
|
||||
server.client_handlers.clear()
|
||||
logger.info("All users have been kicked.")
|
||||
else:
|
||||
logger.info(f"All users have been kicked by reason {reason}.")
|
||||
|
||||
def broadcast(server, message):
|
||||
"""
|
||||
Broadcasts a message to all connected clients.
|
||||
|
||||
Args:
|
||||
server (Server): The server object where clients are connected.
|
||||
message (str): The message to send to all clients.
|
||||
"""
|
||||
for client_handler in server.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(server, username, message):
|
||||
"""
|
||||
Sends a message to a specific user by their username.
|
||||
|
||||
Args:
|
||||
server (Server): The server object where clients are connected.
|
||||
username (str): The username of the client to send the message to.
|
||||
message (str): The message to send to the specified user.
|
||||
"""
|
||||
for client_handler in server.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.")
|
@ -1,8 +1,8 @@
|
||||
"""
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/damp11113/PyserSSH
|
||||
Copyright (C) 2023-2024 damp11113 (MIT)
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
Copyright (C) 2023-present DPSoftware Foundation (MIT)
|
||||
|
||||
Visit https://github.com/damp11113/PyserSSH
|
||||
Visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
|
||||
MIT License
|
||||
|
||||
@ -29,19 +29,51 @@ import socket
|
||||
|
||||
from .system.sysfunc import replace_enter_with_crlf
|
||||
|
||||
def Send(client, string, ln=True):
|
||||
channel = client["channel"]
|
||||
def Send(client, string, ln=True, directchannel=False):
|
||||
if directchannel:
|
||||
channel = client
|
||||
else:
|
||||
channel = client["channel"]
|
||||
|
||||
if ln:
|
||||
channel.send(replace_enter_with_crlf(str(string) + "\n"))
|
||||
else:
|
||||
channel.send(replace_enter_with_crlf(str(string)))
|
||||
|
||||
def Clear(client, oldclear=False):
|
||||
def NewSend(client, *astring, ln=True, end=b'\n', sep=b' ', directchannel=False):
|
||||
if directchannel:
|
||||
channel = client
|
||||
else:
|
||||
channel = client["channel"]
|
||||
|
||||
if ln:
|
||||
if not b'\n' in end:
|
||||
end += b'\n'
|
||||
else:
|
||||
# Ensure that `end` does not contain `b'\n'` if `ln` is False
|
||||
end = end.replace(b'\n', b'')
|
||||
|
||||
# Prepare the strings to be sent
|
||||
if astring:
|
||||
for i, s in enumerate(astring):
|
||||
# Convert `s` to bytes if it's a string
|
||||
if isinstance(s, str):
|
||||
s = s.encode('utf-8')
|
||||
# Use a hypothetical `replace_enter_with_crlf` function if needed
|
||||
channel.send(replace_enter_with_crlf(s))
|
||||
if i != len(astring) - 1:
|
||||
channel.send(sep)
|
||||
channel.send(end)
|
||||
|
||||
def Clear(client, oldclear=False, keep=False):
|
||||
sx, sy = client["windowsize"]["width"], client["windowsize"]["height"]
|
||||
|
||||
if oldclear:
|
||||
for x in range(sy):
|
||||
Send(client, '\b \b' * sx, ln=False) # Send newline after each line
|
||||
elif keep:
|
||||
Send(client, "\033[2J", ln=False)
|
||||
Send(client, "\033[H", ln=False)
|
||||
else:
|
||||
Send(client, "\033[3J", ln=False)
|
||||
Send(client, "\033[1J", ln=False)
|
||||
@ -50,8 +82,11 @@ def Clear(client, oldclear=False):
|
||||
def Title(client, title):
|
||||
Send(client, f"\033]0;{title}\007", ln=False)
|
||||
|
||||
def wait_input(client, prompt="", defaultvalue=None, cursor_scroll=False, echo=True, password=False, passwordmask=b"*", noabort=False, timeout=0):
|
||||
channel = client["channel"]
|
||||
def wait_input(client, prompt="", defaultvalue=None, cursor_scroll=False, echo=True, password=False, passwordmask=b"*", noabort=False, timeout=0, directchannel=False):
|
||||
if directchannel:
|
||||
channel = client
|
||||
else:
|
||||
channel = client["channel"]
|
||||
|
||||
channel.send(replace_enter_with_crlf(prompt))
|
||||
|
||||
@ -112,6 +147,8 @@ def wait_input(client, prompt="", defaultvalue=None, cursor_scroll=False, echo=T
|
||||
channel.sendall(b'\r\n')
|
||||
raise
|
||||
else:
|
||||
channel.setblocking(False)
|
||||
channel.settimeout(None)
|
||||
output = buffer.decode('utf-8')
|
||||
|
||||
# Return default value if specified and no input given
|
||||
@ -120,7 +157,7 @@ def wait_input(client, prompt="", defaultvalue=None, cursor_scroll=False, echo=T
|
||||
else:
|
||||
return output
|
||||
|
||||
def wait_inputkey(client, prompt="", raw=False, timeout=0):
|
||||
def wait_inputkey(client, prompt="", raw=True, timeout=0):
|
||||
channel = client["channel"]
|
||||
|
||||
if prompt != "":
|
||||
@ -139,16 +176,74 @@ def wait_inputkey(client, prompt="", raw=False, timeout=0):
|
||||
if bool(re.compile(b'\x1b\[[0-9;]*[mGK]').search(byte)):
|
||||
pass
|
||||
|
||||
channel.setblocking(False)
|
||||
channel.settimeout(None)
|
||||
if prompt != "":
|
||||
channel.send("\r\n")
|
||||
return byte.decode('utf-8') # only regular character
|
||||
|
||||
else:
|
||||
channel.setblocking(False)
|
||||
channel.settimeout(None)
|
||||
if prompt != "":
|
||||
channel.send("\r\n")
|
||||
return byte
|
||||
|
||||
except socket.timeout:
|
||||
channel.setblocking(False)
|
||||
channel.settimeout(None)
|
||||
channel.send("\r\n")
|
||||
if prompt != "":
|
||||
channel.send("\r\n")
|
||||
return None
|
||||
except Exception:
|
||||
channel.setblocking(False)
|
||||
channel.settimeout(None)
|
||||
if prompt != "":
|
||||
channel.send("\r\n")
|
||||
raise
|
||||
|
||||
def wait_inputmouse(client, timeout=0):
|
||||
channel = client["channel"]
|
||||
Send(client, "\033[?1000h", ln=False)
|
||||
|
||||
if timeout != 0:
|
||||
channel.settimeout(timeout)
|
||||
|
||||
try:
|
||||
byte = channel.recv(10)
|
||||
|
||||
if not byte or byte == b'\x04':
|
||||
raise EOFError()
|
||||
|
||||
if byte.startswith(b'\x1b[M'):
|
||||
# Parse mouse event
|
||||
if len(byte) < 6 or not byte.startswith(b'\x1b[M'):
|
||||
channel.setblocking(False)
|
||||
channel.settimeout(None)
|
||||
Send(client, "\033[?1000l", ln=False)
|
||||
return None, None, None
|
||||
|
||||
# Extract button, x, y from the sequence
|
||||
button = byte[3] - 32
|
||||
x = byte[4] - 32
|
||||
y = byte[5] - 32
|
||||
|
||||
channel.setblocking(False)
|
||||
channel.settimeout(None)
|
||||
Send(client, "\033[?1000l", ln=False)
|
||||
return button, x, y
|
||||
else:
|
||||
channel.setblocking(False)
|
||||
channel.settimeout(None)
|
||||
Send(client, "\033[?1000l", ln=False)
|
||||
return byte, None, None
|
||||
|
||||
except socket.timeout:
|
||||
channel.setblocking(False)
|
||||
channel.settimeout(None)
|
||||
channel.send("\r\n")
|
||||
Send(client, "\033[?1000l", ln=False)
|
||||
return None, None, None
|
||||
except Exception:
|
||||
channel.setblocking(False)
|
||||
channel.settimeout(None)
|
||||
@ -173,18 +268,22 @@ def wait_choose(client, choose, prompt="", timeout=0):
|
||||
exported = " ".join(tempchooselist)
|
||||
|
||||
if prompt.strip() == "":
|
||||
Send(channel, f'\r{exported}', ln=False)
|
||||
Send(client, f'\r{exported}', ln=False)
|
||||
else:
|
||||
Send(channel, f'\r{prompt}{exported}', ln=False)
|
||||
Send(client, f'\r{prompt}{exported}', ln=False)
|
||||
|
||||
keyinput = wait_inputkey(channel, raw=True)
|
||||
keyinput = wait_inputkey(client, raw=True)
|
||||
|
||||
if keyinput == b'\r': # Enter key
|
||||
Send(channel, "\033[K")
|
||||
channel.setblocking(False)
|
||||
channel.settimeout(None)
|
||||
Send(client, "\033[K")
|
||||
return chooseindex
|
||||
elif keyinput == b'\x03': # ' ctrl+c' key for cancel
|
||||
Send(channel, "\033[K")
|
||||
return None
|
||||
channel.setblocking(False)
|
||||
channel.settimeout(None)
|
||||
Send(client, "\033[K")
|
||||
return 0
|
||||
elif keyinput == b'\x1b[D': # Up arrow key
|
||||
chooseindex -= 1
|
||||
if chooseindex < 0:
|
||||
|
@ -1,8 +1,8 @@
|
||||
"""
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/damp11113/PyserSSH
|
||||
Copyright (C) 2023-2024 damp11113 (MIT)
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
Copyright (C) 2023-present DPSoftware Foundation (MIT)
|
||||
|
||||
Visit https://github.com/damp11113/PyserSSH
|
||||
Visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
|
||||
MIT License
|
||||
|
||||
@ -25,37 +25,42 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
import paramiko
|
||||
import threading
|
||||
from functools import wraps
|
||||
import logging
|
||||
import socket
|
||||
import random
|
||||
import traceback
|
||||
|
||||
from .system.SFTP import SSHSFTPServer
|
||||
from .system.sysfunc import replace_enter_with_crlf
|
||||
from .system.interface import Sinterface
|
||||
from .interactive import *
|
||||
from .system.inputsystem import expect
|
||||
from .system.info import system_banner, __version__
|
||||
from .system.info import version, system_banner
|
||||
from .system.clientype import Client as Clientype
|
||||
from .system.ProWrapper import SSHTransport, TelnetTransport, ITransport
|
||||
|
||||
#paramiko.sftp_file.SFTPFile.MAX_REQUEST_SIZE = pow(2, 22)
|
||||
# paramiko.sftp_file.SFTPFile.MAX_REQUEST_SIZE = pow(2, 22)
|
||||
|
||||
sftpclient = ["WinSCP", "Xplore"]
|
||||
|
||||
logger = logging.getLogger("PyserSSH")
|
||||
logger = logging.getLogger("PyserSSH.Server")
|
||||
|
||||
class Server:
|
||||
def __init__(self, accounts, system_message=True, disable_scroll_with_arrow=True, sftp=True, sftproot=os.getcwd(), system_commands=True, compression=True, usexternalauth=False, history=True, inputsystem=True, XHandler=None, title=f"PyserSSH v{__version__}", inspeed=32768):
|
||||
def __init__(self, accounts, system_message=True, disable_scroll_with_arrow=True, sftp=False, system_commands=True, compression=True, usexternalauth=False, history=True, inputsystem=True, XHandler=None, title=f"PyserSSH v{version}", inspeed=32768, enable_preauth_banner=False, enable_exec_system_command=True, enable_remote_status=False, inputsystem_echo=True):
|
||||
"""
|
||||
A simple SSH server
|
||||
system_message set to False to disable welcome message from system
|
||||
disable_scroll_with_arrow set to False to enable seek text with arrow (Beta)
|
||||
sftp set to True to enable SFTP server
|
||||
system_commands set to False to disable system commmands
|
||||
compression set to False to disable SSH compression
|
||||
enable_remote_status set to True to enable mobaxterm remote monitor (Beta)
|
||||
"""
|
||||
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.disable_scroll_with_arrow = disable_scroll_with_arrow
|
||||
self.sftproot = sftproot
|
||||
self.sftpena = sftp
|
||||
self.enasyscom = system_commands
|
||||
self.compressena = compression
|
||||
@ -65,243 +70,289 @@ class Server:
|
||||
self.XHandler = XHandler
|
||||
self.title = title
|
||||
self.inspeed = inspeed
|
||||
self.enaloginbanner = enable_preauth_banner
|
||||
self.enasysexec = enable_exec_system_command
|
||||
self.enaremostatus = enable_remote_status
|
||||
self.inputsysecho = inputsystem_echo
|
||||
|
||||
self.system_banner = system_banner
|
||||
if self.XHandler != None:
|
||||
self.XHandler.serverself = self
|
||||
|
||||
self._event_handlers = {}
|
||||
self.client_handlers = {} # Dictionary to store event handlers for each client
|
||||
self.__processmode = None
|
||||
self.isrunning = False
|
||||
self.__daemon = False
|
||||
self.private_key = ""
|
||||
self.__custom_server_args = ()
|
||||
self.__custom_server = None
|
||||
self.__protocol = "ssh"
|
||||
|
||||
if self.enasyscom:
|
||||
print("\033[33m!!Warning!! System commands is enable! \033[0m")
|
||||
|
||||
def on_user(self, event_name):
|
||||
"""Handle event"""
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
def wrapper(channel, *args, **kwargs):
|
||||
def wrapper(client, *args, **kwargs):
|
||||
# Ignore the third argument
|
||||
filtered_args = args[:2] + args[3:]
|
||||
return func(channel, *filtered_args, **kwargs)
|
||||
return func(client, *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_client_disconnection(self, handler, chandlers):
|
||||
if not chandlers["transport"].is_active():
|
||||
if handler:
|
||||
handler(chandlers)
|
||||
del self.client_handlers[chandlers["peername"]]
|
||||
|
||||
def _handle_event(self, event_name, *args, **kwargs):
|
||||
handler = self._event_handlers.get(event_name)
|
||||
if handler:
|
||||
handler(*args, **kwargs)
|
||||
if event_name == "error" and isinstance(args[0], Clientype):
|
||||
args[0].last_error = traceback.format_exc()
|
||||
|
||||
if event_name == "disconnected":
|
||||
self.handle_client_disconnection(*args, **kwargs)
|
||||
self.handle_client_disconnection(handler, *args, **kwargs)
|
||||
elif handler:
|
||||
return handler(*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)
|
||||
|
||||
bh_session.default_max_packet_size = self.inspeed
|
||||
def _handle_client(self, socketchannel, addr):
|
||||
self._handle_event("preserver", socketchannel)
|
||||
|
||||
logger.info("Starting session...")
|
||||
server = Sinterface(self)
|
||||
bh_session.start_server(server=server)
|
||||
|
||||
logger.info(bh_session.remote_version)
|
||||
if not self.__custom_server:
|
||||
if self.__protocol.lower() == "telnet":
|
||||
bh_session = TelnetTransport(socketchannel, server) # Telnet server
|
||||
else:
|
||||
bh_session = SSHTransport(socketchannel, server, self.private_key) # SSH server
|
||||
else:
|
||||
bh_session = self.__custom_server(socketchannel, server, *self.__custom_server_args) # custom server
|
||||
|
||||
bh_session.enable_compression(self.compressena)
|
||||
|
||||
bh_session.max_packet_size(self.inspeed)
|
||||
|
||||
try:
|
||||
bh_session.start_server()
|
||||
except:
|
||||
return
|
||||
|
||||
channel = bh_session.accept()
|
||||
|
||||
if self.sftpena:
|
||||
bh_session.set_subsystem_handler('sftp', paramiko.SFTPServer, SSHSFTPServer, channel, self.accounts, self.client_handlers)
|
||||
|
||||
if not bh_session.is_authenticated():
|
||||
logger.warning("user not authenticated")
|
||||
bh_session.close()
|
||||
return
|
||||
|
||||
if channel is None:
|
||||
logger.warning("no channel")
|
||||
bh_session.close()
|
||||
return
|
||||
|
||||
try:
|
||||
logger.info("user authenticated")
|
||||
peername = channel.getpeername()
|
||||
peername = bh_session.getpeername()
|
||||
if peername not in self.client_handlers:
|
||||
# Create a new event handler for this client if it doesn't exist
|
||||
self.client_handlers[peername] = {
|
||||
"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": {},
|
||||
"x11": {}
|
||||
}
|
||||
self.client_handlers[peername] = Clientype(channel, bh_session, peername)
|
||||
|
||||
client_handler = self.client_handlers[peername]
|
||||
client_handler["current_user"] = server.current_user
|
||||
client_handler["current_user"] = bh_session.get_username()
|
||||
client_handler["channel"] = channel # Update the channel attribute for the client handler
|
||||
client_handler["transport"] = bh_session # Update the channel attribute for the client handler
|
||||
client_handler["last_activity_time"] = time.time()
|
||||
client_handler["last_login_time"] = time.time()
|
||||
client_handler["prompt"] = self.accounts.get_prompt(bh_session.get_username())
|
||||
client_handler["session_id"] = random.randint(10000, 99999) + int(time.time() * 1000)
|
||||
|
||||
self.accounts.set_user_last_login(self.client_handlers[channel.getpeername()]["current_user"], peername[0])
|
||||
|
||||
logger.info("saved user data to client handlers")
|
||||
|
||||
#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:
|
||||
while self.client_handlers[channel.getpeername()]["windowsize"] == {}:
|
||||
pass
|
||||
if not (int(channel.get_out_window_size()) == int(bh_session.get_default_window_size()) and bh_session.get_connection_type() == "SSH"):
|
||||
#timeout for waiting 10 sec
|
||||
for i in range(100):
|
||||
if self.client_handlers[channel.getpeername()]["windowsize"]:
|
||||
break
|
||||
time.sleep(0.1)
|
||||
|
||||
channel.send(f"\033]0;{self.title}\007".encode())
|
||||
if self.client_handlers[channel.getpeername()]["windowsize"] == {}:
|
||||
logger.info("timeout for waiting window size in 10 sec")
|
||||
self.client_handlers[channel.getpeername()]["windowsize"] = {
|
||||
"width": 80,
|
||||
"height": 24,
|
||||
"pixelwidth": 0,
|
||||
"pixelheight": 0
|
||||
}
|
||||
|
||||
if self.sysmess:
|
||||
channel.sendall(replace_enter_with_crlf(self.system_banner))
|
||||
channel.sendall(replace_enter_with_crlf("\n"))
|
||||
try:
|
||||
self._handle_event("pre-shell", self.client_handlers[channel.getpeername()])
|
||||
except Exception as e:
|
||||
self._handle_event("error", self.client_handlers[channel.getpeername()], e)
|
||||
|
||||
while self.client_handlers[channel.getpeername()]["isexeccommandrunning"]:
|
||||
time.sleep(0.1)
|
||||
|
||||
userbanner = self.accounts.get_banner(self.client_handlers[channel.getpeername()]["current_user"])
|
||||
|
||||
if self.accounts.get_user_enable_inputsystem_echo(self.client_handlers[channel.getpeername()]["current_user"]) and self.inputsysecho:
|
||||
echo = True
|
||||
else:
|
||||
echo = False
|
||||
|
||||
if echo:
|
||||
if self.title.strip() != "":
|
||||
channel.send(f"\033]0;{self.title}\007".encode())
|
||||
|
||||
if self.sysmess or userbanner != None:
|
||||
if userbanner is None and self.sysmess:
|
||||
channel.sendall(replace_enter_with_crlf(system_banner))
|
||||
elif userbanner != None and self.sysmess:
|
||||
channel.sendall(replace_enter_with_crlf(system_banner))
|
||||
channel.sendall(replace_enter_with_crlf(userbanner))
|
||||
elif userbanner != None and not self.sysmess:
|
||||
channel.sendall(replace_enter_with_crlf(userbanner))
|
||||
|
||||
channel.sendall(replace_enter_with_crlf("\n"))
|
||||
|
||||
client_handler["connecttype"] = "ssh"
|
||||
|
||||
try:
|
||||
self._handle_event("connect", self.client_handlers[channel.getpeername()])
|
||||
except Exception as e:
|
||||
self._handle_event("error", self.client_handlers[channel.getpeername()], e)
|
||||
|
||||
client_handler["connecttype"] = "ssh"
|
||||
if self.enainputsystem:
|
||||
if self.enainputsystem and self.accounts.get_user_enable_inputsystem(self.client_handlers[channel.getpeername()]["current_user"]):
|
||||
try:
|
||||
if self.accounts.get_user_timeout(self.client_handlers[channel.getpeername()]["current_user"]) != None:
|
||||
channel.setblocking(False)
|
||||
channel.settimeout(self.accounts.get_user_timeout(self.client_handlers[channel.getpeername()]["current_user"]))
|
||||
|
||||
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)
|
||||
if echo:
|
||||
channel.send(replace_enter_with_crlf(self.client_handlers[channel.getpeername()]["prompt"] + " "))
|
||||
|
||||
isConnect = True
|
||||
|
||||
while isConnect:
|
||||
isConnect = expect(self, self.client_handlers[channel.getpeername()], echo)
|
||||
|
||||
self._handle_event("disconnected", self.client_handlers[peername])
|
||||
channel.close()
|
||||
bh_session.close()
|
||||
except KeyboardInterrupt:
|
||||
self._handle_event("disconnected", self.client_handlers[peername]["current_user"])
|
||||
self._handle_event("disconnected", self.client_handlers[peername])
|
||||
channel.close()
|
||||
bh_session.close()
|
||||
except Exception as e:
|
||||
self._handle_event("syserror", client_handler, e)
|
||||
self._handle_event("error", client_handler, e)
|
||||
logger.error(e)
|
||||
finally:
|
||||
self._handle_event("disconnected", self.client_handlers[peername]["current_user"])
|
||||
self._handle_event("disconnected", self.client_handlers[peername])
|
||||
channel.close()
|
||||
bh_session.close()
|
||||
else:
|
||||
if self.sftpena:
|
||||
logger.info("user is sftp")
|
||||
if self.accounts.get_user_sftp_allow(self.client_handlers[channel.getpeername()]["current_user"]):
|
||||
client_handler["connecttype"] = "sftp"
|
||||
self._handle_event("connectsftp", self.client_handlers[channel.getpeername()])
|
||||
while bh_session.is_active():
|
||||
time.sleep(0.1)
|
||||
|
||||
self._handle_event("disconnected", self.client_handlers[peername])
|
||||
else:
|
||||
self._handle_event("disconnected", self.client_handlers[peername]["current_user"])
|
||||
self._handle_event("disconnected", self.client_handlers[peername])
|
||||
channel.close()
|
||||
else:
|
||||
self._handle_event("disconnected", self.client_handlers[peername]["current_user"])
|
||||
self._handle_event("disconnected", self.client_handlers[peername])
|
||||
channel.close()
|
||||
except:
|
||||
pass
|
||||
bh_session.close()
|
||||
|
||||
def stop_server(self):
|
||||
"""Stop server"""
|
||||
logger.info("Stopping the server...")
|
||||
try:
|
||||
for client_handler in self.client_handlers.values():
|
||||
channel = client_handler.get("channel")
|
||||
channel = client_handler.channel
|
||||
if channel:
|
||||
channel.close()
|
||||
self.isrunning = False
|
||||
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()
|
||||
|
||||
self.isrunning = True
|
||||
try:
|
||||
logger.info("Listening for connections...")
|
||||
while self.isrunning:
|
||||
client, addr = self.server.accept()
|
||||
if self.__processmode == "thread":
|
||||
logger.info(f"Starting client thread for connection {addr}")
|
||||
client_thread = threading.Thread(target=self._handle_client, args=(client, addr), daemon=True)
|
||||
client_thread.start()
|
||||
else:
|
||||
logger.info(f"Starting client for connection {addr}")
|
||||
self._handle_client(client, addr)
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
self.stop_server()
|
||||
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")
|
||||
self._handle_event("disconnected", channel.getpeername(), self.client_handlers[channel.getpeername()]["current_user"])
|
||||
if reason is None:
|
||||
if channel:
|
||||
channel.close()
|
||||
logger.info(f"User '{username}' has been kicked.")
|
||||
else:
|
||||
if channel:
|
||||
Send(channel, f"You have been disconnected for {reason}")
|
||||
channel.close()
|
||||
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")
|
||||
self._handle_event("disconnected", channel.getpeername(), self.client_handlers[channel.getpeername()]["current_user"])
|
||||
if reason is None:
|
||||
if channel:
|
||||
channel.close()
|
||||
logger.info(f"peername '{peername}' has been kicked.")
|
||||
def run(self, private_key_path=None, host="0.0.0.0", port=2222, waiting_mode="thread", maxuser=0, daemon=False, listen_thread=True, protocol="ssh", custom_server: ITransport = None, custom_server_args: tuple = None, custom_server_require_socket=True):
|
||||
"""mode: single, thread,
|
||||
protocol: ssh, telnet (beta), serial, custom
|
||||
For serial need to set serial port at host (ex. host="com3") and set baudrate at port (ex. port=9600) and change listen_mode to "single".
|
||||
"""
|
||||
if protocol.lower() == "ssh":
|
||||
if private_key_path != None:
|
||||
logger.info("Loading private key")
|
||||
self.private_key = paramiko.RSAKey(filename=private_key_path)
|
||||
else:
|
||||
if channel:
|
||||
Send(channel, f"You have been disconnected for {reason}")
|
||||
channel.close()
|
||||
logger.info(f"peername '{peername}' has been kicked by reason {reason}.")
|
||||
raise ValueError("No private key")
|
||||
|
||||
def kickall(self, reason=None):
|
||||
for peername, client_handler in self.client_handlers.items():
|
||||
channel = client_handler.get("channel")
|
||||
self._handle_event("disconnected", channel.getpeername(), self.client_handlers[channel.getpeername()]["current_user"])
|
||||
self.__processmode = waiting_mode.lower()
|
||||
self.__daemon = daemon
|
||||
|
||||
if reason is None:
|
||||
if channel:
|
||||
channel.close()
|
||||
if custom_server:
|
||||
self.__custom_server = custom_server
|
||||
self.__custom_server_args = custom_server_args
|
||||
|
||||
if ((custom_server and protocol.lower() == "custom") and custom_server_require_socket) or protocol.lower() in ["ssh", "telnet"]:
|
||||
logger.info("Creating server...")
|
||||
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))
|
||||
|
||||
logger.info("Set listen limit")
|
||||
if maxuser == 0:
|
||||
self.server.listen()
|
||||
else:
|
||||
if channel:
|
||||
Send(channel, f"You have been disconnected for {reason}")
|
||||
channel.close()
|
||||
self.server.listen(maxuser)
|
||||
|
||||
if reason is None:
|
||||
self.client_handlers.clear()
|
||||
logger.info("All users have been kicked.")
|
||||
if listen_thread:
|
||||
logger.info("Starting listening in threading")
|
||||
client_thread = threading.Thread(target=self._start_listening_thread, daemon=self.__daemon)
|
||||
client_thread.start()
|
||||
else:
|
||||
print(f"\033[32mServer is running on {host}:{port}\033[0m")
|
||||
self._start_listening_thread()
|
||||
else:
|
||||
logger.info(f"All users have been kicked by reason {reason}.")
|
||||
client_thread = threading.Thread(target=self._handle_client, args=(None, None), daemon=True)
|
||||
client_thread.start()
|
||||
|
||||
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.")
|
||||
print(f"\033[32mServer is running on {host}:{port}\033[0m")
|
495
src/PyserSSH/system/ProWrapper.py
Normal file
495
src/PyserSSH/system/ProWrapper.py
Normal file
@ -0,0 +1,495 @@
|
||||
"""
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
Copyright (C) 2023-present DPSoftware Foundation (MIT)
|
||||
|
||||
Visit https://github.com/DPSoftware-Foundation/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 serial
|
||||
import socket
|
||||
import paramiko
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Union
|
||||
|
||||
from .interface import Sinterface
|
||||
from ..interactive import Send, wait_input
|
||||
|
||||
class ITransport(ABC):
|
||||
@abstractmethod
|
||||
def enable_compression(self, enable: bool) -> None:
|
||||
"""
|
||||
Enables or disables data compression for the transport.
|
||||
|
||||
Args:
|
||||
enable (bool): If True, enable compression. If False, disable it.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def max_packet_size(self, size: int) -> None:
|
||||
"""
|
||||
Sets the maximum packet size for the transport.
|
||||
|
||||
Args:
|
||||
size (int): The maximum packet size in bytes.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def start_server(self) -> None:
|
||||
"""
|
||||
Starts the server for the transport, allowing it to accept incoming connections.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def accept(self, timeout: Union[int, None] = None) -> "IChannel":
|
||||
"""
|
||||
Accepts an incoming connection and returns an IChannel instance for communication.
|
||||
|
||||
Args:
|
||||
timeout (Union[int, None]): The time in seconds to wait for a connection.
|
||||
If None, waits indefinitely.
|
||||
|
||||
Returns:
|
||||
IChannel: An instance of IChannel representing the connection.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_subsystem_handler(self, name: str, handler: callable, *args: any, **kwargs: any) -> None:
|
||||
"""
|
||||
Sets a handler for a specific subsystem in the transport.
|
||||
|
||||
Args:
|
||||
name (str): The name of the subsystem.
|
||||
handler (callable): The handler function to be called for the subsystem.
|
||||
*args: Arguments to pass to the handler.
|
||||
**kwargs: Keyword arguments to pass to the handler.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def close(self) -> None:
|
||||
"""
|
||||
Closes the transport connection, releasing any resources used.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def is_authenticated(self) -> bool:
|
||||
"""
|
||||
Checks if the transport is authenticated.
|
||||
|
||||
Returns:
|
||||
bool: True if the transport is authenticated, otherwise False.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def getpeername(self) -> tuple[str, int]: # (host, port)
|
||||
"""
|
||||
Retrieves the peer's address and port.
|
||||
|
||||
Returns:
|
||||
tuple[str, int]: The host and port of the peer.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_username(self) -> str:
|
||||
"""
|
||||
Retrieves the username associated with the transport.
|
||||
|
||||
Returns:
|
||||
str: The username.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def is_active(self) -> bool:
|
||||
"""
|
||||
Checks if the transport is active.
|
||||
|
||||
Returns:
|
||||
bool: True if the transport is active, otherwise False.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_auth_method(self) -> str:
|
||||
"""
|
||||
Retrieves the authentication method used for the transport.
|
||||
|
||||
Returns:
|
||||
str: The authentication method (e.g., password, public key).
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_username(self, username: str) -> None:
|
||||
"""
|
||||
Sets the username for the transport.
|
||||
|
||||
Args:
|
||||
username (str): The username to be set.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_default_window_size(self) -> int:
|
||||
"""
|
||||
Retrieves the default window size for the transport.
|
||||
|
||||
Returns:
|
||||
int: The default window size.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_connection_type(self) -> str:
|
||||
"""
|
||||
Retrieves the type of connection for the transport.
|
||||
|
||||
Returns:
|
||||
str: The connection type (e.g., TCP, UDP).
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class IChannel(ABC):
|
||||
@abstractmethod
|
||||
def send(self, s: Union[bytes, bytearray]) -> None:
|
||||
"""
|
||||
Sends data over the channel.
|
||||
|
||||
Args:
|
||||
s (Union[bytes, bytearray]): The data to send.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def sendall(self, s: Union[bytes, bytearray]) -> None:
|
||||
"""
|
||||
Sends all data over the channel, blocking until all data is sent.
|
||||
|
||||
Args:
|
||||
s (Union[bytes, bytearray]): The data to send.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def getpeername(self) -> tuple[str, int]:
|
||||
"""
|
||||
Retrieves the peer's address and port.
|
||||
|
||||
Returns:
|
||||
tuple[str, int]: The host and port of the peer.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def settimeout(self, timeout: Union[float, None]) -> None:
|
||||
"""
|
||||
Sets the timeout for blocking operations on the channel.
|
||||
|
||||
Args:
|
||||
timeout (Union[float, None]): The timeout in seconds. If None, the operation will block indefinitely.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def setblocking(self, blocking: bool) -> None:
|
||||
"""
|
||||
Sets whether the channel operates in blocking mode or non-blocking mode.
|
||||
|
||||
Args:
|
||||
blocking (bool): If True, the channel operates in blocking mode. If False, non-blocking mode.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def recv(self, nbytes: int) -> bytes:
|
||||
"""
|
||||
Receives data from the channel.
|
||||
|
||||
Args:
|
||||
nbytes (int): The number of bytes to receive.
|
||||
|
||||
Returns:
|
||||
bytes: The received data.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_id(self) -> int:
|
||||
"""
|
||||
Retrieves the unique identifier for the channel.
|
||||
|
||||
Returns:
|
||||
int: The channel's unique identifier.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def close(self) -> None:
|
||||
"""
|
||||
Closes the channel and releases any resources used.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_out_window_size(self) -> int:
|
||||
"""
|
||||
Retrieves the output window size for the channel.
|
||||
|
||||
Returns:
|
||||
int: The output window size.
|
||||
"""
|
||||
pass
|
||||
|
||||
#--------------------------------------------------------------------------------------------
|
||||
|
||||
class SSHTransport(ITransport):
|
||||
def __init__(self, socketchannel: socket.socket, interface: Sinterface, key):
|
||||
self.socket: socket.socket = socketchannel
|
||||
self.interface: Sinterface = interface
|
||||
self.key = key
|
||||
|
||||
self.bh_session = paramiko.Transport(self.socket)
|
||||
self.bh_session.add_server_key(self.key)
|
||||
self.bh_session.default_window_size = 2147483647
|
||||
|
||||
def enable_compression(self, enable):
|
||||
self.bh_session.use_compression(enable)
|
||||
|
||||
def max_packet_size(self, size):
|
||||
self.bh_session.default_max_packet_size = size
|
||||
|
||||
def start_server(self):
|
||||
self.bh_session.start_server(server=self.interface)
|
||||
|
||||
def accept(self, timeout=None):
|
||||
return SSHChannel(self.bh_session.accept(timeout))
|
||||
|
||||
def set_subsystem_handler(self, name, handler, *args, **kwargs):
|
||||
self.bh_session.set_subsystem_handler(name, handler, *args, **kwargs)
|
||||
|
||||
def close(self):
|
||||
self.bh_session.close()
|
||||
|
||||
def is_authenticated(self):
|
||||
return self.bh_session.is_authenticated()
|
||||
|
||||
def getpeername(self):
|
||||
return self.bh_session.getpeername()
|
||||
|
||||
def get_username(self):
|
||||
return self.bh_session.get_username()
|
||||
|
||||
def is_active(self):
|
||||
return self.bh_session.is_active()
|
||||
|
||||
def get_auth_method(self):
|
||||
return self.bh_session.auth_handler.auth_method
|
||||
|
||||
def set_username(self, username):
|
||||
self.bh_session.auth_handler.username = username
|
||||
|
||||
def get_default_window_size(self):
|
||||
return self.bh_session.default_window_size
|
||||
|
||||
def get_connection_type(self):
|
||||
return "SSH"
|
||||
|
||||
class SSHChannel(IChannel):
|
||||
def __init__(self, channel: paramiko.Channel):
|
||||
self.channel: paramiko.Channel = channel
|
||||
|
||||
def send(self, s):
|
||||
self.channel.send(s)
|
||||
|
||||
def sendall(self, s):
|
||||
self.channel.sendall(s)
|
||||
|
||||
def getpeername(self):
|
||||
return self.channel.getpeername()
|
||||
|
||||
def settimeout(self, timeout):
|
||||
self.channel.settimeout(timeout)
|
||||
|
||||
def setblocking(self, blocking):
|
||||
self.channel.setblocking(blocking)
|
||||
|
||||
def recv(self, nbytes):
|
||||
return self.channel.recv(nbytes)
|
||||
|
||||
def get_id(self):
|
||||
return self.channel.get_id()
|
||||
|
||||
def close(self):
|
||||
self.channel.close()
|
||||
|
||||
def get_out_window_size(self):
|
||||
return self.channel.out_window_size
|
||||
|
||||
#--------------------------------------------------------------------------------------------
|
||||
|
||||
# Telnet command and option codes
|
||||
IAC = 255
|
||||
DO = 253
|
||||
WILL = 251
|
||||
TTYPE = 24
|
||||
ECHO = 1
|
||||
SGA = 3 # Suppress Go Ahead
|
||||
|
||||
def send_telnet_command(sock, command, option):
|
||||
sock.send(bytes([IAC, command, option]))
|
||||
|
||||
class TelnetTransport(ITransport):
|
||||
def __init__(self, socketchannel: socket.socket, interface: Sinterface):
|
||||
self.socket: socket.socket = socketchannel
|
||||
self.interface: Sinterface = interface
|
||||
self.username = None
|
||||
self.isactive = True
|
||||
self.isauth = False
|
||||
self.auth_method = None
|
||||
|
||||
def enable_compression(self, enable):
|
||||
pass
|
||||
|
||||
def max_packet_size(self, size):
|
||||
pass
|
||||
|
||||
def start_server(self):
|
||||
pass
|
||||
|
||||
def set_subsystem_handler(self, name: str, handler: callable, *args: any, **kwargs: any) -> None:
|
||||
pass
|
||||
|
||||
def negotiate_options(self):
|
||||
# Negotiating TTYPE (Terminal Type), ECHO, and SGA (Suppress Go Ahead)
|
||||
send_telnet_command(self.socket, DO, TTYPE)
|
||||
send_telnet_command(self.socket, WILL, ECHO)
|
||||
send_telnet_command(self.socket, WILL, SGA)
|
||||
|
||||
def accept(self, timeout=None):
|
||||
# Perform Telnet negotiation
|
||||
self.negotiate_options()
|
||||
|
||||
# Simple authentication prompt
|
||||
username = wait_input(self.socket, "Login as: ", directchannel=True)
|
||||
|
||||
try:
|
||||
allowauth = self.interface.get_allowed_auths(username).split(',')
|
||||
except:
|
||||
allowauth = self.interface.get_allowed_auths(username)
|
||||
|
||||
if allowauth[0] == "password":
|
||||
password = wait_input(self.socket, "Password", password=True, directchannel=True)
|
||||
result = self.interface.check_auth_password(username, password)
|
||||
|
||||
if result == 0:
|
||||
self.isauth = True
|
||||
self.username = username
|
||||
self.auth_method = "password"
|
||||
return TelnetChannel(self.socket)
|
||||
else:
|
||||
Send(self.socket, "Access denied", directchannel=True)
|
||||
self.close()
|
||||
elif allowauth[0] == "public_key":
|
||||
Send(self.socket, "Public key isn't supported for telnet", directchannel=True)
|
||||
self.close()
|
||||
elif allowauth[0] == "none":
|
||||
result = self.interface.check_auth_none(username)
|
||||
|
||||
if result == 0:
|
||||
self.username = username
|
||||
self.isauth = True
|
||||
self.auth_method = "none"
|
||||
return TelnetChannel(self.socket)
|
||||
else:
|
||||
Send(self.socket, "Access denied", directchannel=True)
|
||||
self.close()
|
||||
else:
|
||||
Send(self.socket, "Access denied", directchannel=True)
|
||||
|
||||
def close(self):
|
||||
self.isactive = False
|
||||
self.socket.close()
|
||||
|
||||
def is_authenticated(self):
|
||||
return self.isauth
|
||||
|
||||
def getpeername(self):
|
||||
return self.socket.getpeername()
|
||||
|
||||
def get_username(self):
|
||||
return self.username
|
||||
|
||||
def is_active(self):
|
||||
return self.isactive
|
||||
|
||||
def get_auth_method(self):
|
||||
return self.auth_method
|
||||
|
||||
def set_username(self, username):
|
||||
self.username = username
|
||||
|
||||
def get_default_window_size(self):
|
||||
return 0
|
||||
|
||||
def get_connection_type(self):
|
||||
return "Telnet"
|
||||
|
||||
|
||||
class TelnetChannel(IChannel):
|
||||
def __init__(self, channel: socket.socket):
|
||||
self.channel: socket.socket = channel
|
||||
|
||||
def send(self, s):
|
||||
self.channel.send(s)
|
||||
|
||||
def sendall(self, s):
|
||||
self.channel.sendall(s)
|
||||
|
||||
def getpeername(self):
|
||||
return self.channel.getpeername()
|
||||
|
||||
def settimeout(self, timeout):
|
||||
self.channel.settimeout(timeout)
|
||||
|
||||
def setblocking(self, blocking):
|
||||
self.channel.setblocking(blocking)
|
||||
|
||||
def recv(self, nbytes):
|
||||
return self.channel.recv(nbytes)
|
||||
|
||||
def get_id(self):
|
||||
return 0
|
||||
|
||||
def close(self) -> None:
|
||||
return self.channel.close()
|
||||
|
||||
def get_out_window_size(self) -> int:
|
||||
return 0
|
@ -1,8 +1,8 @@
|
||||
"""
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/damp11113/PyserSSH
|
||||
Copyright (C) 2023-2024 damp11113 (MIT)
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
Copyright (C) 2023-present DPSoftware Foundation (MIT)
|
||||
|
||||
Visit https://github.com/damp11113/PyserSSH
|
||||
Visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
|
||||
MIT License
|
||||
|
||||
@ -40,17 +40,20 @@ class SSHSFTPHandle(paramiko.SFTPHandle):
|
||||
# use the stored filename
|
||||
try:
|
||||
paramiko.SFTPServer.set_file_attr(self.filename, attr)
|
||||
return paramiko.SFTP_OK
|
||||
return paramiko.sftp.SFTP_OK
|
||||
except OSError as e:
|
||||
return paramiko.SFTPServer.convert_errno(e.errno)
|
||||
|
||||
class SSHSFTPServer(paramiko.SFTPServerInterface):
|
||||
ROOT = None
|
||||
ACCOUNT = None
|
||||
CLIENTHANDELES = None
|
||||
def __init__(self, server: paramiko.ServerInterface, *args, **kwargs):
|
||||
super().__init__(server)
|
||||
self.channel = args[0]
|
||||
self.account = args[1]
|
||||
self.clientH = args[2]
|
||||
|
||||
def _realpath(self, path):
|
||||
return self.ROOT + self.canonicalize(path)
|
||||
root = self.account.get_user_sftp_root_path(self.clientH[self.channel.getpeername()]["current_user"])
|
||||
return root + self.canonicalize(path)
|
||||
|
||||
def list_folder(self, path):
|
||||
path = self._realpath(path)
|
||||
@ -80,6 +83,12 @@ class SSHSFTPServer(paramiko.SFTPServerInterface):
|
||||
return paramiko.SFTPServer.convert_errno(e.errno)
|
||||
|
||||
def open(self, path, flags, attr):
|
||||
# check if write request
|
||||
is_write = (flags & os.O_WRONLY or flags & os.O_RDWR) + (flags & os.O_CREAT) != 0
|
||||
|
||||
if self.account.get_user_sftp_readonly(self.clientH[self.channel.getpeername()]["current_user"]) and is_write:
|
||||
return paramiko.sftp.SFTP_PERMISSION_DENIED
|
||||
|
||||
path = self._realpath(path)
|
||||
try:
|
||||
binary_flag = getattr(os, 'O_BINARY', 0)
|
||||
@ -120,23 +129,32 @@ class SSHSFTPServer(paramiko.SFTPServerInterface):
|
||||
return fobj
|
||||
|
||||
def remove(self, path):
|
||||
if self.account.get_user_sftp_readonly(self.clientH[self.channel.getpeername()]["current_user"]):
|
||||
return paramiko.sftp.SFTP_PERMISSION_DENIED
|
||||
|
||||
path = self._realpath(path)
|
||||
try:
|
||||
os.remove(path)
|
||||
except OSError as e:
|
||||
return paramiko.SFTPServer.convert_errno(e.errno)
|
||||
return paramiko.SFTP_OK
|
||||
return paramiko.sftp.SFTP_OK
|
||||
|
||||
def rename(self, oldpath, newpath):
|
||||
if self.account.get_user_sftp_readonly(self.clientH[self.channel.getpeername()]["current_user"]):
|
||||
return paramiko.sftp.SFTP_PERMISSION_DENIED
|
||||
|
||||
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
|
||||
return paramiko.sftp.SFTP_OK
|
||||
|
||||
def mkdir(self, path, attr):
|
||||
if self.account.get_user_sftp_readonly(self.clientH[self.channel.getpeername()]["current_user"]):
|
||||
return paramiko.sftp.SFTP_PERMISSION_DENIED
|
||||
|
||||
path = self._realpath(path)
|
||||
try:
|
||||
os.mkdir(path)
|
||||
@ -144,45 +162,58 @@ class SSHSFTPServer(paramiko.SFTPServerInterface):
|
||||
paramiko.SFTPServer.set_file_attr(path, attr)
|
||||
except OSError as e:
|
||||
return paramiko.SFTPServer.convert_errno(e.errno)
|
||||
return paramiko.SFTP_OK
|
||||
return paramiko.sftp.SFTP_OK
|
||||
|
||||
def rmdir(self, path):
|
||||
if self.account.get_user_sftp_readonly(self.clientH[self.channel.getpeername()]["current_user"]):
|
||||
return paramiko.sftp.SFTP_PERMISSION_DENIED
|
||||
|
||||
path = self._realpath(path)
|
||||
try:
|
||||
os.rmdir(path)
|
||||
except OSError as e:
|
||||
return paramiko.SFTPServer.convert_errno(e.errno)
|
||||
return paramiko.SFTP_OK
|
||||
return paramiko.sftp.SFTP_OK
|
||||
|
||||
def chattr(self, path, attr):
|
||||
if self.account.get_user_sftp_readonly(self.clientH[self.channel.getpeername()]["current_user"]):
|
||||
return paramiko.sftp.SFTP_PERMISSION_DENIED
|
||||
|
||||
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
|
||||
return paramiko.sftp.SFTP_OK
|
||||
|
||||
def symlink(self, target_path, path):
|
||||
if self.account.get_user_sftp_readonly(self.clientH[self.channel.getpeername()]["current_user"]):
|
||||
return paramiko.sftp.SFTP_PERMISSION_DENIED
|
||||
|
||||
root = self.account.get_user_sftp_root_path(self.clientH[self.channel.getpeername()]["current_user"])
|
||||
|
||||
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:])
|
||||
target_path = os.path.join(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:
|
||||
if abspath[:len(root)] != 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
|
||||
return paramiko.sftp.SFTP_OK
|
||||
|
||||
def readlink(self, path):
|
||||
root = self.account.get_user_sftp_root_path(self.clientH[self.channel.getpeername()]["current_user"])
|
||||
|
||||
path = self._realpath(path)
|
||||
try:
|
||||
symlink = os.readlink(path)
|
||||
@ -190,8 +221,8 @@ class SSHSFTPServer(paramiko.SFTPServerInterface):
|
||||
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 symlink[:len(root)] == root:
|
||||
symlink = symlink[len(root):]
|
||||
if (len(symlink) == 0) or (symlink[0] != '/'):
|
||||
symlink = '/' + symlink
|
||||
else:
|
||||
|
@ -1,8 +1,8 @@
|
||||
"""
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/damp11113/PyserSSH
|
||||
Copyright (C) 2023-2024 damp11113 (MIT)
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
Copyright (C) 2023-present DPSoftware Foundation (MIT)
|
||||
|
||||
Visit https://github.com/damp11113/PyserSSH
|
||||
Visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
|
||||
MIT License
|
||||
|
||||
|
293
src/PyserSSH/system/clientype.py
Normal file
293
src/PyserSSH/system/clientype.py
Normal file
@ -0,0 +1,293 @@
|
||||
"""
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
Copyright (C) 2023-present DPSoftware Foundation (MIT)
|
||||
|
||||
Visit https://github.com/DPSoftware-Foundation/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
|
||||
|
||||
from ..interactive import Send
|
||||
from .ProWrapper import IChannel, ITransport
|
||||
|
||||
class Client:
|
||||
def __init__(self, channel, transport, peername):
|
||||
"""
|
||||
Initializes a new client instance.
|
||||
|
||||
Args:
|
||||
channel (IChannel): The communication channel for the client.
|
||||
transport (ITransport): The transport layer for the client.
|
||||
peername (tuple): The peer's address and port (host, port).
|
||||
"""
|
||||
self.current_user = None
|
||||
self.transport: ITransport = transport
|
||||
self.channel: IChannel = channel
|
||||
self.subchannel = {}
|
||||
self.connecttype = None
|
||||
self.last_activity_time = None
|
||||
self.last_login_time = None
|
||||
self.windowsize = {}
|
||||
self.x11 = {}
|
||||
self.prompt = None
|
||||
self.inputbuffer = None
|
||||
self.peername = peername
|
||||
self.auth_method = self.transport.get_auth_method()
|
||||
self.session_id = None
|
||||
self.terminal_type = None
|
||||
self.env_variables = {}
|
||||
self.last_error = None
|
||||
self.last_command = None
|
||||
self.isexeccommandrunning = False
|
||||
|
||||
def get_id(self):
|
||||
"""
|
||||
Retrieves the client's session ID.
|
||||
|
||||
Returns:
|
||||
str or None: The session ID of the client.
|
||||
"""
|
||||
return self.session_id
|
||||
|
||||
def get_name(self):
|
||||
"""
|
||||
Retrieves the current username of the client.
|
||||
|
||||
Returns:
|
||||
str: The current username of the client.
|
||||
"""
|
||||
return self.current_user
|
||||
|
||||
def get_peername(self):
|
||||
"""
|
||||
Retrieves the peer's address (host, port) for the client.
|
||||
|
||||
Returns:
|
||||
tuple: The peer's address (host, port).
|
||||
"""
|
||||
return self.peername
|
||||
|
||||
def get_prompt(self):
|
||||
"""
|
||||
Retrieves the prompt string for the client.
|
||||
|
||||
Returns:
|
||||
str: The prompt string for the client.
|
||||
"""
|
||||
return self.prompt
|
||||
|
||||
def get_channel(self):
|
||||
"""
|
||||
Retrieves the communication channel for the client.
|
||||
|
||||
Returns:
|
||||
IChannel: The channel instance for the client.
|
||||
"""
|
||||
return self.channel
|
||||
|
||||
def get_prompt_buffer(self):
|
||||
"""
|
||||
Retrieves the current input buffer for the client as a string.
|
||||
|
||||
Returns:
|
||||
str: The input buffer as a string.
|
||||
"""
|
||||
return str(self.inputbuffer)
|
||||
|
||||
def get_terminal_size(self):
|
||||
"""
|
||||
Retrieves the terminal size (width, height) for the client.
|
||||
|
||||
Returns:
|
||||
tuple[int, int]: The terminal's width and height.
|
||||
"""
|
||||
return self.windowsize["width"], self.windowsize["height"]
|
||||
|
||||
def get_connection_type(self):
|
||||
"""
|
||||
Retrieves the connection type for the client.
|
||||
|
||||
Returns:
|
||||
str: The connection type (e.g., TCP, UDP).
|
||||
"""
|
||||
return self.connecttype
|
||||
|
||||
def get_auth_with(self):
|
||||
"""
|
||||
Retrieves the authentication method used for the client.
|
||||
|
||||
Returns:
|
||||
str: The authentication method (e.g., password, public key).
|
||||
"""
|
||||
return self.auth_method
|
||||
|
||||
def get_session_duration(self):
|
||||
"""
|
||||
Calculates the duration of the current session for the client.
|
||||
|
||||
Returns:
|
||||
float: The duration of the session in seconds.
|
||||
"""
|
||||
return time.time() - self.last_login_time
|
||||
|
||||
def get_environment(self, variable):
|
||||
"""
|
||||
Retrieves the value of an environment variable for the client.
|
||||
|
||||
Args:
|
||||
variable (str): The name of the environment variable.
|
||||
|
||||
Returns:
|
||||
str: The value of the environment variable.
|
||||
"""
|
||||
return self.env_variables.get(variable)
|
||||
|
||||
def get_last_error(self):
|
||||
"""
|
||||
Retrieves the last error message encountered by the client.
|
||||
|
||||
Returns:
|
||||
str: The last error message, or None if no error occurred.
|
||||
"""
|
||||
return self.last_error
|
||||
|
||||
def get_last_command(self):
|
||||
"""
|
||||
Retrieves the last command executed by the client.
|
||||
|
||||
Returns:
|
||||
str: The last command executed.
|
||||
"""
|
||||
return self.last_command
|
||||
|
||||
def set_name(self, name):
|
||||
"""
|
||||
Sets the current username for the client.
|
||||
|
||||
Args:
|
||||
name (str): The username to set for the client.
|
||||
"""
|
||||
self.current_user = name
|
||||
|
||||
def set_prompt(self, prompt):
|
||||
"""
|
||||
Sets the prompt string for the client.
|
||||
|
||||
Args:
|
||||
prompt (str): The prompt string to set for the client.
|
||||
"""
|
||||
self.prompt = prompt
|
||||
|
||||
def set_environment(self, variable, value):
|
||||
"""
|
||||
Sets the value of an environment variable for the client.
|
||||
|
||||
Args:
|
||||
variable (str): The name of the environment variable.
|
||||
value (str): The value to set for the environment variable.
|
||||
"""
|
||||
self.env_variables[variable] = value
|
||||
|
||||
def open_new_subchannel(self, timeout=None):
|
||||
"""
|
||||
Opens a new subchannel for communication with the client.
|
||||
|
||||
Args:
|
||||
timeout (Union[int, None]): The timeout duration in seconds.
|
||||
If None, the operation waits indefinitely.
|
||||
|
||||
Returns:
|
||||
tuple: A tuple containing the subchannel ID and the new subchannel
|
||||
(IChannel). If an error occurs, returns (None, None).
|
||||
"""
|
||||
try:
|
||||
channel = self.transport.accept(timeout)
|
||||
id = channel.get_id()
|
||||
except:
|
||||
return None, None
|
||||
|
||||
self.subchannel[id] = channel
|
||||
return id, channel
|
||||
|
||||
def get_subchannel(self, id):
|
||||
"""
|
||||
Retrieves a subchannel by its ID.
|
||||
|
||||
Args:
|
||||
id (int): The ID of the subchannel to retrieve.
|
||||
|
||||
Returns:
|
||||
IChannel: The subchannel instance.
|
||||
"""
|
||||
return self.subchannel.get(id)
|
||||
|
||||
def switch_user(self, user):
|
||||
"""
|
||||
Switches the current user for the client.
|
||||
|
||||
Args:
|
||||
user (str): The new username to switch to.
|
||||
"""
|
||||
self.current_user = user
|
||||
self.transport.set_username(user)
|
||||
|
||||
def close_subchannel(self, id):
|
||||
"""
|
||||
Closes a specific subchannel by its ID.
|
||||
|
||||
Args:
|
||||
id (int): The ID of the subchannel to close.
|
||||
"""
|
||||
self.subchannel[id].close()
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Closes the main communication channel for the client.
|
||||
"""
|
||||
self.channel.close()
|
||||
|
||||
def send(self, data):
|
||||
"""
|
||||
Sends data over the main communication channel.
|
||||
|
||||
Args:
|
||||
data (str): The data to send.
|
||||
"""
|
||||
Send(self.channel, data, directchannel=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"client id: {self.session_id}"
|
||||
|
||||
def __repr__(self):
|
||||
attrs = vars(self) # or self.__dict__
|
||||
|
||||
non_none_attrs = {key: value for key, value in attrs.items() if value is not None}
|
||||
|
||||
attrs_repr = ', '.join(f"{key}={value!r}" for key, value in non_none_attrs.items())
|
||||
return f"Client({attrs_repr})"
|
||||
|
||||
# for backward compatibility only
|
||||
def __getitem__(self, key):
|
||||
return getattr(self, key)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
setattr(self, key, value)
|
@ -1,8 +1,8 @@
|
||||
"""
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/damp11113/PyserSSH
|
||||
Copyright (C) 2023-2024 damp11113 (MIT)
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
Copyright (C) 2023-present DPSoftware Foundation (MIT)
|
||||
|
||||
Visit https://github.com/damp11113/PyserSSH
|
||||
Visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
|
||||
MIT License
|
||||
|
||||
@ -25,10 +25,32 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
__version__ = "4.2"
|
||||
version = "5.1.4"
|
||||
|
||||
system_banner = (
|
||||
f"\033[36mPyserSSH V{__version__} \033[0m\n"
|
||||
f"\033[36mPyserSSH V{version} \033[0m"
|
||||
#"\033[33m!!Warning!! This is Testing Version of PyserSSH \033[0m\n"
|
||||
"\033[35mUse Putty and WinSCP (SFTP) for best experience \033[0m"
|
||||
#"\033[35mUse Putty and WinSCP (SFTP) for best experience \033[0m"
|
||||
)
|
||||
|
||||
def Flag_TH(returnlist=False):
|
||||
Flags = [
|
||||
"\n",
|
||||
f"\033[31m ======= == == ====== ======= ====== ====== ====== == == \033[0m\n",
|
||||
f"\033[37m == == == == == === == == == == == == \033[0m\n",
|
||||
f"\033[34m ======= ==== ======= ======= ====== ======= ======= ======== \033[0m\n",
|
||||
f"\033[34m ===== == ===== ==== === == ===== ===== ======== \033[0m\n",
|
||||
f"\033[37m == == === === == == === === == == \033[0m\n",
|
||||
f"\033[31m == == ====== ======= == == ====== ====== == == \033[0m\n",
|
||||
" Made by \033[33mD\033[38;2;255;126;1mP\033[38;2;43;205;150mSoftware\033[0m \033[38;2;204;208;43mFoundation\033[0m from Thailand\n",
|
||||
"\n"
|
||||
]
|
||||
|
||||
if returnlist:
|
||||
return Flags
|
||||
else:
|
||||
exporttext = ""
|
||||
|
||||
for line in Flags:
|
||||
exporttext += line
|
||||
return exporttext
|
@ -1,8 +1,8 @@
|
||||
"""
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/damp11113/PyserSSH
|
||||
Copyright (C) 2023-2024 damp11113 (MIT)
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
Copyright (C) 2023-present DPSoftware Foundation (MIT)
|
||||
|
||||
Visit https://github.com/damp11113/PyserSSH
|
||||
Visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
|
||||
MIT License
|
||||
|
||||
@ -27,20 +27,20 @@ SOFTWARE.
|
||||
import socket
|
||||
import time
|
||||
import logging
|
||||
import shlex
|
||||
import traceback
|
||||
|
||||
from .sysfunc import replace_enter_with_crlf
|
||||
from .syscom import systemcommand
|
||||
|
||||
logger = logging.getLogger("PyserSSH")
|
||||
logger = logging.getLogger("PyserSSH.InputSystem")
|
||||
|
||||
def expect(self, chan, peername, echo=True):
|
||||
def expect(self, client, echo=True):
|
||||
buffer = bytearray()
|
||||
cursor_position = 0
|
||||
outindexall = 0
|
||||
history_index_position = 0 # Initialize history index position outside the loop
|
||||
currentuser = self.client_handlers[chan.getpeername()]
|
||||
chan = client["channel"]
|
||||
peername = client["peername"]
|
||||
|
||||
try:
|
||||
while True:
|
||||
try:
|
||||
@ -51,7 +51,7 @@ def expect(self, chan, peername, echo=True):
|
||||
chan.close()
|
||||
raise EOFError()
|
||||
|
||||
self._handle_event("onrawtype", self.client_handlers[chan.getpeername()], byte)
|
||||
self._handle_event("rawtype", self.client_handlers[chan.getpeername()], byte)
|
||||
|
||||
self.client_handlers[chan.getpeername()]["last_activity_time"] = time.time()
|
||||
|
||||
@ -66,7 +66,11 @@ def expect(self, chan, peername, echo=True):
|
||||
buffer = buffer[:cursor_position - 1] + buffer[cursor_position:]
|
||||
cursor_position -= 1
|
||||
outindexall -= 1
|
||||
chan.sendall(b"\b \b")
|
||||
if cursor_position != outindexall:
|
||||
chan.sendall(b"\b \b")
|
||||
chan.sendall(buffer[cursor_position:])
|
||||
else:
|
||||
chan.sendall(b"\b \b")
|
||||
else:
|
||||
chan.sendall(b"\x07")
|
||||
elif byte == b"\x1b" and chan.recv(1) == b'[':
|
||||
@ -76,18 +80,22 @@ def expect(self, chan, peername, echo=True):
|
||||
# Right arrow key, move cursor right if not at the end
|
||||
if cursor_position < len(buffer):
|
||||
chan.sendall(b'\x1b[C')
|
||||
cursor_position += 1
|
||||
# cursor_position += 1
|
||||
cursor_position = min(len(buffer), 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:
|
||||
# cursor_position -= 1
|
||||
cursor_position = max(0, cursor_position - 1)
|
||||
|
||||
if self.history:
|
||||
if arrow_key == b'A':
|
||||
if history_index_position == 0:
|
||||
command = self.accounts.get_lastcommand(currentuser["current_user"])
|
||||
command = self.accounts.get_lastcommand(client["current_user"])
|
||||
else:
|
||||
command = self.accounts.get_history(currentuser["current_user"], history_index_position)
|
||||
command = self.accounts.get_history(client["current_user"], history_index_position)
|
||||
|
||||
# Clear the buffer
|
||||
for i in range(cursor_position):
|
||||
@ -105,9 +113,9 @@ def expect(self, chan, peername, echo=True):
|
||||
elif arrow_key == b'B':
|
||||
if history_index_position != -1:
|
||||
if history_index_position == 0:
|
||||
command = self.accounts.get_lastcommand(currentuser["current_user"])
|
||||
command = self.accounts.get_lastcommand(client["current_user"])
|
||||
else:
|
||||
command = self.accounts.get_history(currentuser["current_user"], history_index_position)
|
||||
command = self.accounts.get_history(client["current_user"], history_index_position)
|
||||
|
||||
# Clear the buffer
|
||||
for i in range(cursor_position):
|
||||
@ -136,11 +144,13 @@ def expect(self, chan, peername, echo=True):
|
||||
else:
|
||||
history_index_position = -1
|
||||
|
||||
self._handle_event("ontype", self.client_handlers[chan.getpeername()], byte)
|
||||
self._handle_event("type", self.client_handlers[chan.getpeername()], byte)
|
||||
if echo:
|
||||
if outindexall != cursor_position:
|
||||
chan.sendall(b" ")
|
||||
chan.sendall(b'\033[s')
|
||||
chan.sendall(byte + buffer[cursor_position:])
|
||||
chan.sendall(f"\033[{cursor_position}G".encode())
|
||||
chan.sendall(b'\033[u')
|
||||
else:
|
||||
chan.sendall(byte)
|
||||
|
||||
@ -149,13 +159,16 @@ def expect(self, chan, peername, echo=True):
|
||||
cursor_position += 1
|
||||
outindexall += 1
|
||||
|
||||
client["inputbuffer"] = buffer
|
||||
|
||||
if echo:
|
||||
chan.sendall(b'\r\n')
|
||||
|
||||
command = str(buffer.decode('utf-8')).strip()
|
||||
|
||||
if self.history and command.strip() != "" and self.accounts.get_lastcommand(currentuser["current_user"]) != command:
|
||||
self.accounts.add_history(currentuser["current_user"], command)
|
||||
if self.history and command.strip() != "" and self.accounts.get_lastcommand(client["current_user"]) != command:
|
||||
self.accounts.add_history(client["current_user"], command)
|
||||
client["last_command"] = command
|
||||
|
||||
if command.strip() != "":
|
||||
if self.accounts.get_user_timeout(self.client_handlers[chan.getpeername()]["current_user"]) != None:
|
||||
@ -164,27 +177,27 @@ def expect(self, chan, peername, echo=True):
|
||||
|
||||
try:
|
||||
if self.enasyscom:
|
||||
sct = systemcommand(currentuser, command)
|
||||
sct = systemcommand(client, command, self)
|
||||
else:
|
||||
sct = False
|
||||
|
||||
if not sct:
|
||||
if self.XHandler != None:
|
||||
self._handle_event("beforexhandler", currentuser, command)
|
||||
self._handle_event("beforexhandler", client, command)
|
||||
|
||||
self.XHandler.call(currentuser, command)
|
||||
self.XHandler.call(client, command)
|
||||
|
||||
self._handle_event("afterxhandler", currentuser, command)
|
||||
self._handle_event("afterxhandler", client, command)
|
||||
else:
|
||||
self._handle_event("command", currentuser, command)
|
||||
self._handle_event("command", client, command)
|
||||
|
||||
except Exception as e:
|
||||
self._handle_event("error", currentuser, e)
|
||||
|
||||
try:
|
||||
chan.send(replace_enter_with_crlf(self.accounts.get_prompt(currentuser["current_user"]) + " ").encode('utf-8'))
|
||||
except:
|
||||
logger.error("Send error")
|
||||
self._handle_event("error", client, e)
|
||||
if echo:
|
||||
try:
|
||||
chan.send(replace_enter_with_crlf(client["prompt"] + " "))
|
||||
except:
|
||||
logger.error("Send error")
|
||||
|
||||
chan.setblocking(False)
|
||||
chan.settimeout(None)
|
||||
@ -192,14 +205,14 @@ def expect(self, chan, peername, echo=True):
|
||||
if self.accounts.get_user_timeout(self.client_handlers[chan.getpeername()]["current_user"]) != None:
|
||||
chan.setblocking(False)
|
||||
chan.settimeout(self.accounts.get_user_timeout(self.client_handlers[chan.getpeername()]["current_user"]))
|
||||
|
||||
except socket.error:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.error(str(e))
|
||||
finally:
|
||||
try:
|
||||
if not byte:
|
||||
logger.info(f"{peername} is disconnected")
|
||||
self._handle_event("disconnected", self.client_handlers[peername]["current_user"])
|
||||
return False
|
||||
return True
|
||||
except:
|
||||
logger.info(f"{peername} is disconnected by timeout")
|
||||
self._handle_event("timeout", self.client_handlers[peername]["current_user"])
|
||||
return False
|
@ -1,8 +1,8 @@
|
||||
"""
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/damp11113/PyserSSH
|
||||
Copyright (C) 2023-2024 damp11113 (MIT)
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
Copyright (C) 2023-present DPSoftware Foundation (MIT)
|
||||
|
||||
Visit https://github.com/damp11113/PyserSSH
|
||||
Visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
|
||||
MIT License
|
||||
|
||||
@ -24,35 +24,192 @@ 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 paramiko
|
||||
import ast
|
||||
|
||||
from .syscom import systemcommand
|
||||
from .RemoteStatus import startremotestatus
|
||||
|
||||
def parse_exec_request(command_string):
|
||||
try:
|
||||
# Remove the leading 'b' and convert bytes to string
|
||||
command_string = command_string.decode('utf-8')
|
||||
|
||||
# Split the string into precommand and env parts
|
||||
try:
|
||||
parts = command_string.split(', ')
|
||||
except:
|
||||
parts = command_string.split(',')
|
||||
|
||||
precommand_str = None
|
||||
env_str = None
|
||||
user_str = None
|
||||
|
||||
for part in parts:
|
||||
if part.startswith('precommand='):
|
||||
precommand_str = part.split('=', 1)[1].strip()
|
||||
elif part.startswith('env='):
|
||||
env_str = part.split('=', 1)[1].strip()
|
||||
elif part.startswith('user='):
|
||||
user_str = part.split('=', 1)[1].strip()
|
||||
|
||||
# Parse precommand using ast.literal_eval if present
|
||||
precommand = ast.literal_eval(precommand_str) if precommand_str else None
|
||||
|
||||
# Parse env using ast.literal_eval if present
|
||||
env = ast.literal_eval(env_str) if env_str else None
|
||||
|
||||
user = ast.literal_eval(user_str) if user_str else None
|
||||
|
||||
return precommand, env, user
|
||||
|
||||
except (ValueError, SyntaxError, TypeError) as e:
|
||||
# Handle parsing errors here
|
||||
print(f"Error parsing SSH command string: {e}")
|
||||
return None, None, None
|
||||
|
||||
def parse_exec_request_kwargs(command_string):
|
||||
try:
|
||||
# Remove the leading 'b' and convert bytes to string
|
||||
command_string = command_string.decode('utf-8')
|
||||
|
||||
# Split the string into key-value pairs
|
||||
try:
|
||||
parts = command_string.split(', ')
|
||||
except:
|
||||
parts = command_string.split(',')
|
||||
|
||||
kwargs = {}
|
||||
|
||||
for part in parts:
|
||||
if '=' in part:
|
||||
key, value = part.split('=', 1)
|
||||
key = key.strip()
|
||||
try:
|
||||
value = ast.literal_eval(value.strip())
|
||||
except (ValueError, SyntaxError):
|
||||
# If literal_eval fails, treat value as string
|
||||
value = value.strip()
|
||||
kwargs[key] = value
|
||||
|
||||
return kwargs
|
||||
|
||||
except (ValueError, SyntaxError, TypeError) as e:
|
||||
# Handle parsing errors here
|
||||
print(f"Error parsing command kwargs: {e}")
|
||||
return {}
|
||||
|
||||
class Sinterface(paramiko.ServerInterface):
|
||||
def __init__(self, serverself):
|
||||
self.current_user = None
|
||||
self.serverself = serverself
|
||||
|
||||
def check_channel_request(self, kind, chanid):
|
||||
def check_channel_request(self, kind, channel_id):
|
||||
if kind == 'session':
|
||||
return paramiko.OPEN_SUCCEEDED
|
||||
return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED
|
||||
|
||||
def get_allowed_auths(self, username):
|
||||
return self.serverself.accounts.get_allowed_auths(username)
|
||||
|
||||
def check_auth_password(self, username, password):
|
||||
data = {
|
||||
"username": username,
|
||||
"password": password,
|
||||
"auth_type": "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):
|
||||
self.current_user = username # Store the current user upon successful authentication
|
||||
return paramiko.AUTH_SUCCESSFUL
|
||||
else:
|
||||
return paramiko.AUTH_FAILED
|
||||
|
||||
def check_auth_none(self, username):
|
||||
data = {
|
||||
"username": username,
|
||||
"auth_type": "none"
|
||||
}
|
||||
|
||||
if self.serverself.accounts.validate_credentials(username) and not self.serverself.usexternalauth:
|
||||
return paramiko.AUTH_SUCCESSFUL
|
||||
else:
|
||||
if self.serverself._handle_event("auth", data):
|
||||
return paramiko.AUTH_SUCCESSFUL
|
||||
else:
|
||||
return paramiko.AUTH_FAILED
|
||||
|
||||
def check_auth_publickey(self, username, key):
|
||||
data = {
|
||||
"username": username,
|
||||
"public_key": key,
|
||||
"auth_type": "key"
|
||||
}
|
||||
|
||||
if self.serverself.accounts.validate_credentials(username, public_key=key) and not self.serverself.usexternalauth:
|
||||
return paramiko.AUTH_SUCCESSFUL
|
||||
else:
|
||||
if self.serverself._handle_event("auth", data):
|
||||
return paramiko.AUTH_SUCCESSFUL
|
||||
else:
|
||||
return paramiko.AUTH_FAILED
|
||||
|
||||
def get_banner(self):
|
||||
if self.serverself.enaloginbanner:
|
||||
try:
|
||||
banner, lang = self.serverself._handle_event("authbanner", None)
|
||||
return banner, lang
|
||||
except:
|
||||
return "", ""
|
||||
else:
|
||||
return "", ""
|
||||
|
||||
def check_channel_exec_request(self, channel, execommand):
|
||||
if b"##Moba##" in execommand and self.serverself.enaremostatus:
|
||||
startremotestatus(self.serverself, channel)
|
||||
|
||||
client = self.serverself.client_handlers[channel.getpeername()]
|
||||
|
||||
if self.serverself.enasysexec:
|
||||
precommand, env, user = parse_exec_request(execommand)
|
||||
|
||||
if env != None:
|
||||
client.env_variables = env
|
||||
|
||||
if user != None:
|
||||
self.serverself._handle_event("exec", client, user)
|
||||
|
||||
if precommand != None:
|
||||
client.isexeccommandrunning = True
|
||||
try:
|
||||
if self.serverself.enasyscom:
|
||||
sct = systemcommand(client, precommand)
|
||||
else:
|
||||
sct = False
|
||||
|
||||
if not sct:
|
||||
if self.serverself.XHandler != None:
|
||||
self.serverself._handle_event("beforexhandler", client, precommand)
|
||||
|
||||
self.serverself.XHandler.call(client, precommand)
|
||||
|
||||
self.serverself._handle_event("afterxhandler", client, precommand)
|
||||
else:
|
||||
self.serverself._handle_event("command", client, precommand)
|
||||
except Exception as e:
|
||||
self.serverself._handle_event("error", client, e)
|
||||
|
||||
client.isexeccommandrunning = False
|
||||
else:
|
||||
kwargs = parse_exec_request_kwargs(execommand)
|
||||
|
||||
self.serverself._handle_event("exec", client, **kwargs)
|
||||
|
||||
|
||||
return True
|
||||
|
||||
def check_channel_pty_request(self, channel, term, width, height, pixelwidth, pixelheight, modes):
|
||||
data = {
|
||||
"term": term,
|
||||
@ -69,7 +226,9 @@ class Sinterface(paramiko.ServerInterface):
|
||||
"pixelheight": pixelheight,
|
||||
}
|
||||
try:
|
||||
time.sleep(0.01) # fix waiting windowsize
|
||||
self.serverself.client_handlers[channel.getpeername()]["windowsize"] = data2
|
||||
self.serverself.client_handlers[channel.getpeername()]["terminal_type"] = term
|
||||
self.serverself._handle_event("connectpty", self.serverself.client_handlers[channel.getpeername()], data)
|
||||
except:
|
||||
pass
|
||||
@ -102,4 +261,4 @@ class Sinterface(paramiko.ServerInterface):
|
||||
"pixelheight": pixelheight
|
||||
}
|
||||
self.serverself.client_handlers[channel.getpeername()]["windowsize"] = data
|
||||
self.serverself._handle_event("resized", self.serverself.client_handlers[channel.getpeername()], data)
|
||||
self.serverself._handle_event("resized", self.serverself.client_handlers[channel.getpeername()], data)
|
272
src/PyserSSH/system/remotestatus.py
Normal file
272
src/PyserSSH/system/remotestatus.py
Normal file
@ -0,0 +1,272 @@
|
||||
"""
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
Copyright (C) 2023-present DPSoftware Foundation (MIT)
|
||||
|
||||
Visit https://github.com/DPSoftware-Foundation/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 logging
|
||||
import os
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
import sys
|
||||
import psutil
|
||||
from datetime import datetime
|
||||
import platform
|
||||
|
||||
from ..interactive import Send
|
||||
from .info import version
|
||||
|
||||
if platform.system() == "Windows":
|
||||
import ctypes
|
||||
|
||||
logger = logging.getLogger("PyserSSH.RemoteStatus")
|
||||
|
||||
if platform.system() == "Windows":
|
||||
class LASTINPUTINFO(ctypes.Structure):
|
||||
_fields_ = [
|
||||
('cbSize', ctypes.c_uint),
|
||||
('dwTime', ctypes.c_uint),
|
||||
]
|
||||
|
||||
def get_idle_time():
|
||||
if platform.system() == "Windows":
|
||||
lastInputInfo = LASTINPUTINFO()
|
||||
lastInputInfo.cbSize = ctypes.sizeof(lastInputInfo)
|
||||
ctypes.windll.user32.GetLastInputInfo(ctypes.byref(lastInputInfo))
|
||||
millis = ctypes.windll.kernel32.GetTickCount() - lastInputInfo.dwTime
|
||||
return millis / 1000.0
|
||||
elif platform.system() == "Linux":
|
||||
with open('/proc/stat') as f:
|
||||
for line in f:
|
||||
if line.startswith('btime'):
|
||||
boot_time = float(line.split()[1])
|
||||
break
|
||||
|
||||
with open('/proc/uptime') as f:
|
||||
uptime_seconds = float(f.readline().split()[0])
|
||||
idle_time_seconds = uptime_seconds - (time.time() - boot_time)
|
||||
|
||||
return idle_time_seconds
|
||||
else:
|
||||
return time.time() - psutil.boot_time()
|
||||
|
||||
def get_system_uptime():
|
||||
if platform.system() == "Windows":
|
||||
kernel32 = ctypes.windll.kernel32
|
||||
uptime = kernel32.GetTickCount64() / 1000.0
|
||||
return uptime
|
||||
elif platform.system() == "Linux":
|
||||
with open('/proc/uptime') as f:
|
||||
uptime_seconds = float(f.readline().split()[0])
|
||||
|
||||
return uptime_seconds
|
||||
else:
|
||||
return 0
|
||||
|
||||
def get_folder_size(folder_path):
|
||||
total_size = 0
|
||||
for dirpath, _, filenames in os.walk(folder_path):
|
||||
for f in filenames:
|
||||
fp = os.path.join(dirpath, f)
|
||||
total_size += os.path.getsize(fp)
|
||||
return total_size
|
||||
|
||||
def get_folder_usage(folder_path, limit_size):
|
||||
folder_size = get_folder_size(folder_path)
|
||||
used_size = folder_size
|
||||
free_size = limit_size - folder_size
|
||||
percent_used = (folder_size / limit_size) * 100 if limit_size > 0 else 0
|
||||
return used_size, free_size, limit_size, percent_used
|
||||
|
||||
librarypath = os.path.abspath(__file__).replace("\\", "/").split("/system/remotestatus.py")[0]
|
||||
|
||||
def remotestatus(serverself, channel, oneloop=False):
|
||||
try:
|
||||
while True:
|
||||
# Get RAM information
|
||||
mem = psutil.virtual_memory()
|
||||
|
||||
ramoutput = f"""\
|
||||
==> /proc/meminfo <==
|
||||
MemTotal: {mem.total // 1024} kB
|
||||
MemFree: {mem.free // 1024} kB
|
||||
MemAvailable: {mem.available // 1024} kB
|
||||
Buffers: 0 kB
|
||||
Cached: 0 kB
|
||||
SwapCached: 0 kB
|
||||
Active: 0 kB
|
||||
Inactive: 0 kB"""
|
||||
|
||||
cpu_data = []
|
||||
|
||||
#currentprocess = psutil.Process().cpu_times()
|
||||
|
||||
#cpu_data.append(["cpu", int(currentprocess.user), 0, int(currentprocess.system), 0, 0, 0, 0])
|
||||
|
||||
#if platform.system() == "Linux":
|
||||
io_counters = psutil.disk_io_counters(perdisk=False)
|
||||
io_wait_time = io_counters.read_time + io_counters.write_time
|
||||
for idx, cpu_time in enumerate(psutil.cpu_times(True), start=-1):
|
||||
if idx == -1:
|
||||
cpu_data.append(["cpu", int(cpu_time.user), 0, int(cpu_time.system), int(cpu_time.idle), io_wait_time, int(cpu_time.interrupt), 0, 0, 0, 0])
|
||||
else:
|
||||
cpu_data.append(
|
||||
[f"cpu{idx}", int(cpu_time.user), 0, int(cpu_time.system), int(cpu_time.idle), io_wait_time, int(cpu_time.interrupt), 0, 0, 0, 0])
|
||||
|
||||
# Calculate maximum widths for formatting (optional)
|
||||
max_widths = [max(len(str(row[i])) for row in cpu_data) for i in range(len(cpu_data[0]))]
|
||||
|
||||
disk_data = [
|
||||
["Filesystem", "1K-blocks", "Used", "Available", "Use%", "Mounted on"],
|
||||
]
|
||||
|
||||
for disk in psutil.disk_partitions(True):
|
||||
usage = psutil.disk_usage(disk.device)
|
||||
mountpoint = disk.mountpoint
|
||||
|
||||
if mountpoint == "C:\\":
|
||||
mountpoint = "/"
|
||||
|
||||
disk_data.append(
|
||||
[disk.device.replace('\\', '/'), usage.total // 1024, usage.used // 1024, usage.free // 1024, f"{int(usage.percent)}%",
|
||||
mountpoint])
|
||||
|
||||
libused, libfree, libtotal, libpercent = get_folder_usage(librarypath, 1024*1024)
|
||||
|
||||
disk_data.append(["/dev/pyserssh", libtotal // 1024, libused // 1024, libfree // 1024, f"{int(libpercent)}%", "/python/pyserssh"])
|
||||
|
||||
max_widths3 = [max(len(str(row[i])) for row in disk_data) for i in range(len(disk_data[0]))]
|
||||
|
||||
"""
|
||||
network_data = [
|
||||
["Inter-|", " Receive", "", "", "", "", "", "", " |", " Transmit" "", "", "", "", "", "", "", ""],
|
||||
[" face |", "bytes", "packets", "errs", "drop", "fifo", "frame", "compressed", "multicast|", "bytes", "packets",
|
||||
"errs", "drop", "fifo", "colls", "carrier", "compressed"]
|
||||
]
|
||||
|
||||
for interface, stats in psutil.net_io_counters(pernic=True).items():
|
||||
network_data.append(
|
||||
[f"{interface}:", stats.bytes_recv, stats.packets_recv, stats.errin, stats.dropin, 0, 0, 0, 0,
|
||||
stats.bytes_sent, stats.packets_sent, stats.errout, stats.dropout, 0, 0, 0, 0])
|
||||
|
||||
max_widths2 = [max(len(str(row[i])) for row in network_data) for i in range(len(network_data[0]))]
|
||||
|
||||
protocol_names = {
|
||||
(socket.AF_INET, socket.SOCK_STREAM): 'tcp',
|
||||
(socket.AF_INET, socket.SOCK_DGRAM): 'udp',
|
||||
(socket.AF_INET6, socket.SOCK_STREAM): 'tcp6',
|
||||
(socket.AF_INET6, socket.SOCK_DGRAM): 'udp6',
|
||||
}
|
||||
|
||||
netstat_data = [
|
||||
["Proto", "Recv-Q", "Send-Q", "Local Address", "Foreign Address", "State", "PID/Program name"],
|
||||
]
|
||||
|
||||
for conn in psutil.net_connections("all"):
|
||||
if conn.status in ['TIME_WAIT', 'CLOSING', "NONE"]:
|
||||
continue
|
||||
|
||||
laddr_ip, laddr_port = conn.laddr if conn.laddr else ('', '')
|
||||
raddr_ip, raddr_port = conn.raddr if conn.raddr else ('', '')
|
||||
|
||||
protocol = protocol_names.get((conn.family, conn.type), 'Unknown')
|
||||
|
||||
try:
|
||||
process = psutil.Process(conn.pid)
|
||||
processname = f"{conn.pid}/{process.name()}"
|
||||
except psutil.NoSuchProcess:
|
||||
processname = conn.pid
|
||||
|
||||
netstat_data.append(
|
||||
[protocol, 0, 0, f"{laddr_ip}:{laddr_port}", f"{raddr_ip}:{raddr_port}", conn.status, processname])
|
||||
|
||||
max_widths4 = [max(len(str(row[i])) for row in netstat_data) for i in range(len(netstat_data[0]))]
|
||||
"""
|
||||
|
||||
who_data = []
|
||||
|
||||
for idx, client in enumerate(serverself.client_handlers.values()):
|
||||
last_login_date = datetime.utcfromtimestamp(client.last_login_time).strftime('%Y-%m-%d %H:%M')
|
||||
who_data.append([client.current_user, f"pty/{idx}", last_login_date, f"({client.peername[0]})"])
|
||||
|
||||
max_widths5 = [max(len(str(row[i])) for row in who_data) for i in range(len(who_data[0]))]
|
||||
|
||||
Send(channel, ramoutput, directchannel=True)
|
||||
Send(channel, "", directchannel=True)
|
||||
|
||||
# only support for CPU status current python process
|
||||
Send(channel, "==> /proc/stat <==", directchannel=True)
|
||||
for row in cpu_data:
|
||||
Send(channel, " ".join("{:<{width}}".format(item, width=max_widths[i]) for i, item in enumerate(row)), directchannel=True)
|
||||
|
||||
Send(channel, "", directchannel=True)
|
||||
Send(channel, "==> /proc/version <==", directchannel=True)
|
||||
Send(channel, f"PyserSSH v{version} run on {platform.platform()} {platform.machine()} {platform.architecture()[0]} with python {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro} {sys.version_info.releaselevel} {platform.python_build()[0]} {platform.python_build()[1]} {platform.python_compiler()} {platform.python_implementation()} {platform.python_revision()}", directchannel=True)
|
||||
|
||||
Send(channel, "", directchannel=True)
|
||||
Send(channel, "==> /proc/uptime <==", directchannel=True)
|
||||
Send(channel, f"{get_system_uptime()} {get_idle_time()}", directchannel=True)
|
||||
|
||||
Send(channel, "", directchannel=True)
|
||||
Send(channel, "==> /proc/sys/kernel/hostname <==", directchannel=True)
|
||||
Send(channel, platform.node(), directchannel=True)
|
||||
|
||||
# fixing later for network status
|
||||
#Send(channel, "", directchannel=True)
|
||||
#Send(channel, "==> /proc/net/dev <==", directchannel=True)
|
||||
#for row in network_data:
|
||||
# Send(channel, " ".join("{:<{width}}".format(item, width=max_widths2[i]) for i, item in enumerate(row)), directchannel=True)
|
||||
|
||||
Send(channel, "", directchannel=True)
|
||||
Send(channel, "==> /proc/df <==", directchannel=True)
|
||||
for row in disk_data:
|
||||
Send(channel, " ".join("{:<{width}}".format(item, width=max_widths3[i]) for i, item in enumerate(row)), directchannel=True)
|
||||
|
||||
# fixing later for network status
|
||||
#Send(channel, "", directchannel=True)
|
||||
#Send(channel, "==> /proc/netstat <==", directchannel=True)
|
||||
#for row in netstat_data:
|
||||
# Send(channel, " ".join("{:<{width}}".format(item, width=max_widths4[i]) for i, item in enumerate(row)), directchannel=True)
|
||||
|
||||
Send(channel, "", directchannel=True)
|
||||
Send(channel, "==> /proc/who <==", directchannel=True)
|
||||
for row in who_data:
|
||||
Send(channel, " ".join("{:<{width}}".format(item, width=max_widths5[i]) for i, item in enumerate(row)), directchannel=True)
|
||||
|
||||
Send(channel, "", directchannel=True)
|
||||
Send(channel, "==> /proc/end <==", directchannel=True)
|
||||
Send(channel, "##Moba##", directchannel=True)
|
||||
|
||||
if oneloop:
|
||||
break
|
||||
|
||||
time.sleep(1)
|
||||
except socket.error:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
def startremotestatus(serverself, channel):
|
||||
t = threading.Thread(target=remotestatus, args=(serverself, channel), daemon=True)
|
||||
t.start()
|
@ -1,8 +1,8 @@
|
||||
"""
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/damp11113/PyserSSH
|
||||
Copyright (C) 2023-2024 damp11113 (MIT)
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
Copyright (C) 2023-present DPSoftware Foundation (MIT)
|
||||
|
||||
Visit https://github.com/damp11113/PyserSSH
|
||||
Visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
|
||||
MIT License
|
||||
|
||||
@ -26,21 +26,56 @@ SOFTWARE.
|
||||
"""
|
||||
import shlex
|
||||
|
||||
from ..interactive import *
|
||||
from ..interactive import Send, Clear, Title, wait_choose
|
||||
|
||||
def systemcommand(client, command):
|
||||
channel = client["channel"]
|
||||
def system_account_command(client, accounts, action):
|
||||
banner = "accman adduser <username> <password>\naccman deluser <username>\naccman passwd <username> <new password>\naccman list"
|
||||
try:
|
||||
if action[0] == "adduser":
|
||||
accounts.add_account(action[1], action[2])
|
||||
Send(client, f"Added {action[1]}")
|
||||
elif action[0] == "deluser":
|
||||
if accounts.has_user(action[1]):
|
||||
if not accounts.is_user_has_sudo(action[1]):
|
||||
if wait_choose(client, ["No", "Yes"], prompt="Sure? ") == 1:
|
||||
accounts.remove_account(action[1])
|
||||
Send(client, f"Removed {action[1]}")
|
||||
else:
|
||||
Send(client, f"{action[1]} isn't removable.")
|
||||
else:
|
||||
Send(client, f"{action[1]} not found")
|
||||
elif action[0] == "passwd":
|
||||
if accounts.has_user(action[1]):
|
||||
accounts.change_password(action[1], action[2])
|
||||
Send(client, f"Password updated successfully.")
|
||||
else:
|
||||
Send(client, f"{action[1]} not found")
|
||||
elif action[0] == "list":
|
||||
for user in accounts.list_users():
|
||||
Send(client, user)
|
||||
else:
|
||||
Send(client, banner)
|
||||
except:
|
||||
Send(client, banner)
|
||||
|
||||
def systemcommand(client, command, serverself):
|
||||
if command == "whoami":
|
||||
Send(channel, client["current_user"])
|
||||
Send(client, client["current_user"])
|
||||
return True
|
||||
elif command.startswith("title"):
|
||||
args = shlex.split(command)
|
||||
title = args[1]
|
||||
Title(client, title)
|
||||
return True
|
||||
elif command.startswith("accman"):
|
||||
args = shlex.split(command)
|
||||
if serverself.accounts.is_user_has_sudo(client.current_user):
|
||||
system_account_command(client, serverself.accounts, args[1:])
|
||||
else:
|
||||
Send(client, "accman: Permission denied.")
|
||||
return True
|
||||
elif command == "exit":
|
||||
channel.close()
|
||||
client["channel"].close()
|
||||
return True
|
||||
elif command == "clear":
|
||||
Clear(client)
|
||||
|
@ -1,8 +1,8 @@
|
||||
"""
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/damp11113/PyserSSH
|
||||
Copyright (C) 2023-2024 damp11113 (MIT)
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
Copyright (C) 2023-present DPSoftware Foundation (MIT)
|
||||
|
||||
Visit https://github.com/damp11113/PyserSSH
|
||||
Visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
|
||||
MIT License
|
||||
|
||||
@ -26,10 +26,21 @@ SOFTWARE.
|
||||
"""
|
||||
|
||||
def replace_enter_with_crlf(input_string):
|
||||
if '\n' in input_string:
|
||||
if isinstance(input_string, str):
|
||||
# Replace '\n' with '\r\n' in the string
|
||||
input_string = input_string.replace('\n', '\r\n')
|
||||
return input_string
|
||||
# Encode the string to bytes
|
||||
return input_string.encode()
|
||||
elif isinstance(input_string, bytes):
|
||||
# Decode bytes to string
|
||||
decoded_string = input_string.decode()
|
||||
# Replace '\n' with '\r\n' in the string
|
||||
modified_string = decoded_string.replace('\n', '\r\n')
|
||||
# Encode the modified string back to bytes
|
||||
|
||||
return modified_string.encode()
|
||||
else:
|
||||
raise TypeError("Input must be a string or bytes")
|
||||
|
||||
def text_centered_screen(text, screen_width, screen_height, spacecharacter=" "):
|
||||
screen = []
|
||||
|
174
src/PyserSSH/utils/ServerManager.py
Normal file
174
src/PyserSSH/utils/ServerManager.py
Normal file
@ -0,0 +1,174 @@
|
||||
import time
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger("PyserSSH.Utils.ServerManager")
|
||||
|
||||
class ServerManager:
|
||||
def __init__(self):
|
||||
self.servers = {}
|
||||
|
||||
def add_server(self, name, server, *args, **kwargs):
|
||||
"""
|
||||
Adds a server to the manager with the specified name. Raises an error if a server with the same name already exists.
|
||||
|
||||
Args:
|
||||
name (str): The name of the server.
|
||||
server (object): The server instance to be added.
|
||||
*args: Arguments for server initialization.
|
||||
**kwargs: Keyword arguments for server initialization.
|
||||
|
||||
Raises:
|
||||
ValueError: If a server with the same name already exists.
|
||||
"""
|
||||
if name in self.servers:
|
||||
raise ValueError(f"Server with name '{name}' already exists.")
|
||||
self.servers[name] = {"server": server, "args": args, "kwargs": kwargs, "status": "stopped"}
|
||||
logger.info(f"Server '{name}' added.")
|
||||
|
||||
def remove_server(self, name):
|
||||
"""
|
||||
Removes a server from the manager by name. Raises an error if the server does not exist.
|
||||
|
||||
Args:
|
||||
name (str): The name of the server to be removed.
|
||||
|
||||
Raises:
|
||||
ValueError: If no server with the specified name exists.
|
||||
"""
|
||||
if name not in self.servers:
|
||||
raise ValueError(f"No server found with name '{name}'.")
|
||||
del self.servers[name]
|
||||
logger.info(f"Server '{name}' removed.")
|
||||
|
||||
def get_server(self, name):
|
||||
"""
|
||||
Retrieves a server by its name.
|
||||
|
||||
Args:
|
||||
name (str): The name of the server to retrieve.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing the server instance, arguments, keyword arguments, and its status, or None if the server is not found.
|
||||
"""
|
||||
return self.servers.get(name, None)
|
||||
|
||||
def start_server(self, name):
|
||||
"""
|
||||
Starts a server with the specified name if it is not already running. Blocks until the server starts.
|
||||
|
||||
Args:
|
||||
name (str): The name of the server to start.
|
||||
|
||||
Raises:
|
||||
ValueError: If no server with the specified name exists or the server cannot be started.
|
||||
"""
|
||||
server_info = self.get_server(name)
|
||||
if not server_info:
|
||||
raise ValueError(f"No server found with name '{name}'.")
|
||||
|
||||
if server_info["status"] == "running":
|
||||
logger.info(f"Server '{name}' is already running.")
|
||||
return
|
||||
|
||||
server = server_info["server"]
|
||||
args, kwargs = server_info["args"], server_info["kwargs"]
|
||||
|
||||
logger.info(f"Starting server '{name}' with arguments {args}...")
|
||||
server_info["status"] = "starting"
|
||||
server.run(*args, **kwargs)
|
||||
|
||||
while not server.isrunning:
|
||||
logger.debug(f"Waiting for server '{name}' to start...")
|
||||
time.sleep(0.1)
|
||||
|
||||
server_info["status"] = "running"
|
||||
logger.info(f"Server '{name}' is now running.")
|
||||
|
||||
def stop_server(self, name):
|
||||
"""
|
||||
Stops a server with the specified name if it is running. Blocks until the server stops.
|
||||
|
||||
Args:
|
||||
name (str): The name of the server to stop.
|
||||
|
||||
Raises:
|
||||
ValueError: If no server with the specified name exists or the server cannot be stopped.
|
||||
"""
|
||||
server_info = self.get_server(name)
|
||||
if not server_info:
|
||||
raise ValueError(f"No server found with name '{name}'.")
|
||||
|
||||
if server_info["status"] == "stopped":
|
||||
logger.info(f"Server '{name}' is already stopped.")
|
||||
return
|
||||
|
||||
server = server_info["server"]
|
||||
|
||||
logger.info(f"Shutting down server '{name}'...")
|
||||
server_info["status"] = "shutting down"
|
||||
server.stop_server()
|
||||
|
||||
while server.isrunning:
|
||||
logger.debug(f"Waiting for server '{name}' to shut down...")
|
||||
time.sleep(0.1)
|
||||
|
||||
server_info["status"] = "stopped"
|
||||
logger.info(f"Server '{name}' has been stopped.")
|
||||
|
||||
def start_all_servers(self):
|
||||
"""
|
||||
Starts all servers managed by the ServerManager. Blocks until each server starts.
|
||||
"""
|
||||
for name, server_info in self.servers.items():
|
||||
if server_info["status"] == "running":
|
||||
logger.info(f"Server '{name}' is already running.")
|
||||
continue
|
||||
server, args, kwargs = server_info["server"], server_info["args"], server_info["kwargs"]
|
||||
logger.info(f"Starting server '{name}' with arguments {args}...")
|
||||
server_info["status"] = "starting"
|
||||
server.run(*args, **kwargs)
|
||||
|
||||
while not server.isrunning:
|
||||
logger.debug(f"Waiting for server '{name}' to start...")
|
||||
time.sleep(0.1)
|
||||
|
||||
server_info["status"] = "running"
|
||||
logger.info(f"Server '{name}' is now running.")
|
||||
|
||||
def stop_all_servers(self):
|
||||
"""
|
||||
Stops all servers managed by the ServerManager. Blocks until each server stops.
|
||||
"""
|
||||
for name, server_info in self.servers.items():
|
||||
if server_info["status"] == "stopped":
|
||||
logger.info(f"Server '{name}' is already stopped.")
|
||||
continue
|
||||
server = server_info["server"]
|
||||
logger.info(f"Shutting down server '{name}'...")
|
||||
server_info["status"] = "shutting down"
|
||||
server.stop_server()
|
||||
|
||||
while server.isrunning:
|
||||
logger.debug(f"Waiting for server '{name}' to shut down...")
|
||||
time.sleep(0.1)
|
||||
|
||||
server_info["status"] = "stopped"
|
||||
logger.info(f"Server '{name}' has been stopped.")
|
||||
|
||||
def get_status(self, name):
|
||||
"""
|
||||
Retrieves the status of a server by name.
|
||||
|
||||
Args:
|
||||
name (str): The name of the server to get the status of.
|
||||
|
||||
Returns:
|
||||
str: The current status of the server (e.g., 'running', 'stopped', etc.).
|
||||
|
||||
Raises:
|
||||
ValueError: If no server with the specified name exists.
|
||||
"""
|
||||
server_info = self.get_server(name)
|
||||
if not server_info:
|
||||
raise ValueError(f"No server found with name '{name}'.")
|
||||
return server_info["status"]
|
@ -1,8 +1,8 @@
|
||||
"""
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/damp11113/PyserSSH
|
||||
Copyright (C) 2023-2024 damp11113 (MIT)
|
||||
PyserSSH - A Scriptable SSH server. For more info visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
Copyright (C) 2023-present DPSoftware Foundation (MIT)
|
||||
|
||||
Visit https://github.com/damp11113/PyserSSH
|
||||
Visit https://github.com/DPSoftware-Foundation/PyserSSH
|
||||
|
||||
MIT License
|
||||
|
||||
@ -25,6 +25,14 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
class PTOP:
|
||||
def __init__(self, client, interval=1):
|
||||
pass # working
|
||||
"""
|
||||
note
|
||||
|
||||
ansi cursor arrow
|
||||
up - \x1b[A
|
||||
down - \x1b[B
|
||||
left - \x1b[D
|
||||
right - \x1b[C
|
||||
|
||||
https://en.wikipedia.org/wiki/ANSI_escape_code
|
||||
"""
|
22
src/PyserSSH/utils/keygen.py
Normal file
22
src/PyserSSH/utils/keygen.py
Normal file
@ -0,0 +1,22 @@
|
||||
import paramiko
|
||||
|
||||
def generate_ssh_keypair(private_key_path='id_rsa', public_key_path='id_rsa.pub', key_size=2048):
|
||||
"""
|
||||
Generates an SSH key pair (private and public) and saves them to specified files.
|
||||
|
||||
Args:
|
||||
- private_key_path (str): Path to save the private key.
|
||||
- public_key_path (str): Path to save the public key.
|
||||
- key_size (int): Size of the RSA key (default is 2048).
|
||||
"""
|
||||
# Generate RSA key pair
|
||||
private_key = paramiko.RSAKey.generate(key_size)
|
||||
|
||||
# Save the private key to a file
|
||||
private_key.write_private_key_file(private_key_path)
|
||||
|
||||
# Save the public key to a file
|
||||
with open(public_key_path, 'w') as pub_file:
|
||||
pub_file.write(f"{private_key.get_name()} {private_key.get_base64()}")
|
||||
|
||||
print(f"SSH Key pair generated: {private_key_path} and {public_key_path}")
|
@ -9,4 +9,5 @@ python setup.py sdist
|
||||
title uploading to pypi
|
||||
twine upload -r pypi dist/*
|
||||
|
||||
title done!
|
||||
pause
|
Loading…
x
Reference in New Issue
Block a user