如何让paramiko SSH服务器执行命令?

How to make paramiko SSH server execute cammands?

我正在尝试编写一个 SSH 服务器,一切正常,但问题似乎是我无法让客户端正常在服务器上执行命令,而且找不到正确的方法来执行此操作,因为没有提及它在文档中,看不到如何让服务器接受连接的演示示例,所以我完全迷失在这个领域。代码是:

#!/bin/python3
import paramiko
import socket    

class Ctx(paramiko.server.ServerInterface):
    
    def get_allowed_auths(self, username):  return "password,publickey"
    def check_auth_publickey(self, key):    return paramiko.AUTH_SUCCESSFUL
    def check_channel_request(self, kind, channelID): return paramiko.OPEN_SUCCEEDED
    def check_channel_shell_request(self, channel):  return True
    def check_channel_pty_request(self, c, t, w, h, p, ph, m): return True
    def get_banner(self):         return ("This is MY SSH Server\n\r", "EN")

    def check_channel_exec_request(self, channel, command):
        print(command)      # Print command 
        self.event.set()    # I dont know why this is used.
        return True         # return True to accept command exec request

    def check_auth_password(self, username, password):
        if password == "1999":    return paramiko.AUTH_SUCCESSFUL
        else:                     return paramiko.AUTH_FAILED

paramiko.util.log_to_file("demo_server.log")    # setup log file
host_key = paramiko.RSAKey(filename="./rsa")    # setup rsa key file that will be used during authnitication
ctx = Ctx()                                     # create ServerInterface context object
sock = socket.socket()                          # Create socket object
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(("127.0.0.1", 5555))              # bind socket to specific Port
sock.listen(100)                            # Listen for TCP connections
print("***************** Listening for connection **************************")
client, addr = sock.accept()                # accept TCP socket connection
print("[+]*****************  Listeing for SSH connections ***************** ")
server = paramiko.Transport(client)
server.add_server_key(host_key)             # Setup key
server.start_server(server=ctx)             # SSH start_server
channel = server.accept(30)                 # Accept Auth requests
if channel is None:
    print("[+] *****************  No Auth request Was made. ***************** ")
    exit(1)
channel.send("[+]*****************  Welcome ***************** \n\r")
while True:                                 # This is supposed to be used to listen to commands
    channel.event.wait(5)                   # but I'm not sure what it does actually

正如您在 check_channel_exec_request 中的 print 语句的输出中所看到的,您正在接收一个命令名称。您只需要执行命令并将输出发送到客户端。一种实现方式可能如下所示:

def check_channel_exec_request(self, channel, command):
    try:
        res = subprocess.run(command, shell=True, stdout=subprocess.PIPE)
        channel.send(res.stdout)
        channel.send_exit_status(res.returncode)
    except Exception as err:
        print('exception: {}'.format(err))
        channel.send('An error occurred: {}\r\n'.format(err))
        channel.send_exit_status(255)
    finally:
        self.event.set()
    return True

这里使用subprocess.run(...)执行命令然后发送 输出到客户端。这有几个限制 实施...

  • 它不是交互式的(输出直到 命令完成后)。
  • 它不处理 stderr
  • 上的命令输出

...但希望这足以让您入门。


您的代码的另一个问题是您对 client.event。这是一个 Python Event 对象,用于线程间的信号传递。当你写:

channel.event.wait(5)

您说的是“最多等待 5 秒以设置事件”。一个 事件由调用 event.set() 的东西设置,您可以 看看我们在 check_channel_exec_request.

你使用这个的方式没有意义,写:

while true:
  channel.event.wait(5)

你有一个无限循环。你想要的东西会等待 命令执行然后关闭通道,所以也许是什么 喜欢:

channel.event.wait(30)
channel.close()

这意味着“最多等待 30 秒让命令完成,并且 即使没有,也请关闭频道。

通过这两项更改,您的代码将接受单个命令,并且 出口。如果你想让服务器保持 运行ning 以便你可以连接 多次,你将需要实现某种循环 您代码的主要部分。

这是包含我建议的所有更改的代码:

#!/bin/python3
import paramiko
import socket
import subprocess
import time


class Ctx(paramiko.server.ServerInterface):
    def get_allowed_auths(self, username):
        return "password,publickey"

    def check_auth_publickey(self, key):
        return paramiko.AUTH_SUCCESSFUL

    def check_channel_request(self, kind, channelID):
        return paramiko.OPEN_SUCCEEDED

    def check_channel_shell_request(self, channel):
        return True

    def check_channel_pty_request(self, c, t, w, h, p, ph, m):
        return True

    def get_banner(self):
        return ("This is MY SSH Server\n\r", "EN")

    def check_channel_exec_request(self, channel, command):
        try:
            res = subprocess.run(command, shell=True, stdout=subprocess.PIPE)
            channel.send(res.stdout)
            channel.send_exit_status(res.returncode)
        except Exception as err:
            print('exception: {}'.format(err))
            channel.send('An error occurred: {}\r\n'.format(err))
            channel.send_exit_status(255)
        finally:
            self.event.set()
        return True

    def check_auth_password(self, username, password):
        if password == "1999":    return paramiko.AUTH_SUCCESSFUL
        else:                     return paramiko.AUTH_FAILED


paramiko.util.log_to_file("demo_server.log")  # setup log file
host_key = paramiko.RSAKey(
    filename="./test_rsa.key"
)  # setup rsa key file that will be used during authnitication
ctx = Ctx()  # create ServerInterface context object
sock = socket.socket()  # Create socket object
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(("127.0.0.1", 5555))  # bind socket to specific Port
sock.listen(100)  # Listen for TCP connections

print("***************** Listening for connection **************************")
client, addr = sock.accept()  # accept TCP socket connection
print("[+]*****************  Listening for SSH connections ***************** ")
server = paramiko.Transport(client)
server.add_server_key(host_key)  # Setup key
server.start_server(server=ctx)  # SSH start_server
channel = server.accept(30)  # Accept Auth requests
if channel is None:
    print("[+] *****************  No Auth request Was made. ***************** ")
    exit(1)
channel.send("[+]*****************  Welcome ***************** \n\r")

# wait for command execution to complete (or timeout)
channel.event.wait(30)  # but I'm not sure what it does actually
channel.close()

更新 1

需要说明的是,这不会为您提供交互式会话。这让你 运行 像这样的命令:

$ ssh -p 5555 localhost date
This is MY SSH Server
lars@localhost's password:
[+]*****************  Welcome *****************
Sun Aug 15 09:35:53 AM EDT 2021
Connection to localhost closed by remote host.

如果您想启用交互式会话,check_channel_exec_request 不是您想要的。

does this mean that I have to open a new channel for each command, Is this is how it's supposed to be done with SSH or I can just use the wait in loop so that only one channel for all upcoming commands.

使用 this 模型,使用 check_channel_exec_request,每个命令都需要一个新连接。您的代码的主要部分如下所示:

while True:
    print("***************** Listening for connection **************************")
    client, addr = sock.accept()  # accept TCP socket connection
    print("[+]*****************  Listening for SSH connections ***************** ")
    server = paramiko.Transport(client)
    [...]

当然,这不是处理事情的唯一方法,如果你看 您可以在周围找到许多基于 Paramiko 的服务示例 这可能会有所帮助。例如,ShuSSH 显示了一个重要的 Paramiko 服务器实现。

我知道你已经“提前接受”了一个答案,但你可以看看下面的内容,它基于 on SO 修改如下:

  1. 使用线程支持并发 SSH 请求。
  2. 识别用于终止程序的“退出”命令,因为处理 ctrl-C 终止的代码不太理想。设置常量 SUPPORT_EXIT = False 以移除此支持。

程序目前只是记录命令并将其回显给用户。

使用示例:

ssh localhost -p 5555 some-command

代码:

#!/usr/bin/env python
import logging
import socket
import sys
import threading
from queue import Queue

import paramiko

logging.basicConfig()
paramiko.util.log_to_file('demo_server.log', level='INFO')
logger = paramiko.util.get_logger("paramiko")

host_key = paramiko.RSAKey(filename='./rsa')

SUPPORT_EXIT = True

# input queue of requests:
in_q = Queue()

class Server(paramiko.ServerInterface):
    def __init__(self):
        self.event = threading.Event()

    def check_channel_request(self, kind, chanid):
        if kind == 'session':
            return paramiko.OPEN_SUCCEEDED

    def check_auth_password(self, username, password):
        if password == '9999':
            return paramiko.AUTH_SUCCESSFUL
        return paramiko.AUTH_FAILED

    def get_allowed_auths(self, username):
        return 'publickey,password'

    def check_channel_exec_request(self, channel, command):
        # This is the command we need to parse
        # Here we just log it and echo it back to the user:
        command = command.decode() # convert to string from bytes:
        logger.info('Command = %s', command)
        channel.send(command + '\n')
        if SUPPORT_EXIT and command == 'exit':
            # Place None in in_q to signify time to exit:
            in_q.put(None)
        self.event.set()
        return True


def run_server(client):
    t = paramiko.Transport(client)
    t.set_gss_host(socket.getfqdn(""))
    t.load_server_moduli()
    t.add_server_key(host_key)
    server = Server()
    t.start_server(server=server)

    # Wait 30 seconds for a command
    server.event.wait(30)
    t.close()


def accept(sock):
    while True:
        try:
            client, _ = sock.accept()
        except Exception as exc:
            logger.error(exc)
        else:
            in_q.put(client)


def listener():
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.bind(('', 5555))

    sock.listen(100)

    threading.Thread(target=accept, args=(sock,), daemon=True).start()

    while True:
        try:
            client = in_q.get()
            if SUPPORT_EXIT and client is None: # exit command issued
                break
            threading.Thread(target=run_server, args=(client,), daemon=True).start()
        except KeyboardInterrupt:
            sys.exit(0)

if __name__ == '__main__':
    listener()

我之前的回答适合执行单个命令。此新版本支持以下变体:

  1. ssh ip-address -p 5555 -T - 创建交互式会话。现在,每个输入行都被回显并记录下来,直到输入 'quit\n'。
  2. ssh ip-address -p 5555 some-command - 执行单个命令 some-command,但目前仅包括回显命令并记录它。
  3. ssh ip-address -p 5555 exit - 如果在源中设置了 SUPPORT_EXIT = True,则关闭服务器。
#!/usr/bin/env python
import logging
import socket
import sys
import threading
from queue import Queue

import paramiko

logging.basicConfig()
paramiko.util.log_to_file('demo_server.log', level='INFO')
logger = paramiko.util.get_logger("paramiko")

host_key = paramiko.RSAKey(filename='./rsa')

SUPPORT_EXIT = True

# input queue of requests:
in_q = Queue()

def my_processor(stdin, stdout, event):
    stdout.write('This is MY SSH Server:\n\n')
    for command in stdin:
        if command == 'quit\n':
            break
        # Just log the command and send it back:
        logger.info('Command = %s', command)
        stdout.write(command)
    # signal termination
    event.set()

class Server(paramiko.ServerInterface):
    def __init__(self):
        self.event = threading.Event()

    def check_channel_request(self, kind, chanid):
        if kind == 'session':
            return paramiko.OPEN_SUCCEEDED

    def check_auth_password(self, username, password):
        if password == '9999':
            return paramiko.AUTH_SUCCESSFUL
        return paramiko.AUTH_FAILED

    def get_allowed_auths(self, username):
        return 'publickey,password'

    def check_channel_exec_request(self, channel, command):
        # This is the command we need to parse
        command = command.decode() # convert to string from bytes:
        if SUPPORT_EXIT and command == 'exit':
            # Place None in in_q to signify time to exit:
            in_q.put(None)
        # We just log it and echo it back to the user:
        logger.info('Command = %s', command)
        channel.send(command + '\n')
        self.event.set() # Command execution complete
        # Show command successfully "wired up" to stdin, stdout and stderr:
        # Return False if invalid command:
        return True

    def check_channel_shell_request(self, channel):
        """ No command specified, interactive session implied """
        stdout = channel.makefile('w')
        stdin = channel.makefile('r')
        threading.Thread(target=my_processor, args=(stdin, stdout, self.event), daemon=True).start()
        # Show command successfully "wired up" to stdin, stdout and stderr:
        return True


def run_server(client):
    t = paramiko.Transport(client)
    t.set_gss_host(socket.getfqdn(""))
    t.load_server_moduli()
    t.add_server_key(host_key)
    server = Server()
    t.start_server(server=server)
    # wait for termination:
    server.event.wait()
    t.close()


def accept(sock):
    while True:
        try:
            client, _ = sock.accept()
        except Exception as exc:
            logger.error(exc)
        else:
            in_q.put(client)


def listener():
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.bind(('', 5555))

    sock.listen(100)

    threading.Thread(target=accept, args=(sock,), daemon=True).start()

    while True:
        try:
            client = in_q.get()
            if SUPPORT_EXIT and client is None: # exit command issued
                break
            threading.Thread(target=run_server, args=(client,), daemon=True).start()
        except KeyboardInterrupt:
            sys.exit(0)

if __name__ == '__main__':
    listener()