Fix #1711: adapter: access tokens

Generate server access token in enable_attach(), propagate it to the adapter, and have the adapter authenticate to the server via "pydevdAuthorize".

Generate client access token in adapter when not spawned by server, and propagate it to pydevd.

Fix pydevd to correctly propagate access tokens for subprocesses that are not forked.
This commit is contained in:
Pavel Minaev 2019-12-08 18:06:51 -08:00 committed by Pavel Minaev
parent f51c96450c
commit 0d79c16f80
12 changed files with 111 additions and 58 deletions

View file

@ -170,4 +170,4 @@ __file__ = os.path.abspath(__file__)
# Preload encodings that we're going to use to avoid import deadlocks on Python 2,
# before importing anything from ptvsd.
map(codecs.lookup, ["ascii", "utf8", "utf-8", "latin1", "latin-1", "idna"])
map(codecs.lookup, ["ascii", "utf8", "utf-8", "latin1", "latin-1", "idna", "hex"])

View file

@ -67,14 +67,16 @@ def _get_setup_updated_with_protocol(setup):
def _get_python_c_args(host, port, indC, args, setup):
setup = _get_setup_updated_with_protocol(setup)
return ("import sys; sys.path.append(r'%s'); import pydevd; pydevd.PydevdCustomization.DEFAULT_PROTOCOL=%r;"
"pydevd.settrace(host=%r, port=%s, suspend=False, trace_only_current_thread=False, patch_multiprocessing=True); "
return ("import sys; sys.path.append(r'%s'); import pydevd; pydevd.PydevdCustomization.DEFAULT_PROTOCOL=%r; "
"pydevd.settrace(host=%r, port=%s, suspend=False, trace_only_current_thread=False, patch_multiprocessing=True, access_token=%r, ide_access_token=%r); "
"from pydevd import SetupHolder; SetupHolder.setup = %s; %s"
) % (
pydev_src_dir,
pydevd_constants.get_protocol(),
host,
port,
setup.get('access-token'),
setup.get('ide-access-token'),
setup,
args[indC + 1])

View file

@ -6,6 +6,7 @@ from __future__ import absolute_import, division, print_function, unicode_litera
import argparse
import atexit
import codecs
import json
import locale
import os
@ -19,7 +20,7 @@ __file__ = os.path.abspath(__file__)
def main(args):
from ptvsd.common import log, options as common_options
from ptvsd.common import compat, log, options as common_options
from ptvsd.adapter import ide, servers, sessions, options as adapter_options
if args.log_stderr:
@ -31,10 +32,17 @@ def main(args):
log.to_file(prefix="ptvsd.adapter")
log.describe_environment("ptvsd.adapter startup environment:")
if args.for_enable_attach and args.port is None:
log.error("--for-enable-attach requires --port")
if args.for_server and args.port is None:
log.error("--for-server requires --port")
sys.exit(64)
# adapter_options.ide_access_token = args.ide_access_token
adapter_options.server_access_token = args.server_access_token
if not args.for_server:
adapter_options.adapter_access_token = compat.force_str(
codecs.encode(os.urandom(32), "hex")
)
server_host, server_port = servers.listen()
ide_host, ide_port = ide.listen(port=args.port)
endpoints_info = {
@ -42,7 +50,7 @@ def main(args):
"server": {"host": server_host, "port": server_port},
}
if args.for_enable_attach:
if args.for_server:
log.info("Writing endpoints info to stdout:\n{0!r}", endpoints_info)
print(json.dumps(endpoints_info))
sys.stdout.flush()
@ -99,10 +107,16 @@ def _parse_argv(argv):
help="start the adapter in debugServer mode on the specified host",
)
# parser.add_argument(
# "--ide-access-token", type=str, help="access token expected by the IDE"
# )
parser.add_argument(
"--for-enable-attach", action="store_true", help=argparse.SUPPRESS
"--server-access-token", type=str, help="access token expected by the server"
)
parser.add_argument("--for-server", action="store_true", help=argparse.SUPPRESS)
parser.add_argument(
"--log-dir",
type=str,

View file

@ -111,6 +111,7 @@ def spawn_debuggee(session, start_request, sudo, args, console, console_title):
_, port = servers.Connection.listener.getsockname()
arguments = dict(start_request.arguments)
arguments["port"] = port
arguments["clientAccessToken"] = adapter_options.adapter_access_token
spawn_launcher()
if not session.wait_for(

View file

@ -11,3 +11,12 @@ or configuartion files.
log_stderr = False
"""Whether detailed logs are written to stderr."""
# ide_access_token = None
# """Access token used to authenticate with the IDE."""
server_access_token = None
"""Access token used to authenticate with the server."""
adapter_access_token = None
"""Access token used by the server to authenticate with this adapter."""

View file

@ -13,7 +13,7 @@ import time
import ptvsd
from ptvsd.common import compat, fmt, json, log, messaging, sockets
from ptvsd.adapter import components
from ptvsd.adapter import components, options
_lock = threading.RLock()
@ -49,6 +49,7 @@ class Connection(sockets.ClientConnection):
self.channel.start()
try:
self.authenticate()
info = self.channel.request("pydevdSystemInfo")
process_info = info("process", json.object())
self.pid = process_info("pid", int)
@ -126,6 +127,16 @@ if 'ptvsd' not in sys.modules:
def __str__(self):
return "Server" + fmt("[?]" if self.pid is None else "[pid={0}]", self.pid)
def authenticate(self):
if options.server_access_token is None and options.adapter_access_token is None:
return
auth = self.channel.request(
"pydevdAuthorize", {"debugServerAccessToken": options.server_access_token}
)
if auth["clientAccessToken"] != options.adapter_access_token:
self.channel.close()
raise RuntimeError('Mismatched "clientAccessToken"; server not authorized.')
def request(self, request):
raise request.isnt_valid(
"Requests from the debug server to the IDE are not allowed."
@ -230,6 +241,7 @@ class Server(components.Component):
def initialize(self, request):
assert request.is_request("initialize")
self.connection.authenticate()
request = self.channel.propagate(request)
request.wait_for_response()
self.capabilities = self.Capabilities(self, request.response)
@ -394,6 +406,8 @@ def inject(pid, ptvsd_args):
"--port",
str(port),
]
if options.adapter_access_token is not None:
cmdline += ["--client-access-token", options.adapter_access_token]
cmdline += ptvsd_args
cmdline += ["--pid", str(pid)]

View file

@ -62,7 +62,6 @@ def launch_request(request):
if not request("noDebug", json.default(False)):
port = request("port", int)
ptvsd_args = request("ptvsdArgs", json.array(unicode))
cmdline += [
compat.filename(os.path.dirname(ptvsd.__file__)),
"--client",
@ -70,7 +69,12 @@ def launch_request(request):
"127.0.0.1",
"--port",
str(port),
] + ptvsd_args
]
client_access_token = request("clientAccessToken", unicode, optional=True)
if client_access_token != ():
cmdline += ["--client-access-token", compat.filename(client_access_token)]
ptvsd_args = request("ptvsdArgs", json.array(unicode))
cmdline += ptvsd_args
program = module = code = ()
if "program" in request:
@ -119,7 +123,7 @@ def launch_request(request):
# If neither the property nor the option were specified explicitly, choose
# the default depending on console type - "internalConsole" needs it to
# provide any output at all, but it's unnecessary for the terminals.
redirect_output = (request("console", unicode) == "internalConsole")
redirect_output = request("console", unicode) == "internalConsole"
if redirect_output:
# sys.stdout buffering must be disabled - otherwise we won't see the output
# at all until the buffer fills up.

View file

@ -4,6 +4,7 @@
from __future__ import absolute_import, division, print_function, unicode_literals
import codecs
import contextlib
import json
import os
@ -12,7 +13,7 @@ import sys
import threading
import ptvsd
from ptvsd.common import log, options as common_opts
from ptvsd.common import compat, log, options as common_opts
from ptvsd.server import options as server_opts
from _pydevd_bundle.pydevd_constants import get_global_debugger
from pydevd_file_utils import get_abs_path_real_path_and_base_from_file
@ -25,10 +26,8 @@ _ADAPTER_PATH = os.path.join(os.path.dirname(ptvsd.__file__), "adapter")
def wait_for_attach():
log.info("wait_for_attach()")
dbg = get_global_debugger()
if not bool(dbg):
msg = "wait_for_attach() called before enable_attach()."
log.info(msg)
raise AssertionError(msg)
if dbg is None:
raise RuntimeError("wait_for_attach() called before enable_attach().")
cancel_event = threading.Event()
ptvsd.wait_for_attach.cancel = wait_for_attach.cancel = cancel_event.set
@ -80,16 +79,20 @@ def enable_attach(dont_trace_start_patterns, dont_trace_end_patterns):
if hasattr(enable_attach, "called"):
raise RuntimeError("enable_attach() can only be called once per process.")
server_access_token = compat.force_str(codecs.encode(os.urandom(32), "hex"))
import subprocess
adapter_args = [
sys.executable,
_ADAPTER_PATH,
"--for-server",
"--host",
server_opts.host,
"--port",
str(server_opts.port),
"--for-enable-attach",
"--server-access-token",
server_access_token,
]
if common_opts.log_dir is not None:
@ -122,6 +125,8 @@ def enable_attach(dont_trace_start_patterns, dont_trace_end_patterns):
block_until_connected=True,
dont_trace_start_patterns=dont_trace_start_patterns,
dont_trace_end_patterns=dont_trace_end_patterns,
access_token=server_access_token,
ide_access_token=server_opts.client_access_token,
)
log.info("pydevd debug client connected to: {0}:{1}", host, port)
@ -146,6 +151,7 @@ def attach(dont_trace_start_patterns, dont_trace_end_patterns):
patch_multiprocessing=server_opts.multiprocess,
dont_trace_start_patterns=dont_trace_start_patterns,
dont_trace_end_patterns=dont_trace_end_patterns,
ide_access_token=server_opts.client_access_token,
)

View file

@ -11,7 +11,7 @@ __file__ = os.path.abspath(__file__)
_ptvsd_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
def attach(host, port, client, log_dir=None):
def attach(host, port, client, log_dir=None, client_access_token=None):
try:
import sys
@ -68,6 +68,7 @@ def attach(host, port, client, log_dir=None):
options.client = client
options.host = host
options.port = port
options.client_access_token = client_access_token
if options.client:
ptvsd.attach((options.host, options.port))

View file

@ -108,8 +108,7 @@ switches = [
("--log-stderr", None, set_log_stderr(), False),
# Switches that are used internally by the IDE or ptvsd itself.
("--subprocess-of", "<pid>", set_arg("subprocess_of", pid), False),
("--subprocess-notify", "<port>", set_arg("subprocess_notify", port), False),
("--client-access-token", "<token>", set_arg("client_access_token"), False),
# Targets. The "" entry corresponds to positional command line arguments,
# i.e. the ones not preceded by any switch name.
@ -270,31 +269,24 @@ def attach_to_pid():
log.info("Attaching to process with PID={0}", options.target)
pid = options.target
host = options.host
port = options.port
client = options.client
log_dir = common_opts.log_dir
if log_dir is None:
log_dir = ""
try:
attach_pid_injected_dirname = os.path.join(
os.path.dirname(ptvsd.__file__), "server"
)
assert os.path.exists(attach_pid_injected_dirname)
attach_pid_injected_dirname = os.path.join(
os.path.dirname(ptvsd.__file__), "server"
)
assert os.path.exists(attach_pid_injected_dirname)
log_dir = log_dir.replace("\\", "/")
log_dir = (common_opts.log_dir or "").replace("\\", "/")
encode = lambda s: list(bytearray(s.encode("utf-8")))
setup = {
"script": encode(attach_pid_injected_dirname),
"host": encode(options.host),
"port": options.port,
"client": options.client,
"log_dir": encode(log_dir),
"client_access_token": encode(options.client_access_token),
}
encode = lambda s: list(bytearray(s.encode("utf-8")))
setup = {
"script": encode(attach_pid_injected_dirname),
"host": encode(host),
"port": port,
"client": client,
"log_dir": encode(log_dir),
}
python_code = """
python_code = """
import sys;
import codecs;
decode = lambda s: codecs.utf_8_decode(bytearray(s))[0];
@ -304,32 +296,39 @@ import attach_pid_injected;
sys.path.remove(script_path);
host = decode({host});
log_dir = decode({log_dir}) or None;
attach_pid_injected.attach(port={port}, host=host, client={client}, log_dir=log_dir)
client_access_token = decode({client_access_token}) or None;
attach_pid_injected.attach(
port={port},
host=host,
client={client},
log_dir=log_dir,
client_access_token=client_access_token,
)
"""
python_code = python_code.replace("\r", "").replace("\n", "").format(**setup)
log.info("Code to be injected: \n{0}", python_code.replace(";", ";\n"))
python_code = python_code.replace("\r", "").replace("\n", "").format(**setup)
log.info("Code to be injected: \n{0}", python_code.replace(";", ";\n"))
# pydevd restriction on characters in injected code.
assert not (
{'"', "'", "\r", "\n"} & set(python_code)
), "Injected code should not contain any single quotes, double quots, or newlines."
# pydevd restriction on characters in injected code.
assert not (
{'"', "'", "\r", "\n"} & set(python_code)
), "Injected code should not contain any single quotes, double quotes, or newlines."
pydevd_attach_to_process_path = os.path.join(
os.path.dirname(pydevd.__file__), "pydevd_attach_to_process"
)
pydevd_attach_to_process_path = os.path.join(
os.path.dirname(pydevd.__file__), "pydevd_attach_to_process"
)
assert os.path.exists(pydevd_attach_to_process_path)
sys.path.append(pydevd_attach_to_process_path)
assert os.path.exists(pydevd_attach_to_process_path)
sys.path.append(pydevd_attach_to_process_path)
try:
import add_code_to_python_process # noqa
show_debug_info_on_target_process = 0 # hard-coded (1 to debug)
log.info("Injecting code into process with PID={0} ...", pid)
add_code_to_python_process.run_python_code(
pid,
python_code,
connect_debugger_tracing=True,
show_debug_info=show_debug_info_on_target_process,
show_debug_info=int(os.getenv("PTVSD_ATTACH_BY_PID_DEBUG_INFO", "0")),
)
except Exception:
raise log.exception("Code injection into PID={0} failed:", pid)

View file

@ -53,3 +53,6 @@ multiprocess = True
"""Whether this ptvsd instance is running in multiprocess mode, detouring creation
of new processes and enabling debugging for them.
"""
client_access_token = None
"""Access token to authenticate with the adapter."""

View file

@ -18,7 +18,7 @@ def dump():
log.info("Dumping logs from {0!j}", options.log_dir)
for dirpath, dirnames, filenames in os.walk(options.log_dir):
for name in filenames:
for name in sorted(filenames):
if not name.startswith("ptvsd") and not name.startswith("pydevd"):
continue
try: