Skip to content

Commit 864ef70

Browse files
committed
Have the client send machine id to terminal rather than vice versa
The terminal is the trusted by user party here.
1 parent 9ebe692 commit 864ef70

7 files changed

Lines changed: 88 additions & 61 deletions

File tree

docs/dnd-protocol.rst

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -48,16 +48,6 @@ The list of MIME types is optional, it is needed if the program wants to accept
4848
exotic or private use MIME types on platforms such as macOS, where the system
4949
does not deliver drop events unless the MIME type is registered.
5050

51-
The terminal emulator may respond to this escape code with an escape code of
52-
the form::
53-
54-
OSC _dnd_code ; t=a ; machine id ST
55-
56-
Here, the :ref:`machine id <machine_id>` is an id that identifies the machine
57-
the terminal is running on and can be used by the client to determine whether
58-
to request remote files from the terminal when a drop occurs.
59-
See :ref:`below <machine_id>` for the semantics of the machine id.
60-
6151
When the client is done accepting drops, or at exit, it should send the escape
6252
code::
6353

@@ -143,12 +133,17 @@ Dropping from remote machines
143133
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
144134

145135
In order to support dropping of files from remote machines, the client
146-
can use the :ref:`machine id <machine_id>` previously sent by the terminal.
147-
If it is different from the id of the machine the client is running on, it
148-
can choose to request remote files, as follows.
136+
must inform the terminal of its :ref:`machine id <machine_id>` using the escape code::
137+
138+
OSC _dnd_code ; t=a:x=1 ; machine id ST
149139

150-
Clients can first request the :rfc:`text/uri-list <2483>` MIME
151-
type to get a list of dropped URIs. For every URI in the list, they can
140+
Then, the client must first request the :rfc:`text/uri-list <2483>` MIME
141+
type to get a list of dropped URIs. When responding to this request,
142+
the terminal will send the usual ``t=r`` responses, but, in addition,
143+
if the client has sent its machine id and the terminal determines that
144+
the client is on a different machine based on the id, it will add the ``X=1``
145+
key to its response. The client should use this key to determine if it wants to
146+
request data for entries in the URI list. For every URI in the list, the client can
152147
send the terminal emulator a data request of the form::
153148

154149
OSC _dnd_code ; t=r:x=idx:y=subidx ST
@@ -486,11 +481,22 @@ Key Value Default Description
486481
Machine id
487482
-----------------
488483

489-
The machine id is used to detect when a drag is started on a remote machine. It
490-
is of the form: ``version:ASCII printable chars``. The leading ``version`` field
491-
allows for changing the format or semantics of this field in the future. The
492-
actual id is the machine id (the contents of :file:`/etc/machine-id` on
493-
Linux/BSD and :file:`HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography\\MachineGuid` on Windows and ``IOPlatformUUID`` on macOS). This machine id is then hashed using a :rfc:`HMAC <2104>`
494-
with :rfc:`SHA-256 <6234>` as the digest algorithm and the key being the ASCII bytes:
495-
``tty-dnd-protocol-machine-id``. The hashing is done so as to not easily leak the
496-
actual machine id and to ensure that the value is of fixed size.
484+
The machine id is used to detect when the source and destination machines for a
485+
drag and drop are different. It is of the form: ``version:ASCII printable
486+
chars``. The leading ``version`` field allows for changing the format or
487+
semantics of this field in the future. The actual id is the machine id (the
488+
contents of :file:`/etc/machine-id` on Linux/BSD and
489+
:file:`HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography\\MachineGuid` on
490+
Windows and ``IOPlatformUUID`` on macOS). This machine id is then hashed using
491+
a :rfc:`HMAC <2104>` with :rfc:`SHA-256 <6234>` as the digest algorithm and the
492+
key being the ASCII bytes: ``tty-dnd-protocol-machine-id``. The hashing is done
493+
so as to not easily leak the actual machine id and to ensure that the value is
494+
of fixed size. This gives a final value of::
495+
496+
1:hashed machine id hexadecimal encoded
497+
498+
In the future, the ``version`` field may increase if the hashing algorithm is
499+
changed. If the terminal sees a version it does not understand, it must assume
500+
that the machine id does not match, aka the source and destination machines are
501+
different. This assumption means that remote drag and drop will still work, just with
502+
reduced performance in case of version mismatch.

kitty/dnd.c

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -136,11 +136,13 @@ drop_free_data(Window *w) {
136136
static void
137137
reset_drop(Window *w) {
138138
bool wanted = w->drop.wanted; uint32_t cid = w->drop.client_id;
139+
bool is_remote_client = w->drop.is_remote_client;
139140
drop_free_data(w);
140141
zero_at_ptr(&w->drop);
141142
if (wanted) {
142143
w->drop.wanted = wanted;
143144
w->drop.client_id = cid;
145+
w->drop.is_remote_client = is_remote_client;
144146
}
145147
}
146148

@@ -255,10 +257,23 @@ queue_payload_to_child(id_type id, uint32_t client_id, PendingData *pending, con
255257
if (pending->count) check_for_pending_writes();
256258
}
257259

260+
static bool
261+
is_same_machine(const char *client_machine_id, size_t sz) {
262+
if (!sz || !client_machine_id) return true;
263+
if (sz < 20) return false;
264+
if (client_machine_id[0] != '1' || client_machine_id[1] != ':') return false;
265+
client_machine_id = client_machine_id + 2; sz -= 2;
266+
const char *host_machine_id = machine_id();
267+
if (!host_machine_id) return true;
268+
const size_t hsz = strlen(host_machine_id);
269+
return sz == hsz && memcmp(client_machine_id, host_machine_id, sz) == 0;
270+
}
271+
258272
void
259273
drop_register_window(Window *w, const uint8_t *payload, size_t payload_sz, bool on, uint32_t client_id, bool more) {
260274
w->drop.wanted = on;
261275
w->drop.client_id = client_id;
276+
w->drop.is_remote_client = false;
262277
if (!on) { drop_free_data(w); zero_at_ptr(&w->drop); return; }
263278
if (!payload || !payload_sz) return;
264279
size_t sz = w->drop.registered_mimes ? strlen(w->drop.registered_mimes) : 0;
@@ -286,13 +301,11 @@ drop_register_window(Window *w, const uint8_t *payload, size_t payload_sz, bool
286301
}
287302
}
288303
free(w->drop.registered_mimes); w->drop.registered_mimes = NULL;
289-
const char* host_machine_id = machine_id();
290-
if (host_machine_id) {
291-
char header[32] = {0};
292-
int n = snprintf(header, sizeof(header), "\x1b]%d;t=a", DND_CODE);
293-
queue_payload_to_child(
294-
w->id, w->drop.client_id, &w->drop.pending, header, n, host_machine_id, strlen(host_machine_id), false);
295-
}
304+
}
305+
306+
void
307+
drop_register_machine_id(Window *w, const uint8_t *machine_id, size_t sz) {
308+
w->drop.is_remote_client = !is_same_machine((const char*)machine_id, sz);
296309
}
297310

298311
void
@@ -471,9 +484,12 @@ drop_dispatch_data(Window *w, const char *mime, const char *data, ssize_t sz) {
471484
} else {
472485
char buf[128];
473486
int header_size = snprintf(buf, sizeof(buf), "\x1b]%d;t=r", DND_CODE);
487+
const bool is_uri_list = strcmp(mime, "text/uri-list") == 0;
488+
if (is_uri_list) header_size += snprintf(
489+
buf + header_size, sizeof(buf) - header_size, ":X=%d", w->drop.is_remote_client);
474490
header_size += drop_append_request_keys(w, buf + header_size, sizeof(buf) - header_size);
475491
queue_payload_to_child(w->id, w->drop.client_id, &w->drop.pending, buf, header_size, sz ? data : NULL, sz, true);
476-
if (strcmp(mime, "text/uri-list") == 0) {
492+
if (is_uri_list) {
477493
w->drop.uri_list_sz += sz;
478494
w->drop.uri_list = realloc(w->drop.uri_list, w->drop.uri_list_sz);
479495
if (w->drop.uri_list) memcpy(w->drop.uri_list + w->drop.uri_list_sz - sz, data, sz);
@@ -1102,14 +1118,8 @@ cancel_drag(Window *w, int error_code) {
11021118

11031119
void
11041120
drag_start_offerring(Window *w, const char *client_machine_id, size_t sz) {
1105-
ds.can_offer = true; ds.is_remote_client = false;
1106-
if (sz && client_machine_id) {
1107-
const char *host_machine_id = machine_id();
1108-
if (host_machine_id) {
1109-
size_t hsz = strlen(host_machine_id);
1110-
if (hsz != sz || memcmp(host_machine_id, client_machine_id, sz) != 0) ds.is_remote_client = true;
1111-
}
1112-
}
1121+
ds.can_offer = true;
1122+
ds.is_remote_client = !is_same_machine(client_machine_id, sz);
11131123
}
11141124

11151125
void

kitty/dnd.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111

1212
void drop_register_window(Window *w, const uint8_t *payload, size_t payload_sz, bool on, uint32_t client_id, bool more);
13+
void drop_register_machine_id(Window *w, const uint8_t *machine_id, size_t sz);
1314
void drop_move_on_child(Window *w, const char **mimes, size_t num_mimes, bool is_drop);
1415
void drop_left_child(Window *w);
1516
void drop_free_data(Window *w);

kitty/fast_data_types.pyi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1832,7 +1832,7 @@ class StreamingBase64Decoder:
18321832
def needs_more_data(self) -> bool: ...
18331833

18341834

1835-
class StreamingBase64Encodeer:
1835+
class StreamingBase64Encoder:
18361836
def __init__(self, add_trailing_bytes: bool = True) -> None: ...
18371837
# encode the specified data
18381838
def encode(self, data: ReadableBuffer) -> bytes: ...

kitty/screen.c

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1528,7 +1528,10 @@ screen_handle_dnd_command(Screen *self, const DnDCommand *cmd, const uint8_t *pa
15281528
Window *w = window_for_window_id(self->window_id);
15291529
if (!w) return;
15301530
switch(cmd->type) {
1531-
case 'a': drop_register_window(w, payload, cmd->payload_sz, true, cmd->client_id, cmd->more); break;
1531+
case 'a':
1532+
if (cmd->cell_x == 1) drop_register_machine_id(w, payload, cmd->payload_sz);
1533+
else drop_register_window(w, payload, cmd->payload_sz, true, cmd->client_id, cmd->more);
1534+
break;
15321535
case 'A': drop_register_window(w, NULL, 0, false, cmd->client_id, cmd->more); break;
15331536
case 'm': drop_set_status(w, cmd->operation, (const char*)payload, cmd->payload_sz, cmd->more); break;
15341537
case 'r': {

kitty/state.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ typedef struct Window {
269269
bool is_hovering;
270270
} scrollbar;
271271
struct {
272-
bool wanted, hovered, dropped;
272+
bool wanted, hovered, dropped, is_remote_client;
273273
uint32_t client_id;
274274
char *registered_mimes;
275275
char *uri_list; size_t uri_list_sz;

kitty_tests/dnd.py

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@
33

44
import errno
55
import re
6-
from base64 import standard_b64decode, standard_b64encode
6+
from base64 import standard_b64encode
77
from contextlib import contextmanager
88
from functools import partial
99

1010
from kitty.fast_data_types import (
1111
DND_CODE,
1212
Screen,
13+
StreamingBase64Decoder,
1314
dnd_set_test_write_func,
1415
dnd_test_cleanup_fake_window,
1516
dnd_test_create_fake_window,
@@ -30,10 +31,6 @@ def _osc(payload: str) -> bytes:
3031

3132
def client_register(mimes: str = '', client_id: int = 0) -> bytes:
3233
"""Escape code a client sends to start accepting drops (t=a)."""
33-
meta = f'{DND_CODE};t=a'
34-
if client_id:
35-
meta += f':i={client_id}'
36-
return _osc(f'{meta};{mimes}')
3734

3835

3936
def client_unregister(client_id: int = 0) -> bytes:
@@ -257,14 +254,17 @@ def parse_escape_codes_b64(data: bytes) -> list[dict]:
257254
"""Like *parse_escape_codes* but base64-decodes each chunk's payload."""
258255
result = parse_escape_codes(data)
259256
for entry in result:
257+
d = StreamingBase64Decoder()
258+
decoded = b''
260259
decoded_chunks = []
261-
full = b''
262-
for chunk in entry['chunks']:
263-
dec = standard_b64decode(chunk + b'==') if chunk else b''
260+
for c in entry['chunks']:
261+
dec = d.decode(c)
264262
decoded_chunks.append(dec)
265-
full += dec
263+
decoded += dec
264+
# if d.needs_more_data():
265+
# raise AssertionError('Incomplete base64 data')
266+
entry['payload'] = decoded
266267
entry['chunks'] = decoded_chunks
267-
entry['payload'] = full
268268
return result
269269

270270

@@ -332,13 +332,19 @@ class TestDnDProtocol(BaseTest):
332332
def _assert_no_output(self, capture: _WriteCapture, window_id: int) -> None:
333333
self.ae(capture.peek(window_id), b'', 'unexpected output to child')
334334

335-
def _register_for_drops(self, screen, cap, wid, mimes='text/plain text/uri-list', client_id=0) -> None:
336-
parse_bytes(screen, client_register(mimes, client_id=client_id))
337-
events = self._get_events(cap, wid)
338-
self.assertEqual(len(events), 1, events)
339-
self.ae(events[0]['type'], 'a')
340-
self.ae(events[0]['payload'].strip().decode(), machine_id())
341-
335+
def _register_for_drops(
336+
self, screen, cap, wid, mimes='text/plain text/uri-list', client_id=0, register_machine_id=True
337+
) -> None:
338+
meta = f'{DND_CODE};t=a'
339+
if client_id:
340+
meta += f':i={client_id}'
341+
r = _osc(f'{meta};{mimes}')
342+
parse_bytes(screen, r)
343+
if register_machine_id:
344+
if not isinstance(register_machine_id, str):
345+
register_machine_id = machine_id()
346+
parse_bytes(screen, _osc(f'{DND_CODE};t=a:x=1;1:{register_machine_id}'))
347+
self._assert_no_output(cap, wid)
342348

343349
def _get_events(self, capture: _WriteCapture, window_id: int) -> list[dict]:
344350
return parse_escape_codes(capture.consume(window_id))
@@ -642,15 +648,16 @@ def _setup_uri_drop(self, screen, wid, cap, uri_list_data: bytes, mimes=None):
642648
"""Register, drop, deliver text/uri-list data, discard move/drop events."""
643649
if mimes is None:
644650
mimes = ['text/plain', 'text/uri-list']
645-
self._register_for_drops(screen, cap, wid, 'text/plain text/uri-list')
651+
self._register_for_drops(screen, cap, wid, 'text/plain text/uri-list', register_machine_id='remote')
646652
dnd_test_set_mouse_pos(wid, 0, 0, 0, 0)
647653
dnd_test_fake_drop_event(wid, True, mimes)
648654
cap.consume(wid)
649655
# Client requests and receives the URI list (idx=2 for text/uri-list in the default MIME list)
650656
uri_idx = mimes.index('text/uri-list') + 1 # 1-based
651657
parse_bytes(screen, client_request_data(uri_idx))
652658
dnd_test_fake_drop_data(wid, 'text/uri-list', uri_list_data)
653-
cap.consume(wid) # discard t=r data for text/uri-list
659+
events = parse_escape_codes_b64(cap.consume(wid))
660+
self.assertEqual(events[0]['meta']['X'], '1')
654661

655662
def test_uri_file_transfer_basic(self) -> None:
656663
"""URI file request sends the content of a regular file as t=r chunks."""

0 commit comments

Comments
 (0)