fix(lsp): restore secure ASCII decoding, top-level LspError import, printable-ASCII guard
This commit is contained in:
@@ -27,6 +27,8 @@ from typing import Any
|
||||
|
||||
import structlog
|
||||
|
||||
from cleveragents.lsp.errors import LspError
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
# Maximum time (seconds) to wait for a graceful process termination
|
||||
@@ -112,36 +114,15 @@ class StdioTransport:
|
||||
cwd=self._cwd,
|
||||
)
|
||||
except FileNotFoundError as exc:
|
||||
self._process = None
|
||||
|
||||
from cleveragents.lsp.errors import LspError
|
||||
|
||||
raise LspError(
|
||||
f"LSP server command not found: {self._command}",
|
||||
details={"command": self._command, "args": self._args},
|
||||
) from exc
|
||||
except OSError as exc:
|
||||
# Popen may have partially started the subprocess before
|
||||
# raising (e.g. execve failure post-fork on some platforms).
|
||||
# Ensure cleanup so the process does not leak into the caller's
|
||||
# address space.
|
||||
if self._process is not None:
|
||||
self.stop()
|
||||
self._process = None
|
||||
|
||||
from cleveragents.lsp.errors import LspError
|
||||
|
||||
raise LspError(
|
||||
f"Failed to start LSP server: {exc}",
|
||||
details={"command": self._command, "error": str(exc)},
|
||||
) from exc
|
||||
except Exception:
|
||||
# Catch-all for any other low-level OSError / resource-error
|
||||
# variants that might leave a zombie process behind.
|
||||
if self._process is not None:
|
||||
self.stop()
|
||||
self._process = None
|
||||
raise
|
||||
|
||||
logger.info(
|
||||
"lsp.transport.started",
|
||||
@@ -239,7 +220,23 @@ class StdioTransport:
|
||||
return self._read_one_message(effective_timeout)
|
||||
|
||||
def _read_one_message(self, timeout: float) -> dict[str, Any] | None:
|
||||
"""Parse a single ``Content-Length`` framed JSON-RPC message."""
|
||||
"""Parse a single ``Content-Length``-framed JSON-RPC message.
|
||||
|
||||
Header lines are decoded with ``errors="strict"``; any non-ASCII byte
|
||||
raises :class:`~cleveragents.lsp.errors.LspError`. An additional
|
||||
printable-ASCII guard rejects decoded headers containing characters
|
||||
outside the codepoint range 0x20 (space) to 0x7E (tilde).
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict[str, Any] | None
|
||||
Parsed JSON body, or ``None`` on EOF / timeout.
|
||||
|
||||
Raises
|
||||
------
|
||||
LspError
|
||||
If a header contains non-ASCII bytes or non-printable characters.
|
||||
"""
|
||||
assert self._process is not None
|
||||
assert self._process.stdout is not None
|
||||
|
||||
@@ -254,7 +251,21 @@ class StdioTransport:
|
||||
line = stdout.readline()
|
||||
if not line:
|
||||
return None # EOF — server exited
|
||||
decoded = line.decode("ascii", errors="replace").strip()
|
||||
try:
|
||||
decoded = line.decode("ascii", errors="strict").strip()
|
||||
except UnicodeDecodeError as exc:
|
||||
raise LspError(
|
||||
f"LSP header contains non-ASCII bytes: {exc}",
|
||||
details={"raw_header": repr(line)},
|
||||
) from exc
|
||||
# Printable-ASCII guard: reject control characters
|
||||
# outside the range 0x20 (space) through 0x7E (tilde).
|
||||
if not all(0x20 <= ord(c) <= 0x7E for c in decoded):
|
||||
raise LspError(
|
||||
f"LSP header contains non-printable ASCII characters: "
|
||||
f"{decoded!r}",
|
||||
details={"raw_header": repr(line)},
|
||||
)
|
||||
if not decoded:
|
||||
break # Empty line = end of headers
|
||||
if decoded.lower().startswith("content-length:"):
|
||||
|
||||
Reference in New Issue
Block a user