Dedupe WebCRT command audit on double-fired Enter.

xterm and keydown can both emit carriage return for one keypress; skip duplicate Enter frames on the client and collapse identical audits within 450ms on the server.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-02 14:25:27 +08:00
parent 7ca57278fe
commit 52424dce37
3 changed files with 58 additions and 1 deletions

View file

@ -96,10 +96,25 @@ class WebcrtSession:
_password_mode: bool = field(default=False, repr=False) _password_mode: bool = field(default=False, repr=False)
_stdout_tail: str = field(default="", repr=False) _stdout_tail: str = field(default="", repr=False)
_last_prompt_line: str = field(default="", repr=False) _last_prompt_line: str = field(default="", repr=False)
_last_audited_command: str = field(default="", repr=False)
_last_audited_at: float = field(default=0.0, repr=False)
def touch(self) -> None: def touch(self) -> None:
self.last_activity = time.time() self.last_activity = time.time()
def _should_emit_command_audit(self, cmd: str, *, dedup_sec: float = 0.45) -> bool:
"""Drop duplicate audits from double-fired Enter (same command within dedup_sec)."""
norm = str(cmd or "").strip()
if not norm:
return False
now = time.time()
with self._cmd_buf_lock:
if norm == self._last_audited_command and (now - self._last_audited_at) < dedup_sec:
return False
self._last_audited_command = norm
self._last_audited_at = now
return True
def push_connect_echo(self, text: str) -> None: def push_connect_echo(self, text: str) -> None:
"""Queue login transcript for the WS wait-loop (thread-safe).""" """Queue login transcript for the WS wait-loop (thread-safe)."""
chunk = str(text or "") chunk = str(text or "")
@ -398,6 +413,8 @@ class WebcrtSession:
for cmd in lines: for cmd in lines:
if not str(cmd).strip(): if not str(cmd).strip():
continue continue
if not self._should_emit_command_audit(cmd):
continue
try: try:
_audit( _audit(
"command", "command",

View file

@ -282,6 +282,30 @@ class SessionCommandAuditTests(unittest.TestCase):
sess.write_stdin("\r", audit_line="AL5458-ACC-6120HS(config-if-loopback127)#") sess.write_stdin("\r", audit_line="AL5458-ACC-6120HS(config-if-loopback127)#")
mock_audit.assert_not_called() mock_audit.assert_not_called()
@patch("netx_api.webcrt_session_model._audit")
def test_duplicate_enter_same_command_deduped(self, mock_audit: MagicMock) -> None:
conn = MagicMock()
conn.RETURN = "\n"
conn.remote_conn = MagicMock(spec=["recv_ready", "recv", "exit_status_ready", "resize_pty"])
del conn.remote_conn.send
conn.write_channel = MagicMock()
sess = WebcrtSession(
session_id="s-dedup",
ne_id="ne1",
ne_name="lab",
ne_ip="1.2.3.4",
protocol="ssh",
cols=80,
rows=24,
cli_keymap=False,
conn=conn,
)
line = "AL5458-ACC-6120HS(config)#intt"
sess.write_stdin("\r", audit_line=line)
sess.write_stdin("\r", audit_line=line)
self.assertEqual(mock_audit.call_count, 1)
@patch("netx_api.webcrt_session_model._audit") @patch("netx_api.webcrt_session_model._audit")
def test_password_mode_redacts_command(self, mock_audit: MagicMock) -> None: def test_password_mode_redacts_command(self, mock_audit: MagicMock) -> None:
conn = MagicMock() conn = MagicMock()

View file

@ -269,6 +269,8 @@ export const WebTerminal = forwardRef<WebTerminalHandle, Props>(function WebTerm
const pasteBridgeOpenRef = useRef(false); const pasteBridgeOpenRef = useRef(false);
pasteBridgeOpenRef.current = pasteBridgeOpen; pasteBridgeOpenRef.current = pasteBridgeOpen;
const findInputRef = useRef<HTMLInputElement | null>(null); const findInputRef = useRef<HTMLInputElement | null>(null);
/** Suppress duplicate Enter frames (keydown + xterm onData both fire). */
const enterHandledAtRef = useRef(0);
useEffect(() => { useEffect(() => {
onStatusRef.current = onStatus; onStatusRef.current = onStatus;
@ -338,6 +340,16 @@ export const WebTerminal = forwardRef<WebTerminalHandle, Props>(function WebTerm
sendJson(payload); sendJson(payload);
}; };
const sendEnterStdin = (term: Terminal, explicitAuditLine?: string) => {
enterHandledAtRef.current = performance.now();
sendStdinWithAudit("\r", explicitAuditLine ?? auditLineForEnter(term));
};
const isDuplicateEnterFrame = (data: string): boolean => {
if (!/^[\r\n]+$/.test(data)) return false;
return performance.now() - enterHandledAtRef.current < 120;
};
const sendStdinImmediate = (data: string) => { const sendStdinImmediate = (data: string) => {
sendStdinWithAudit(data); sendStdinWithAudit(data);
}; };
@ -705,11 +717,15 @@ export const WebTerminal = forwardRef<WebTerminalHandle, Props>(function WebTerm
const dataDisposable = term.onData((data) => { const dataDisposable = term.onData((data) => {
const normalized = data.replace(/\x7f/g, "\x08"); const normalized = data.replace(/\x7f/g, "\x08");
if (isDuplicateEnterFrame(normalized)) return;
// Capture visible line before Enter moves the cursor to the next row. // Capture visible line before Enter moves the cursor to the next row.
const auditLine = const auditLine =
normalized.includes("\r") || normalized.includes("\n") normalized.includes("\r") || normalized.includes("\n")
? auditLineForEnter(term) ? auditLineForEnter(term)
: undefined; : undefined;
if (normalized.includes("\r") || normalized.includes("\n")) {
enterHandledAtRef.current = performance.now();
}
// Large pastes from xterm arrive as one onData blob. // Large pastes from xterm arrive as one onData blob.
if (normalized.length > 32 || normalized.includes("\r") || normalized.includes("\n")) { if (normalized.length > 32 || normalized.includes("\r") || normalized.includes("\n")) {
sendStdinThrottled(normalized, auditLine); sendStdinThrottled(normalized, auditLine);
@ -783,7 +799,7 @@ export const WebTerminal = forwardRef<WebTerminalHandle, Props>(function WebTerm
e.stopPropagation(); e.stopPropagation();
maybeFocus(); maybeFocus();
if (e.key === "Enter") { if (e.key === "Enter") {
sendStdinWithAudit("\r", auditLineForEnter(term)); sendEnterStdin(term, auditLineForEnter(term));
return; return;
} }
sendStdinWithAudit(data); sendStdinWithAudit(data);