mirror of
https://github.com/clearml/dropbear
synced 2025-03-09 21:41:07 +00:00
Add first channel tests
These initial tests are checking various edge cases of channel handling that have cropped up over the years.
This commit is contained in:
parent
a7ef149463
commit
65f6e48a06
18
test/conftest.py
Normal file
18
test/conftest.py
Normal file
@ -0,0 +1,18 @@
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption("--port", type=str, help="default is 2244 local, 22 remote")
|
||||
parser.addoption("--dbclient", type=str, default="../dbclient")
|
||||
parser.addoption("--dropbear", type=str, default="../dropbear")
|
||||
parser.addoption("--hostkey", type=str, help="required unless --remote")
|
||||
parser.addoption("--remote", type=str, help="remote host")
|
||||
parser.addoption("--user", type=str, help="optional username")
|
||||
|
||||
def pytest_configure(config):
|
||||
opt = config.option
|
||||
if not opt.hostkey and not opt.remote:
|
||||
raise Exception("--hostkey must be given")
|
||||
if not opt.port:
|
||||
if opt.remote:
|
||||
opt.port = "22"
|
||||
else:
|
||||
opt.port = "2244"
|
||||
|
8
test/requirements.txt
Normal file
8
test/requirements.txt
Normal file
@ -0,0 +1,8 @@
|
||||
attrs==21.2.0
|
||||
iniconfig==1.1.1
|
||||
packaging==21.0
|
||||
pluggy==1.0.0
|
||||
py==1.10.0
|
||||
pyparsing==2.4.7
|
||||
pytest==6.2.5
|
||||
toml==0.10.2
|
134
test/test_channels.py
Normal file
134
test/test_channels.py
Normal file
@ -0,0 +1,134 @@
|
||||
from test_dropbear import *
|
||||
import signal
|
||||
import queue
|
||||
import socket
|
||||
|
||||
# Tests for various edge cases of SSH channels and connection service
|
||||
|
||||
def test_exitcode(request, dropbear):
|
||||
r = dbclient(request, "exit 44")
|
||||
assert r.returncode == 44
|
||||
|
||||
@pytest.mark.xfail(reason="Not yet implemented", strict=True)
|
||||
def test_signal(request, dropbear):
|
||||
r = dbclient(request, "kill -FPE $$")
|
||||
assert r.returncode == -signal.SIGFPE
|
||||
|
||||
@pytest.mark.parametrize("size", [0, 1, 2, 100, 5000, 200_000])
|
||||
def test_roundtrip(request, dropbear, size):
|
||||
dat = os.urandom(size)
|
||||
r = dbclient(request, "cat", input=dat, capture_output=True)
|
||||
r.check_returncode()
|
||||
assert r.stdout == dat
|
||||
|
||||
@pytest.mark.parametrize("size", [0, 1, 2, 100, 20001, 41234])
|
||||
def test_read_pty(request, dropbear, size):
|
||||
# testcase for
|
||||
# https://bugs.openwrt.org/index.php?do=details&task_id=1814
|
||||
# https://github.com/mkj/dropbear/pull/85
|
||||
# From Yousong Zhou
|
||||
# Fixed Oct 2021
|
||||
#
|
||||
#$ ssh -t my.router cat /tmp/bigfile | wc
|
||||
#Connection to my.router closed.
|
||||
# 0 1 14335 <- should be 20001
|
||||
|
||||
# Write the file. No newlines etc which could confuse ptys
|
||||
dat = random_alnum(size)
|
||||
r = dbclient(request, "tmpf=`mktemp`; echo $tmpf; cat > $tmpf", input=dat, capture_output=True, text=True)
|
||||
tmpf = r.stdout.rstrip()
|
||||
r.check_returncode()
|
||||
# Read with a pty, this is what is being tested.
|
||||
# Timing/buffering is subtle, we seem to need to cat a file from disk to hit it.
|
||||
m, s = pty.openpty()
|
||||
r = dbclient(request, "-t", f"cat {tmpf}; rm {tmpf}", stdin=s, capture_output=True)
|
||||
r.check_returncode()
|
||||
assert r.stdout.decode() == dat
|
||||
|
||||
@pytest.mark.parametrize("fd", [1, 2])
|
||||
def test_bg_sleep(request, fd, dropbear):
|
||||
# https://lists.ucc.asn.au/pipermail/dropbear/2006q1/000362.html
|
||||
# Rob Landley "Is this a bug?" 24 Mar 2006
|
||||
# dbclient user@system "sleep 10& echo hello"
|
||||
#
|
||||
# It should return right after printing hello, but it doesn't. It waits until
|
||||
# the child process exits.
|
||||
|
||||
# failure is TimeoutExpired
|
||||
redir = "" if fd == 1 else " >&2 "
|
||||
r = dbclient(request, f"sleep 10& echo hello {redir}",
|
||||
capture_output=True, timeout=2, text=True)
|
||||
r.check_returncode()
|
||||
st = r.stdout if fd == 1 else r.stderr
|
||||
|
||||
if fd == 2 and 'accepted unconditionally' in st:
|
||||
# ignore hostkey warning, a bit of a hack
|
||||
assert st.endswith("\n\nhello\n")
|
||||
else:
|
||||
assert st.rstrip() == "hello"
|
||||
|
||||
|
||||
def test_idle(request, dropbear):
|
||||
# Idle test, -I 1 should make it return before the 2 second timeout
|
||||
r = dbclient(request, "-I", "1", "echo zong; sleep 10",
|
||||
capture_output=True, timeout=2, text=True)
|
||||
r.check_returncode()
|
||||
assert r.stdout.rstrip() == "zong"
|
||||
|
||||
@pytest.mark.parametrize("size", [1, 4000, 40000])
|
||||
def test_netcat(request, dropbear, size):
|
||||
opt = request.config.option
|
||||
if opt.remote:
|
||||
pytest.xfail("don't know netcat address for remote")
|
||||
|
||||
dat1 = os.urandom(size)
|
||||
dat2 = os.urandom(size)
|
||||
with HandleTcp(3344, 1, dat2) as tcp:
|
||||
r = dbclient(request, "-B", "localhost:3344", input=dat1, capture_output=True)
|
||||
r.check_returncode()
|
||||
assert r.stdout == dat2
|
||||
assert tcp.inbound() == dat1
|
||||
|
||||
@pytest.mark.parametrize("size", [1, 4000, 40000])
|
||||
@pytest.mark.parametrize("fwd_flag", "LR")
|
||||
def test_tcpflushout(request, dropbear, size, fwd_flag):
|
||||
""" Tests that an opened TCP connection prevent a SSH session from being closed
|
||||
until that TCP connection has finished transferring
|
||||
"""
|
||||
opt = request.config.option
|
||||
if opt.remote:
|
||||
pytest.xfail("don't know address for remote")
|
||||
|
||||
dat1 = os.urandom(size)
|
||||
dat2 = os.urandom(size)
|
||||
q = queue.Queue()
|
||||
with HandleTcp(3344, timeout=1, response=q) as tcp:
|
||||
|
||||
r = dbclient(request, f"-{fwd_flag}", "7788:localhost:3344", "sleep 0.1; echo -n done",
|
||||
text=True, background=True, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
|
||||
# time to let the listener start
|
||||
time.sleep(0.1)
|
||||
# open a tcp connection
|
||||
c = socket.create_connection(("localhost", 7788))
|
||||
|
||||
# wait for the shell to finish. sleep a bit longer in case it exits.
|
||||
assert r.stdout.read(4) == "done"
|
||||
time.sleep(0.1)
|
||||
|
||||
# now the shell has finished, we can write on the tcp socket
|
||||
c.sendall(dat2)
|
||||
c.shutdown(socket.SHUT_WR)
|
||||
q.put(dat1)
|
||||
|
||||
# return a tcp response
|
||||
q.put(None)
|
||||
# check hasn't exited
|
||||
assert r.poll() == None
|
||||
|
||||
# read the response
|
||||
assert readall_socket(c) == dat1
|
||||
c.close()
|
||||
assert tcp.inbound() == dat2
|
||||
# check has exited, allow time for dbclient to exit
|
||||
time.sleep(0.1)
|
||||
assert r.poll() == 0
|
109
test/test_dropbear.py
Normal file
109
test/test_dropbear.py
Normal file
@ -0,0 +1,109 @@
|
||||
import subprocess
|
||||
import os
|
||||
import pty
|
||||
import tempfile
|
||||
import logging
|
||||
import time
|
||||
import socketserver
|
||||
import threading
|
||||
import queue
|
||||
|
||||
import pytest
|
||||
|
||||
LOCALADDR="127.0.5.5"
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def dropbear(request):
|
||||
opt = request.config.option
|
||||
if opt.remote:
|
||||
yield None
|
||||
return
|
||||
|
||||
args = [opt.dropbear,
|
||||
"-p", LOCALADDR, # bind locally only
|
||||
"-r", opt.hostkey,
|
||||
"-p", opt.port,
|
||||
"-F", "-E",
|
||||
]
|
||||
p = subprocess.Popen(args, stderr=subprocess.PIPE, text=True)
|
||||
# Wait until it has started listening
|
||||
for l in p.stderr:
|
||||
if "Not backgrounding" in l:
|
||||
break
|
||||
# Check it's still running
|
||||
assert p.poll() is None
|
||||
# Ready
|
||||
yield p
|
||||
p.terminate()
|
||||
|
||||
def dbclient(request, *args, **kwargs):
|
||||
opt = request.config.option
|
||||
host = opt.remote or LOCALADDR
|
||||
base_args = [opt.dbclient, "-y", host, "-p", opt.port]
|
||||
if opt.user:
|
||||
full_args.extend(['-l', opt.user])
|
||||
full_args = base_args + list(args)
|
||||
bg = kwargs.get("background")
|
||||
if "background" in kwargs:
|
||||
del kwargs["background"]
|
||||
if bg:
|
||||
return subprocess.Popen(full_args, **kwargs)
|
||||
else:
|
||||
# wait for response
|
||||
return subprocess.run(full_args, **kwargs)
|
||||
|
||||
class HandleTcp(socketserver.ThreadingMixIn, socketserver.TCPServer):
|
||||
""" Listens for a single incoming request, sends a response if given,
|
||||
and returns the inbound data.
|
||||
Reponse can be a queue object, in which case each item in the queue will
|
||||
be sent as a response, until it receives a None item.
|
||||
"""
|
||||
def __init__(self, port, timeout, response=None):
|
||||
super().__init__(('localhost', port), self.Handler)
|
||||
self.port = port
|
||||
self.timeout = timeout
|
||||
self.response = response
|
||||
self.sink = None
|
||||
|
||||
class Handler(socketserver.StreamRequestHandler):
|
||||
def handle(self):
|
||||
if isinstance(self.server.response, queue.Queue):
|
||||
while True:
|
||||
i = self.server.response.get()
|
||||
if i is None:
|
||||
break
|
||||
self.wfile.write(i)
|
||||
elif self.server.response:
|
||||
self.wfile.write(self.server.response)
|
||||
assert self.server.sink is None, ">1 request sent to handler"
|
||||
self.server.sink = self.rfile.read()
|
||||
|
||||
def __enter__(self):
|
||||
self.server_thread = threading.Thread(target=self.serve_forever)
|
||||
self.server_thread.daemon = True
|
||||
self.server_thread.start()
|
||||
return self
|
||||
|
||||
def __exit__(self, *exc_stuff):
|
||||
self.shutdown()
|
||||
self.server_thread.join()
|
||||
|
||||
def inbound(self):
|
||||
""" Returns the data sent to the socket """
|
||||
return self.sink
|
||||
|
||||
def readall_socket(sock):
|
||||
b = []
|
||||
while True:
|
||||
i = sock.recv(4096)
|
||||
if not i:
|
||||
break
|
||||
b.append(i)
|
||||
return b''.join(b)
|
||||
|
||||
# returns a str
|
||||
def random_alnum(size):
|
||||
r = os.urandom(500 + size*5)
|
||||
return bytes(i for i in r if bytes((i,)).isalnum())[:size].decode()
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user