Skip to content

Commit

Permalink
#812 native x11 clipboard improvements:
Browse files Browse the repository at this point in the history
* distinguish targets we never want to handle from the ones that we ignore when not explicitly exposed by the peer,
* use tuples for lists of atoms
* python3 string nonsense: convert all network metadata to strings,
* make timeout values configurable via env vars,
* filter atom targets in and out,
* cache 'TARGETS' data so we can detect spurious target requests and drop them

git-svn-id: https://xpra.org/svn/Xpra/trunk@22375 3bb7dfac-3a0b-4e04-842a-767bc560f471
  • Loading branch information
totaam committed Apr 11, 2019
1 parent bceea5a commit b348b10
Show file tree
Hide file tree
Showing 2 changed files with 82 additions and 44 deletions.
56 changes: 38 additions & 18 deletions src/xpra/clipboard/clipboard_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,21 +38,31 @@
LOOP_DISABLE = envbool("XPRA_CLIPBOARD_LOOP_DISABLE", True)
LOOP_PREFIX = os.environ.get("XPRA_CLIPBOARD_LOOP_PREFIX", "Xpra-Clipboard-Loop-Detection:")

def get_discard_targets():
_discard_target_strs_ = os.environ.get("XPRA_DISCARD_TARGETS")
def get_discard_targets(envname="DISCARD", default_value=()):
_discard_target_strs_ = os.environ.get("XPRA_%s_TARGETS" % envname)
if _discard_target_strs_ is None:
return [
r"^SAVE_TARGETS$",
r"^COMPOUND_TEXT",
r"^NeXT",
r"^com\.apple\.",
r"^CorePasteboardFlavorType",
r"^dyn\.",
r"^text/plain;charset=utf-8",
]
return default_value
return _discard_target_strs_.split(",")
DISCARD_TARGETS = tuple(re.compile(dt) for dt in get_discard_targets())
log("discard_targets=%s", csv(get_discard_targets()))
#targets we never wish to handle:
DISCARD_TARGETS = tuple(re.compile(dt) for dt in get_discard_targets("DISCARD", (
r"^NeXT",
r"^com\.apple\.",
r"^CorePasteboardFlavorType",
r"^dyn\.",
)))
#targets some applications are known to request,
#even when the peer did not expose them as valid targets,
#rather forwarding the request and then timing out,
#we will just drop them
DISCARD_EXTRA_TARGETS = tuple(re.compile(dt) for dt in get_discard_targets("DISCARD_EXTRA", (
r"^SAVE_TARGETS$",
r"^COMPOUND_TEXT",
r"GTK_TEXT_BUFFER_CONTENTS",
r"^text/plain;charset=utf-8",
)))
log("DISCARD_TARGETS=%s", csv(DISCARD_TARGETS))
log("DISCARD_EXTRA_TARGETS=%s", csv(DISCARD_EXTRA_TARGETS))


TEXT_TARGETS = ("UTF8_STRING", "TEXT", "STRING", "text/plain")

Expand All @@ -69,8 +79,12 @@ def get_discard_targets():
def must_discard(target):
return any(x for x in DISCARD_TARGETS if x.match(bytestostr(target)))

def must_discard_extra(target):
return any(x for x in DISCARD_EXTRA_TARGETS if x.match(bytestostr(target)))


def _filter_targets(targets):
f = [target for target in targets if not must_discard(target)]
f = tuple(target for target in targets if not must_discard(target))
log("_filter_targets(%s)=%s", targets, f)
return f

Expand Down Expand Up @@ -267,6 +281,9 @@ def _process_clipboard_token(self, packet):
targets = packet[2]
if len(packet)>=8:
target, dtype, dformat, wire_encoding, wire_data = packet[3:8]
target = bytestostr(target)
wire_encoding = bytestostr(wire_encoding)
dtype = bytestostr(dtype)
raw_data = self._munge_wire_selection_to_raw(wire_encoding, dtype, dformat, wire_data)
target_data = {target : (dtype, dformat, raw_data)}
#older versions always claimed the selection when the token is received:
Expand Down Expand Up @@ -333,11 +350,11 @@ def got_contents(dtype, dformat, data):
send_token(rsel)

def _munge_raw_selection_to_wire(self, target, dtype, dformat, data):
log("_munge_raw_selection_to_wire%s", (target, dtype, dformat, data))
log("_munge_raw_selection_to_wire%s", (target, dtype, dformat, repr_ellipsized(bytestostr(data))))
# Some types just cannot be marshalled:
if type in ("WINDOW", "PIXMAP", "BITMAP", "DRAWABLE",
"PIXEL", "COLORMAP"):
log("skipping clipboard data of type: %s, format=%s, len(data)=%s", dtype, dformat, len(data or ""))
log("skipping clipboard data of type: %s, format=%s, len(data)=%s", dtype, dformat, len(data or b""))
return None, None
if target=="TARGETS" and dtype=="ATOM":
#targets is special cased here
Expand Down Expand Up @@ -389,9 +406,9 @@ def _munge_wire_selection_to_raw(self, encoding, dtype, dformat, data):
olen = len(data)
data = data[:max_recv_datalen]
log.info("Data copied out truncated because of clipboard policy %d to %d", olen, max_recv_datalen)
if encoding == b"bytes":
if encoding == "bytes":
return data
if encoding == b"integers":
if encoding == "integers":
if not data:
return ""
if dformat == 32:
Expand Down Expand Up @@ -441,6 +458,7 @@ def no_contents():
log.warn("clipboard request %s dropped for testing!", request_id)
return
def got_contents(dtype, dformat, data):
dtype = bytestostr(dtype)
log("got_contents(%s, %s, %s:%s) data=0x%s..",
dtype, dformat, type(data), len(data or ""), hexstr((data or "")[:200]))
if dtype is None or data is None or (dformat==0 and data==b""):
Expand Down Expand Up @@ -481,6 +499,8 @@ def _may_compress(self, dtype, dformat, wire_data):
def _process_clipboard_contents(self, packet):
request_id, selection, dtype, dformat, wire_encoding, wire_data = packet[1:7]
log("process clipboard contents, selection=%s, type=%s, format=%s", selection, dtype, dformat)
wire_encoding = bytestostr(wire_encoding)
dtype = bytestostr(dtype)
raw_data = self._munge_wire_selection_to_raw(wire_encoding, dtype, dformat, wire_data)
log("clipboard wire -> raw: %r -> %r", (dtype, dformat, wire_encoding, wire_data), raw_data)
self._clipboard_got_contents(request_id, dtype, dformat, raw_data)
Expand Down
70 changes: 44 additions & 26 deletions src/xpra/x11/gtk_x11/clipboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,14 @@
)
from xpra.clipboard.clipboard_core import (
ClipboardProtocolHelperCore, ClipboardProxyCore,
must_discard,
must_discard, must_discard_extra, _filter_targets,
)
from xpra.x11.bindings.window_bindings import ( #@UnresolvedImport
constants, PropertyError,
X11WindowBindings,
)
from xpra.util import csv, repr_ellipsized
from xpra.os_util import bytestostr
from xpra.util import csv, repr_ellipsized, envint
from xpra.log import Logger

gdk = import_gdk()
Expand All @@ -41,17 +42,23 @@

sizeof_long = struct.calcsize(b'@L')

CONVERT_TIMEOUT = envint("XPRA_CLIPBOARD_CONVERT_TIMEOUT", 500)
REMOTE_TIMEOUT = envint("XPRA_CLIPBOARD_REMOTE_TIMEOUT", 1500)
assert 0<CONVERT_TIMEOUT<5000
assert 0<REMOTE_TIMEOUT<5000


def xatoms_to_strings(data):
l = len(data)
assert l%sizeof_long==0, "invalid length for atom array: %i" % l
natoms = l//sizeof_long
atoms = struct.unpack(b"@"+b"L"*natoms, data)
with xsync:
return [X11Window.XGetAtomName(atom) for atom in atoms]
return tuple(name for name in (bytestostr(X11Window.XGetAtomName(atom)) for atom in atoms if atom) if name is not None)

def strings_to_xatoms(data):
with xsync:
atom_array = [X11Window.get_xatom(atom) for atom in data]
atom_array = tuple(X11Window.get_xatom(atom) for atom in data if atom)
return struct.pack(b"@" + b"L" * len(atom_array), *atom_array)


Expand Down Expand Up @@ -178,7 +185,7 @@ def _send_clipboard_request_handler(self, proxy, selection, target):
request_id = self._clipboard_request_counter
self._clipboard_request_counter += 1
log("send_clipboard_request id=%s", request_id)
timer = glib.timeout_add(1500, self.timeout_request, request_id)
timer = glib.timeout_add(REMOTE_TIMEOUT, self.timeout_request, request_id)
self._clipboard_outstanding_requests[request_id] = (timer, selection, target)
self.progress()
self.send("clipboard-request", request_id, self.local_to_remote(selection), target)
Expand Down Expand Up @@ -214,13 +221,13 @@ def _clipboard_got_contents(self, request_id, dtype=None, dformat=None, data=Non


def _munge_raw_selection_to_wire(self, target, dtype, dformat, data):
if dformat==32 and dtype in (b"ATOM", b"ATOM_PAIR"):
return "atoms", xatoms_to_strings(data)
if dformat==32 and dtype in ("ATOM", "ATOM_PAIR"):
return "atoms", _filter_targets(xatoms_to_strings(data))
return ClipboardProtocolHelperCore._munge_raw_selection_to_wire(self, target, dtype, dformat, data)

def _munge_wire_selection_to_raw(self, encoding, dtype, dformat, data):
if dtype==b"ATOM":
return strings_to_xatoms(data)
if encoding=="atoms":
return strings_to_xatoms(_filter_targets(data))
return ClipboardProtocolHelperCore._munge_wire_selection_to_raw(self, encoding, dtype, dformat, data)

gobject.type_register(X11Clipboard)
Expand Down Expand Up @@ -287,13 +294,14 @@ def got_token(self, targets, target_data=None, claim=True, synchronous_client=Fa
log("got token, selection=%s, targets=%s, target data=%s, claim=%s, can-receive=%s",
self._selection, targets, target_data, claim, self._can_receive)
if self._can_receive:
self.targets = targets
self.targets = tuple(bytestostr(x) for x in (targets or ()))
self.target_data = target_data or {}
if targets:
self.got_contents("TARGETS", "ATOM", 32, targets)
if target_data and synchronous_client:
target = target_data.keys()[0]
dtype, dformat, data = target_data.get(target)
dtype = bytestostr(dtype)
self.got_contents(target, dtype, dformat, data)
if not claim:
log("token packet without claim, not setting the token flag")
Expand Down Expand Up @@ -361,7 +369,7 @@ def do_selection_request_event(self, event):
log("do_selection_request_event(%s)", event)
requestor = event.requestor
assert requestor
log("clipboard request for %s from window %#x: '%s'",
log("clipboard request for %s from window %#x: %s",
self._selection, get_xwindow(requestor), self.get_wininfo(get_xwindow(requestor)))
prop = event.property
target = str(event.target)
Expand Down Expand Up @@ -400,11 +408,16 @@ def nodata():
if self.targets and target not in self.targets:
log.info("client is requesting an unknown target: '%s'", target)
log.info(" valid targets: %s", csv(self.targets))
if must_discard_extra(target):
log.info(" dropping the request")
nodata()
return

target_data = self.target_data.get(target)
if target_data:
#we have it already
dtype, dformat, data = target_data
dtype = bytestostr(dtype)
log("setting target data for '%s': %s, %s, %s (%s)",
target, dtype, dformat, repr_ellipsized(str(data)), type(data))
self.set_selection_response(requestor, target, prop, dtype, dformat, data, event.time)
Expand All @@ -429,6 +442,9 @@ def set_selection_response(self, requestor, target, prop, dtype, dformat, data,
X11Window.sendSelectionNotify(xid, self._selection, target, prop, time)

def got_contents(self, target, dtype=None, dformat=None, data=None):
#if this is the special target 'TARGETS', cache the result:
if target=="TARGETS" and dtype=="ATOM" and dformat==32:
self.targets = xatoms_to_strings(data)
#the remote peer sent us a response,
#find all the pending requests for this target
#and give them the response they are waiting for:
Expand Down Expand Up @@ -477,21 +493,22 @@ def do_owner_changed(self):
def get_contents(self, target, got_contents, time=0):
log("get_contents(%s, %s, %i) owned=%s, have-token=%s",
target, got_contents, time, self.owned, self._have_token)
if target=="TARGETS":
if self.targets:
xatoms = strings_to_xatoms(self.targets)
got_contents("ATOM", 32, xatoms)
return
else:
target_data = self.target_data.get(target)
if target_data:
dtype, dformat, value = target_data
got_contents(dtype, dformat, value)
return
if False:
if target=="TARGETS":
if self.targets:
xatoms = strings_to_xatoms(self.targets)
got_contents("ATOM", 32, xatoms)
return
else:
target_data = self.target_data.get(target)
if target_data:
dtype, dformat, value = target_data
got_contents(dtype, dformat, value)
return
prop = "%s-%s" % (self._selection, target)
request_id = self.local_request_counter
self.local_request_counter += 1
timer = glib.timeout_add(1000, self.timeout_get_contents, target, request_id)
timer = glib.timeout_add(CONVERT_TIMEOUT, self.timeout_get_contents, target, request_id)
self.local_requests.setdefault(target, {})[request_id] = (timer, got_contents, time)
with xsync:
owner = X11Window.XGetSelectionOwner(self._selection)
Expand All @@ -509,15 +526,15 @@ def timeout_get_contents(self, target, request_id):
except KeyError:
return
glib.source_remove(timer)
log.warn("Warning: clipboard request for '%s' timed out", target)
log.warn("Warning: %s selection request for '%s' timed out", self._selection, target)
log.warn(" request %i at time=%i", request_id, time)
if target=="TARGETS":
got_contents("ATOM", 32, b"")
else:
got_contents(None, None, None)

def do_property_notify(self, event):
log("property_notify(%s)", event)
log("do_property_notify(%s)", event)
#ie: atom="PRIMARY-TARGETS", atom="PRIMARY-STRING"
parts = event.atom.split("-", 1)
assert len(parts)==2
Expand All @@ -526,13 +543,14 @@ def do_property_notify(self, event):
try:
with xsync:
dtype, dformat = X11Window.GetWindowPropertyType(self.xid, event.atom)
dtype = bytestostr(dtype)
MAX_DATA_SIZE = 4*1024*1024
data = X11Window.XGetWindowProperty(self.xid, event.atom, dtype, None, MAX_DATA_SIZE)
X11Window.XDeleteProperty(self.xid, event.atom)
except PropertyError:
log("do_property_notify() property '%s' is gone?", event.atom, exc_info=True)
return
log("%s=%s (%s : %s)", event.atom, repr_ellipsized(str(data)), dtype, dformat)
log("%s=%s (%s : %s)", event.atom, repr_ellipsized(bytestostr(data)), dtype, dformat)
if target=="TARGETS":
self.targets = data or ()
self.got_local_contents(target, dtype, dformat, data)
Expand Down

0 comments on commit b348b10

Please sign in to comment.