Coverage for /home/antoine/projects/xpra-git/dist/python3/lib64/python/xpra/net/file_transfer.py : 20%
Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# This file is part of Xpra.
2# Copyright (C) 2010-2020 Antoine Martin <antoine@xpra.org>
3# Copyright (C) 2008, 2010 Nathaniel Smith <njs@pobox.com>
4# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
5# later version. See the file COPYING for details.
7import os
8import subprocess
9import hashlib
10import uuid
12from xpra.child_reaper import getChildReaper
13from xpra.os_util import monotonic_time, bytestostr, strtobytes, umask_context, POSIX, WIN32
14from xpra.util import typedict, csv, envint, envbool, engs
15from xpra.scripts.config import parse_bool, parse_with_unit
16from xpra.simple_stats import std_unit
17from xpra.make_thread import start_thread
18from xpra.log import Logger
20printlog = Logger("printing")
21filelog = Logger("file")
23DELETE_PRINTER_FILE = envbool("XPRA_DELETE_PRINTER_FILE", True)
24FILE_CHUNKS_SIZE = max(0, envint("XPRA_FILE_CHUNKS_SIZE", 65536))
25MAX_CONCURRENT_FILES = max(1, envint("XPRA_MAX_CONCURRENT_FILES", 10))
26PRINT_JOB_TIMEOUT = max(60, envint("XPRA_PRINT_JOB_TIMEOUT", 3600))
27SEND_REQUEST_TIMEOUT = max(300, envint("XPRA_SEND_REQUEST_TIMEOUT", 3600))
28CHUNK_TIMEOUT = 10*1000
30MIMETYPE_EXTS = {
31 "application/postscript" : "ps",
32 "application/pdf" : "pdf",
33 "raw" : "raw",
34 }
36DENY = 0
37ACCEPT = 1 #the file / URL will be sent
38OPEN = 2 #don't send, open on sender
40def osclose(fd):
41 try:
42 os.close(fd)
43 except OSError as e:
44 filelog("os.close(%s)", fd, exc_info=True)
45 filelog.error("Error closing file download:")
46 filelog.error(" %s", e)
48def basename(filename):
49 #we can't use os.path.basename,
50 #because the remote end may have sent us a filename
51 #which is using a different pathsep
52 tmp = filename
53 for sep in ("\\", "/", os.sep):
54 i = tmp.rfind(sep) + 1
55 tmp = tmp[i:]
56 filename = tmp
57 if WIN32: # pragma: no cover
58 #many characters aren't allowed at all on win32:
59 tmp = ""
60 for char in filename:
61 if ord(char)<32 or char in ("<", ">", ":", "\"", "|", "?", "*"):
62 char = "_"
63 tmp += char
64 return tmp
66def safe_open_download_file(basefilename, mimetype):
67 from xpra.platform.paths import get_download_dir
68 dd = os.path.expanduser(get_download_dir())
69 filename = os.path.abspath(os.path.join(dd, basename(basefilename)))
70 ext = MIMETYPE_EXTS.get(mimetype)
71 if ext and not filename.endswith("."+ext):
72 #on some platforms (win32),
73 #we want to force an extension
74 #so that the file manager can display them properly when you double-click on them
75 filename += "."+ext
76 #make sure we use a filename that does not exist already:
77 root, ext = os.path.splitext(filename)
78 base = 0
79 while os.path.exists(filename):
80 filelog("cannot save file as %r: file already exists", filename)
81 base += 1
82 filename = root+("-%s" % base)+ext
83 filelog("safe_open_download_file(%s, %s) will use %r", basefilename, mimetype, filename)
84 flags = os.O_CREAT | os.O_RDWR | os.O_EXCL
85 try:
86 flags |= os.O_BINARY #@UndefinedVariable (win32 only)
87 except AttributeError:
88 pass
89 with umask_context(0o133):
90 fd = os.open(filename, flags)
91 filelog("using filename '%s', file descriptor=%s", filename, fd)
92 return filename, fd
94def s(v):
95 try:
96 return v.decode("utf8")
97 except (AttributeError, UnicodeDecodeError):
98 return bytestostr(v)
100def utf8_decode(url):
101 try:
102 return strtobytes(url).decode("utf8")
103 except UnicodeDecodeError:
104 return bytestostr(url)
107class FileTransferAttributes:
109 def __init__(self):
110 self.init_attributes()
112 def init_opts(self, opts, can_ask=True):
113 #get the settings from a config object
114 self.init_attributes(opts.file_transfer, opts.file_size_limit,
115 opts.printing, opts.open_files, opts.open_url, opts.open_command, can_ask)
117 def init_attributes(self, file_transfer="no", file_size_limit=10, printing="no",
118 open_files="no", open_url="no", open_command=None, can_ask=True):
119 filelog("file transfer: init_attributes%s",
120 (file_transfer, file_size_limit, printing, open_files, open_url, open_command, can_ask))
121 def pbool(name, v):
122 return parse_bool(name, v, True)
123 def pask(v):
124 return v.lower() in ("ask", "auto")
125 fta = pask(file_transfer)
126 self.file_transfer_ask = fta and can_ask
127 self.file_transfer = fta or pbool("file-transfer", file_transfer)
128 self.file_size_limit = parse_with_unit("file-size-limit", file_size_limit, "B", min_value=0)
129 self.file_chunks = FILE_CHUNKS_SIZE
130 pa = pask(printing)
131 self.printing_ask = pa and can_ask
132 self.printing = pa or pbool("printing", printing)
133 ofa = pask(open_files)
134 self.open_files_ask = ofa and can_ask
135 self.open_files = ofa or pbool("open-files", open_files)
136 #FIXME: command line options needed here:
137 oua = pask(open_url)
138 self.open_url_ask = oua and can_ask
139 self.open_url = oua or pbool("open-url", open_url)
140 self.file_ask_timeout = SEND_REQUEST_TIMEOUT
141 self.open_command = open_command
142 self.files_requested = {}
143 self.files_accepted = {}
144 self.file_request_callback = {}
145 filelog("file transfer attributes=%s", self.get_file_transfer_features())
147 def get_file_transfer_features(self) -> dict:
148 #used in hello packets
149 return {
150 "file-transfer" : self.file_transfer,
151 "file-transfer-ask" : self.file_transfer_ask,
152 "file-size-limit" : self.file_size_limit//1024//1024, #legacy name (use max-file-size)
153 "max-file-size" : self.file_size_limit,
154 "file-chunks" : self.file_chunks,
155 "open-files" : self.open_files,
156 "open-files-ask" : self.open_files_ask,
157 "printing" : self.printing,
158 "printing-ask" : self.printing_ask,
159 "open-url" : self.open_url,
160 "open-url-ask" : self.open_url_ask,
161 "file-ask-timeout" : self.file_ask_timeout,
162 }
164 def get_info(self) -> dict:
165 #slightly different from above... for legacy reasons
166 #this one is used for get_info() in a proper "file." namespace from server_base.py
167 return {
168 "enabled" : self.file_transfer,
169 "ask" : self.file_transfer_ask,
170 "size-limit" : self.file_size_limit,
171 "chunks" : self.file_chunks,
172 "open" : self.open_files,
173 "open-ask" : self.open_files_ask,
174 "open-url" : self.open_url,
175 "open-url-ask" : self.open_url_ask,
176 "printing" : self.printing,
177 "printing-ask" : self.printing_ask,
178 "ask-timeout" : self.file_ask_timeout,
179 }
182class FileTransferHandler(FileTransferAttributes):
183 """
184 Utility class for receiving files and optionally printing them,
185 used by both clients and server to share the common code and attributes
186 """
188 def init_attributes(self, *args):
189 super().init_attributes(*args)
190 self.remote_file_transfer = False
191 self.remote_file_transfer_ask = False
192 self.remote_printing = False
193 self.remote_printing_ask = False
194 self.remote_open_files = False
195 self.remote_open_files_ask = False
196 self.remote_open_url = False
197 self.remote_open_url_ask = False
198 self.remote_file_ask_timeout = SEND_REQUEST_TIMEOUT
199 self.remote_file_size_limit = 0
200 self.remote_file_chunks = 0
201 self.pending_send_data = {}
202 self.pending_send_data_timers = {}
203 self.send_chunks_in_progress = {}
204 self.receive_chunks_in_progress = {}
205 self.file_descriptors = set()
206 if not getattr(self, "timeout_add", None):
207 from gi.repository import GLib
208 self.timeout_add = GLib.timeout_add
209 self.idle_add = GLib.idle_add
210 self.source_remove = GLib.source_remove
212 def cleanup(self):
213 for t in self.pending_send_data_timers.values():
214 self.source_remove(t)
215 self.pending_send_data_timers = {}
216 for v in self.receive_chunks_in_progress.values():
217 t = v[-2]
218 self.source_remove(t)
219 self.receive_chunks_in_progress = {}
220 for x in tuple(self.file_descriptors):
221 try:
222 os.close(x)
223 except OSError:
224 pass
225 self.file_descriptors = set()
226 self.init_attributes()
228 def parse_file_transfer_caps(self, c):
229 self.remote_file_transfer = c.boolget("file-transfer")
230 self.remote_file_transfer_ask = c.boolget("file-transfer-ask")
231 self.remote_printing = c.boolget("printing")
232 self.remote_printing_ask = c.boolget("printing-ask")
233 self.remote_open_files = c.boolget("open-files")
234 self.remote_open_files_ask = c.boolget("open-files-ask")
235 self.remote_open_url = c.boolget("open-url")
236 self.remote_open_url_ask = c.boolget("open-url-ask")
237 self.remote_file_ask_timeout = c.intget("file-ask-timeout")
238 self.remote_file_size_limit = c.intget("max-file-size") or c.intget("file-size-limit")*1024*1024
239 self.remote_file_chunks = max(0, min(self.remote_file_size_limit, c.intget("file-chunks")))
240 self.dump_remote_caps()
242 def dump_remote_caps(self):
243 filelog("file transfer remote caps: file-transfer=%-5s (ask=%s)",
244 self.remote_file_transfer, self.remote_file_transfer_ask)
245 filelog("file transfer remote caps: printing=%-5s (ask=%s)",
246 self.remote_printing, self.remote_printing_ask)
247 filelog("file transfer remote caps: open-files=%-5s (ask=%s)",
248 self.remote_open_files, self.remote_open_files_ask)
249 filelog("file transfer remote caps: open-url=%-5s (ask=%s)",
250 self.remote_open_url, self.remote_open_url_ask)
252 def get_info(self) -> dict:
253 info = FileTransferAttributes.get_info(self)
254 info["remote"] = {
255 "file-transfer" : self.remote_file_transfer,
256 "file-transfer-ask" : self.remote_file_transfer_ask,
257 "file-size-limit" : self.remote_file_size_limit,
258 "file-chunks" : self.remote_file_chunks,
259 "open-files" : self.remote_open_files,
260 "open-files-ask" : self.remote_open_files_ask,
261 "open-url" : self.remote_open_url,
262 "open-url-ask" : self.remote_open_url_ask,
263 "printing" : self.remote_printing,
264 "printing-ask" : self.remote_printing_ask,
265 "file-ask-timeout" : self.remote_file_ask_timeout,
266 }
267 return info
270 def digest_mismatch(self, filename, digest, expected_digest, algo="sha1"):
271 filelog.error("Error: data does not match, invalid %s file digest for '%s'", algo, filename)
272 filelog.error(" received %s, expected %s", digest, expected_digest)
273 raise Exception("failed %s digest verification" % algo)
276 def _check_chunk_receiving(self, chunk_id, chunk_no):
277 chunk_state = self.receive_chunks_in_progress.get(chunk_id)
278 filelog("_check_chunk_receiving(%s, %s) chunk_state=%s", chunk_id, chunk_no, chunk_state)
279 if chunk_state:
280 if chunk_state[-4]:
281 #transfer has been cancelled
282 return
283 chunk_state[-2] = 0 #this timer has been used
284 if chunk_state[-1]==0:
285 filelog.error("Error: chunked file transfer '%s' timed out", chunk_id)
286 self.receive_chunks_in_progress.pop(chunk_id, None)
288 def cancel_download(self, send_id, message="Cancelled"):
289 filelog("cancel_download(%s, %s)", send_id, message)
290 for chunk_id, chunk_state in dict(self.receive_chunks_in_progress).items():
291 if chunk_state[-3]==send_id:
292 self.cancel_file(chunk_id, message)
293 return
294 filelog.error("Error: cannot cancel download %s, entry not found!", s(send_id))
296 def cancel_file(self, chunk_id, message, chunk=0):
297 filelog("cancel_file%s", (chunk_id, message, chunk))
298 chunk_state = self.receive_chunks_in_progress.get(chunk_id)
299 if chunk_state:
300 #mark it as cancelled:
301 chunk_state[-4] = True
302 timer = chunk_state[-2]
303 if timer:
304 chunk_state[-2] = 0
305 self.source_remove(timer)
306 fd = chunk_state[1]
307 osclose(fd)
308 #remove this transfer after a little while,
309 #so in-flight packets won't cause errors
310 def clean_receive_state():
311 self.receive_chunks_in_progress.pop(chunk_id, None)
312 return False
313 self.timeout_add(20000, clean_receive_state)
314 filename = chunk_state[2]
315 try:
316 os.unlink(filename)
317 except OSError as e:
318 filelog("os.unlink(%s)", filename, exc_info=True)
319 filelog.error("Error: failed to delete temporary download file")
320 filelog.error(" '%s' : %s", filename, e)
321 self.send("ack-file-chunk", chunk_id, False, message, chunk)
323 def _process_send_file_chunk(self, packet):
324 chunk_id, chunk, file_data, has_more = packet[1:5]
325 chunk_id = bytestostr(chunk_id)
326 filelog("_process_send_file_chunk%s", (chunk_id, chunk, "%i bytes" % len(file_data), has_more))
327 chunk_state = self.receive_chunks_in_progress.get(chunk_id)
328 if not chunk_state:
329 filelog.error("Error: cannot find the file transfer id '%r'", chunk_id)
330 self.cancel_file(chunk_id, "file transfer id %r not found" % chunk_id, chunk)
331 return
332 if chunk_state[-4]:
333 filelog("got chunk for a cancelled file transfer, ignoring it")
334 return
335 def progress(position, error=None):
336 start = chunk_state[0]
337 send_id = chunk_state[-3]
338 filesize = chunk_state[6]
339 self.transfer_progress_update(False, send_id, monotonic_time()-start, position, filesize, error)
340 fd = chunk_state[1]
341 if chunk_state[-1]+1!=chunk:
342 filelog.error("Error: chunk number mismatch, expected %i but got %i", chunk_state[-1]+1, chunk)
343 self.cancel_file(chunk_id, "chunk number mismatch", chunk)
344 osclose(fd)
345 progress(-1, "chunk no mismatch")
346 return
347 #update chunk number:
348 chunk_state[-1] = chunk
349 digest = chunk_state[8]
350 written = chunk_state[9]
351 try:
352 os.write(fd, file_data)
353 digest.update(file_data)
354 written += len(file_data)
355 chunk_state[9] = written
356 except OSError as e:
357 filelog.error("Error: cannot write file chunk")
358 filelog.error(" %s", e)
359 self.cancel_file(chunk_id, "write error: %s" % e, chunk)
360 osclose(fd)
361 progress(-1, "write error (%s)" % e)
362 return
363 self.send("ack-file-chunk", chunk_id, True, "", chunk)
364 if has_more:
365 progress(written)
366 timer = chunk_state[-2]
367 if timer:
368 self.source_remove(timer)
369 #remote end will send more after receiving the ack
370 timer = self.timeout_add(CHUNK_TIMEOUT, self._check_chunk_receiving, chunk_id, chunk)
371 chunk_state[-2] = timer
372 return
373 self.receive_chunks_in_progress.pop(chunk_id, None)
374 osclose(fd)
375 #check file size and digest then process it:
376 filename, mimetype, printit, openit, filesize, options = chunk_state[2:8]
377 if written!=filesize:
378 filelog.error("Error: expected a file of %i bytes, got %i", filesize, written)
379 progress(-1, "file size mismatch")
380 return
381 expected_digest = options.strget("sha1")
382 if expected_digest and digest.hexdigest()!=expected_digest:
383 progress(-1, "checksum mismatch")
384 self.digest_mismatch(filename, digest, expected_digest, "sha1")
385 return
387 progress(written)
388 start_time = chunk_state[0]
389 elapsed = monotonic_time()-start_time
390 mimetype = bytestostr(mimetype)
391 filelog("%i bytes received in %i chunks, took %ims", filesize, chunk, elapsed*1000)
392 self.process_downloaded_file(filename, mimetype, printit, openit, filesize, options)
394 def accept_data(self, send_id, dtype, basefilename, printit, openit):
395 #subclasses should check the flags,
396 #and if ask is True, verify they have accepted this specific send_id
397 filelog("accept_data%s", (send_id, dtype, basefilename, printit, openit))
398 filelog("accept_data: printing=%s, printing-ask=%s",
399 self.printing, self.printing_ask)
400 filelog("accept_data: file-transfer=%s, file-transfer-ask=%s",
401 self.file_transfer, self.file_transfer_ask)
402 filelog("accept_data: open-files=%s, open-files-ask=%s",
403 self.open_files, self.open_files_ask)
404 req = self.files_accepted.pop(send_id, None)
405 filelog("accept_data: files_accepted[%s]=%s", send_id, req)
406 if req is not None:
407 return (False, req)
408 if printit:
409 if not self.printing or self.printing_ask:
410 printit = False
411 elif not self.file_transfer or self.file_transfer_ask:
412 return None
413 if openit and (not self.open_files or self.open_files_ask):
414 #we can't ask in this implementation,
415 #so deny the request to open it:
416 openit = False
417 return (printit, openit)
419 def _process_send_file(self, packet):
420 #the remote end is sending us a file
421 start = monotonic_time()
422 basefilename, mimetype, printit, openit, filesize, file_data, options = packet[1:8]
423 send_id = ""
424 if len(packet)>=9:
425 send_id = s(packet[8])
426 #basefilename should be utf8:
427 basefilename = utf8_decode(basefilename)
428 mimetype = bytestostr(mimetype)
429 if filesize<=0:
430 filelog.error("Error: invalid file size: %s", filesize)
431 filelog.error(" file transfer aborted for %r", basefilename)
432 return
433 args = (send_id, "file", basefilename, printit, openit)
434 r = self.accept_data(*args)
435 filelog("%s%s=%s", self.accept_data, args, r)
436 if r is None:
437 filelog.warn("Warning: %s rejected for file '%s'",
438 ("transfer", "printing")[bool(printit)],
439 bytestostr(basefilename))
440 return
441 #accept_data can override the flags:
442 printit, openit = r
443 options = typedict(options)
444 if printit:
445 l = printlog
446 assert self.printing
447 else:
448 l = filelog
449 assert self.file_transfer
450 l("receiving file: %s",
451 [basefilename, mimetype, printit, openit, filesize, "%s bytes" % len(file_data), options])
452 if filesize>self.file_size_limit:
453 l.error("Error: file '%s' is too large:", s(basefilename))
454 l.error(" %sB, the file size limit is %sB",
455 std_unit(filesize), std_unit(self.file_size_limit))
456 return
457 chunk_id = options.strget("file-chunk-id")
458 try:
459 filename, fd = safe_open_download_file(basefilename, mimetype)
460 except OSError as e:
461 filelog("cannot save file %s / %s", basefilename, mimetype, exc_info=True)
462 filelog.error("Error: failed to save downloaded file")
463 filelog.error(" %s", e)
464 if chunk_id:
465 self.send("ack-file-chunk", chunk_id, False, "failed to create file: %s" % e, 0)
466 return
467 self.file_descriptors.add(fd)
468 if chunk_id:
469 l = len(self.receive_chunks_in_progress)
470 if l>=MAX_CONCURRENT_FILES:
471 self.send("ack-file-chunk", chunk_id, False, "too many file transfers in progress: %i" % l, 0)
472 os.close(fd)
473 return
474 digest = hashlib.sha1()
475 chunk = 0
476 timer = self.timeout_add(CHUNK_TIMEOUT, self._check_chunk_receiving, chunk_id, chunk)
477 chunk_state = [
478 monotonic_time(),
479 fd, filename, mimetype,
480 printit, openit, filesize,
481 options, digest, 0, False, send_id,
482 timer, chunk,
483 ]
484 self.receive_chunks_in_progress[chunk_id] = chunk_state
485 self.send("ack-file-chunk", chunk_id, True, "", chunk)
486 return
487 #not chunked, full file:
488 assert file_data, "no data, got %s" % (file_data,)
489 if len(file_data)!=filesize:
490 l.error("Error: invalid data size for file '%s'", s(basefilename))
491 l.error(" received %i bytes, expected %i bytes", len(file_data), filesize)
492 return
493 #check digest if present:
494 def check_digest(algo="sha1", libfn=hashlib.sha1):
495 digest = options.get(algo)
496 if digest:
497 u = libfn()
498 u.update(file_data)
499 l("%s digest: %s - expected: %s", algo, u.hexdigest(), digest)
500 if digest!=u.hexdigest():
501 self.digest_mismatch(filename, digest, u.hexdigest(), algo)
502 check_digest("sha1", hashlib.sha1)
503 check_digest("md5", hashlib.md5)
504 try:
505 os.write(fd, file_data)
506 finally:
507 os.close(fd)
508 self.transfer_progress_update(False, send_id, monotonic_time()-start, filesize, filesize, None)
509 self.process_downloaded_file(filename, mimetype, printit, openit, filesize, options)
512 def process_downloaded_file(self, filename, mimetype, printit, openit, filesize, options):
513 filelog.info("downloaded %s bytes to %s file%s:",
514 filesize, (mimetype or "temporary"), ["", " for printing"][int(printit)])
515 filelog.info(" '%s'", filename)
516 #some file requests may have a custom callback
517 #(ie: bug report tool will just include the file)
518 rf = options.tupleget("request-file")
519 if rf and len(rf)>=2:
520 argf = rf[0]
521 cb = self.file_request_callback.pop(bytestostr(argf), None)
522 if cb:
523 cb(filename, filesize)
524 return
525 if printit or openit:
526 t = start_thread(self.do_process_downloaded_file, "process-download", daemon=False,
527 args=(filename, mimetype, printit, openit, filesize, options))
528 filelog("started process-download thread: %s", t)
530 def do_process_downloaded_file(self, filename, mimetype, printit, openit, filesize, options):
531 filelog("do_process_downloaded_file%s", (filename, mimetype, printit, openit, filesize, options))
532 if printit:
533 self._print_file(filename, mimetype, options)
534 return
535 if openit:
536 if not self.open_files:
537 filelog.warn("Warning: opening files automatically is disabled,")
538 filelog.warn(" ignoring uploaded file:")
539 filelog.warn(" '%s'", filename)
540 return
541 self._open_file(filename)
543 def _print_file(self, filename, mimetype, options):
544 printlog("print_file%s", (filename, mimetype, options))
545 printer = options.strget("printer")
546 title = options.strget("title")
547 copies = options.intget("copies", 1)
548 if title:
549 printlog.info(" sending '%s' to printer '%s'", title, printer)
550 else:
551 printlog.info(" sending to printer '%s'", printer)
552 from xpra.platform.printing import print_files, printing_finished, get_printers
553 printers = get_printers()
554 def delfile():
555 if DELETE_PRINTER_FILE:
556 try:
557 os.unlink(filename)
558 except OSError:
559 printlog("failed to delete print job file '%s'", filename)
560 if not printer:
561 printlog.error("Error: the printer name is missing")
562 printlog.error(" printers available: %s", csv(printers.keys()) or "none")
563 delfile()
564 return
565 if printer not in printers:
566 printlog.error("Error: printer '%s' does not exist!", printer)
567 printlog.error(" printers available: %s", csv(printers.keys()) or "none")
568 delfile()
569 return
570 try:
571 job_options = options.dictget("options", {})
572 job_options["copies"] = copies
573 job = print_files(printer, [filename], title, job_options)
574 except Exception as e:
575 printlog("print_files%s", (printer, [filename], title, options), exc_info=True)
576 printlog.error("Error: cannot print file '%s'", os.path.basename(filename))
577 printlog.error(" %s", e)
578 delfile()
579 return
580 printlog("printing %s, job=%s", filename, job)
581 if job<=0:
582 printlog("printing failed and returned %i", job)
583 delfile()
584 return
585 start = monotonic_time()
586 def check_printing_finished():
587 done = printing_finished(job)
588 printlog("printing_finished(%s)=%s", job, done)
589 if done:
590 delfile()
591 return False
592 if monotonic_time()-start>=PRINT_JOB_TIMEOUT:
593 printlog.warn("Warning: print job %s timed out", job)
594 delfile()
595 return False
596 return True #try again..
597 if check_printing_finished():
598 #check every 10 seconds:
599 self.timeout_add(10000, check_printing_finished)
602 def get_open_env(self):
603 env = os.environ.copy()
604 #prevent loops:
605 env["XPRA_XDG_OPEN"] = "1"
606 return env
608 def _open_file(self, url):
609 filelog("_open_file(%s)", url)
610 self.exec_open_command(url)
612 def _open_url(self, url):
613 filelog("_open_url(%s)", url)
614 if POSIX:
615 #we can't use webbrowser,
616 #because this will use "xdg-open" from the $PATH
617 #which may point back to us!
618 self.exec_open_command(url)
619 else:
620 import webbrowser
621 webbrowser.open_new_tab(url)
623 def exec_open_command(self, url):
624 filelog("exec_open_command(%s)", url)
625 try:
626 import shlex
627 command = shlex.split(self.open_command)+[url]
628 except ImportError as e:
629 filelog("exec_open_command(%s) no shlex: %s", url, e)
630 command = self.open_command.split(" ")
631 filelog("exec_open_command(%s) command=%s", url, command)
632 try:
633 proc = subprocess.Popen(command, env=self.get_open_env(), shell=WIN32)
634 except Exception as e:
635 filelog("exec_open_command(%s)", url, exc_info=True)
636 filelog.error("Error: cannot open '%s': %s", url, e)
637 return
638 filelog("exec_open_command(%s) Popen(%s)=%s", url, command, proc)
639 def open_done(*_args):
640 returncode = proc.poll()
641 filelog("open_file: command %s has ended, returncode=%s", command, returncode)
642 if returncode!=0:
643 filelog.warn("Warning: failed to open the downloaded content")
644 filelog.warn(" '%s' returned %s", " ".join(command), returncode)
645 cr = getChildReaper()
646 cr.add_process(proc, "Open file %s" % url, command, True, True, open_done)
648 def file_size_warning(self, action, location, basefilename, filesize, limit):
649 filelog.warn("Warning: cannot %s the file '%s'", action, basefilename)
650 filelog.warn(" this file is too large: %sB", std_unit(filesize))
651 filelog.warn(" the %s file size limit is %sB", location, std_unit(limit))
653 def check_file_size(self, action, filename, filesize):
654 basefilename = os.path.basename(filename)
655 if filesize>self.file_size_limit:
656 self.file_size_warning(action, "local", basefilename, filesize, self.file_size_limit)
657 return False
658 if filesize>self.remote_file_size_limit:
659 self.file_size_warning(action, "remote", basefilename, filesize, self.remote_file_size_limit)
660 return False
661 return True
664 def send_request_file(self, filename, openit=True):
665 self.send("request-file", filename, openit)
666 self.files_requested[filename] = openit
669 def _process_open_url(self, packet):
670 send_id = s(packet[2])
671 url = utf8_decode(packet[1])
672 if not self.open_url:
673 filelog.warn("Warning: received a request to open URL '%s'", url)
674 filelog.warn(" but opening of URLs is disabled")
675 return
676 if not self.open_url_ask or self.accept_data(send_id, "url", url, False, True):
677 self._open_url(url)
678 else:
679 filelog("url '%s' not accepted", url)
682 def send_open_url(self, url):
683 if not self.remote_open_url:
684 filelog.warn("Warning: remote end does not accept URLs")
685 return False
686 if self.remote_open_url_ask:
687 #ask the client if it is OK to send
688 return self.send_data_request("open", b"url", url)
689 self.do_send_open_url(url)
690 return True
692 def do_send_open_url(self, url, send_id=""):
693 self.send("open-url", url, send_id)
695 def send_file(self, filename, mimetype, data, filesize=0, printit=False, openit=False, options=None):
696 if printit:
697 l = printlog
698 if not self.printing:
699 l.warn("Warning: printing is not enabled for %s", self)
700 return False
701 if not self.remote_printing:
702 l.warn("Warning: remote end does not support printing")
703 return False
704 ask = self.remote_printing_ask
705 action = "print"
706 else:
707 if not self.file_transfer:
708 filelog.warn("Warning: file transfers are not enabled for %s", self)
709 return False
710 if not self.remote_file_transfer:
711 printlog.warn("Warning: remote end does not support file transfers")
712 return False
713 l = filelog
714 ask = self.remote_file_transfer_ask
715 action = "upload"
716 if openit:
717 if not self.remote_open_files:
718 l.info("opening the file after transfer is disabled on the remote end")
719 l.info(" sending only, the file will need to be opended manually")
720 openit = False
721 action = "upload"
722 else:
723 ask |= self.remote_open_files_ask
724 action = "open"
725 assert len(data)>=filesize, "data is smaller then the given file size!"
726 data = data[:filesize] #gio may null terminate it
727 l("send_file%s action=%s, ask=%s",
728 (filename, mimetype, type(data), "%i bytes" % filesize, printit, openit, options), action, ask)
729 try:
730 filename = bytestostr(filename).encode("utf8")
731 except Exception:
732 filename = strtobytes(filename)
733 self.dump_remote_caps()
734 if not self.check_file_size(action, filename, filesize):
735 return False
736 if ask:
737 return self.send_data_request(action, b"file", filename, mimetype, data, filesize, printit, openit, options)
738 self.do_send_file(filename, mimetype, data, filesize, printit, openit, options)
739 return True
741 def send_data_request(self, action, dtype, url, mimetype="", data="", filesize=0, printit=False, openit=True, options=None):
742 send_id = uuid.uuid4().hex
743 if len(self.pending_send_data)>=MAX_CONCURRENT_FILES:
744 filelog.warn("Warning: %s dropped", action)
745 filelog.warn(" %i transfer%s already waiting for a response",
746 len(self.pending_send_data), engs(self.pending_send_data))
747 return None
748 self.pending_send_data[send_id] = (dtype, url, mimetype, data, filesize, printit, openit, options or {})
749 delay = self.remote_file_ask_timeout*1000
750 self.pending_send_data_timers[send_id] = self.timeout_add(delay, self.send_data_ask_timeout, send_id)
751 filelog("sending data request for %s '%s' with send-id=%s",
752 s(dtype), s(url), send_id)
753 self.send("send-data-request", dtype, send_id, url, mimetype, filesize, printit, openit, options or {})
754 return send_id
757 def _process_send_data_request(self, packet):
758 dtype, send_id, url, _, filesize, printit, openit = packet[1:8]
759 options = {}
760 if len(packet)>=9:
761 options = packet[8]
762 #filenames and url are always sent encoded as utf8:
763 url = utf8_decode(url)
764 dtype = s(dtype)
765 send_id = s(send_id)
766 self.do_process_send_data_request(dtype, send_id, url, _, filesize, printit, openit, typedict(options))
769 def do_process_send_data_request(self, dtype, send_id, url, _, filesize, printit, openit, options):
770 filelog("do_process_send_data_request: send_id=%s, url=%s, printit=%s, openit=%s, options=%s",
771 s(send_id), url, printit, openit, options)
772 def cb_answer(accept):
773 filelog("accept%s=%s", (url, printit, openit), accept)
774 self.send("send-data-response", send_id, accept)
775 #could be a request we made:
776 #(in which case we can just accept it without prompt)
777 rf = options.tupleget("request-file")
778 if rf and len(rf)>=2:
779 argf, openit = rf[:2]
780 openit = self.files_requested.pop(bytestostr(argf), None)
781 if openit is not None:
782 self.files_accepted[send_id] = openit
783 cb_answer(True)
784 return
785 if dtype=="file":
786 if not self.file_transfer:
787 cb_answer(False)
788 return
789 url = os.path.basename(url)
790 if printit:
791 ask = self.printing_ask
792 elif openit:
793 ask = self.file_transfer_ask or self.open_files_ask
794 else:
795 ask = self.file_transfer_ask
796 elif dtype=="url":
797 if not self.open_url:
798 cb_answer(False)
799 return
800 ask = self.open_url_ask
801 else:
802 filelog.warn("Warning: unknown data request type '%s'", dtype)
803 cb_answer(False)
804 return
805 if not ask:
806 filelog.warn("Warning: received a send-data request for a %s,", dtype)
807 filelog.warn(" but authorization is not required by the client")
808 #fail it because if we responded with True,
809 #it would fail later when we don't find this send_id in our accepted list
810 cb_answer(False)
811 else:
812 self.ask_data_request(cb_answer, send_id, dtype, url, filesize, printit, openit)
814 def ask_data_request(self, cb_answer, send_id, dtype, url, filesize, printit, openit):
815 #subclasses may prompt the user here instead
816 filelog("ask_data_request%s", (send_id, dtype, url, filesize, printit, openit))
817 v = self.accept_data(send_id, dtype, url, printit, openit)
818 cb_answer(v)
820 def _process_send_data_response(self, packet):
821 send_id, accept = packet[1:3]
822 filelog("process send-data-response: send_id=%s, accept=%s", s(send_id), accept)
823 send_id = s(send_id)
824 timer = self.pending_send_data_timers.pop(send_id, None)
825 if timer:
826 self.source_remove(timer)
827 v = self.pending_send_data.pop(send_id, None)
828 if v is None:
829 filelog.warn("Warning: cannot find send-file entry")
830 return
831 dtype = v[0]
832 url = v[1]
833 if accept==DENY:
834 filelog.info("the request to send %s '%s' has been denied", bytestostr(dtype), s(url))
835 return
836 assert accept in (ACCEPT, OPEN), "unknown value for send-data response: %s" % (accept,)
837 if dtype==b"file":
838 mimetype, data, filesize, printit, openit, options = v[2:]
839 if accept==ACCEPT:
840 self.do_send_file(url, mimetype, data, filesize, printit, openit, options, send_id)
841 else:
842 assert openit and accept==OPEN
843 #try to open at this end:
844 self._open_file(url)
845 elif dtype==b"url":
846 if accept==ACCEPT:
847 self.do_send_open_url(url, send_id)
848 else:
849 assert accept==OPEN
850 #open it at this end:
851 self._open_url(url)
852 else:
853 filelog.error("Error: unknown datatype '%s'", dtype)
855 def send_data_ask_timeout(self, send_id):
856 v = self.pending_send_data.pop(send_id, None)
857 self.pending_send_data_timers.pop(send_id, None)
858 if not v:
859 filelog.warn("Warning: send timeout, id '%s' not found!", send_id)
860 return False
861 filename = v[1]
862 printit = v[5]
863 filelog.warn("Warning: failed to %s file '%s',", ["send", "print"][printit], filename)
864 filelog.warn(" the send approval request timed out")
865 return False
867 def do_send_file(self, filename, mimetype, data, filesize=0, printit=False, openit=False, options=None, send_id=""):
868 if printit:
869 action = "print"
870 l = printlog
871 else:
872 action = "upload"
873 l = filelog
874 l("do_send_file%s", (s(filename), mimetype, type(data), "%i bytes" % filesize, printit, openit, options))
875 if not self.check_file_size(action, filename, filesize):
876 return False
877 u = hashlib.sha1()
878 u.update(data)
879 absfile = os.path.abspath(filename)
880 filelog("sha1 digest('%s')=%s", s(absfile), u.hexdigest())
881 options = options or {}
882 options["sha1"] = u.hexdigest()
883 chunk_size = min(self.file_chunks, self.remote_file_chunks)
884 if 0<chunk_size<filesize:
885 if len(self.send_chunks_in_progress)>=MAX_CONCURRENT_FILES:
886 raise Exception("too many file transfers in progress: %i" % len(self.send_chunks_in_progress))
887 #chunking is supported and the file is big enough
888 chunk_id = uuid.uuid4().hex
889 options["file-chunk-id"] = chunk_id
890 #timer to check that the other end is requesting more chunks:
891 timer = self.timeout_add(CHUNK_TIMEOUT, self._check_chunk_sending, chunk_id, 0)
892 chunk_state = [monotonic_time(), data, chunk_size, timer, 0]
893 self.send_chunks_in_progress[chunk_id] = chunk_state
894 cdata = ""
895 filelog("using chunks, sending initial file-chunk-id=%s, for chunk size=%s",
896 chunk_id, chunk_size)
897 else:
898 #send everything now:
899 cdata = self.compressed_wrapper("file-data", data)
900 assert len(cdata)<=filesize #compressed wrapper ensures this is true
901 filelog("sending full file: %i bytes (chunk size=%i)", filesize, chunk_size)
902 basefilename = os.path.basename(filename)
903 #convert str to utf8 bytes:
904 try:
905 base = basefilename.encode("utf8")
906 except (AttributeError, UnicodeEncodeError):
907 base = strtobytes(basefilename)
908 self.send("send-file", base, mimetype, printit, openit, filesize, cdata, options, send_id)
909 return True
911 def _check_chunk_sending(self, chunk_id, chunk_no):
912 chunk_state = self.send_chunks_in_progress.get(chunk_id)
913 filelog("_check_chunk_sending(%s, %s) chunk_state found: %s", chunk_id, chunk_no, bool(chunk_state))
914 if chunk_state:
915 chunk_state[3] = 0 #timer has fired
916 if chunk_state[-1]==chunk_no:
917 filelog.error("Error: chunked file transfer '%s' timed out", chunk_id)
918 filelog.error(" on chunk %i", chunk_no)
919 self.cancel_sending(chunk_id)
921 def cancel_sending(self, chunk_id):
922 chunk_state = self.send_chunks_in_progress.pop(chunk_id, None)
923 filelog("cancel_sending(%s) chunk state found: %s", chunk_id, bool(chunk_state))
924 if chunk_state:
925 timer = chunk_state[3]
926 if timer:
927 chunk_state[3] = 0
928 self.source_remove(timer)
930 def _process_ack_file_chunk(self, packet):
931 #the other end received our send-file or send-file-chunk,
932 #send some more file data
933 filelog("ack-file-chunk: %s", packet[1:])
934 chunk_id, state, error_message, chunk = packet[1:5]
935 chunk_id = bytestostr(chunk_id)
936 if not state:
937 filelog.info("the remote end is cancelling the file transfer:")
938 filelog.info(" %s", bytestostr(error_message))
939 self.cancel_sending(chunk_id)
940 return
941 chunk_state = self.send_chunks_in_progress.get(chunk_id)
942 if not chunk_state:
943 filelog.error("Error: cannot find the file transfer id '%r'", chunk_id)
944 return
945 if chunk_state[-1]!=chunk:
946 filelog.error("Error: chunk number mismatch (%i vs %i)", chunk_state, chunk)
947 self.cancel_sending(chunk_id)
948 return
949 start_time, data, chunk_size, timer, chunk = chunk_state
950 if not data:
951 #all sent!
952 elapsed = monotonic_time()-start_time
953 filelog("%i chunks of %i bytes sent in %ims (%sB/s)",
954 chunk, chunk_size, elapsed*1000, std_unit(chunk*chunk_size/elapsed))
955 self.cancel_sending(chunk_id)
956 return
957 assert chunk_size>0
958 #carve out another chunk:
959 cdata = self.compressed_wrapper("file-data", data[:chunk_size])
960 data = data[chunk_size:]
961 chunk += 1
962 if timer:
963 self.source_remove(timer)
964 timer = self.timeout_add(CHUNK_TIMEOUT, self._check_chunk_sending, chunk_id, chunk)
965 self.send_chunks_in_progress[chunk_id] = [start_time, data, chunk_size, timer, chunk]
966 self.send("send-file-chunk", chunk_id, chunk, cdata, bool(data))
968 def send(self, *parts):
969 raise NotImplementedError()
971 def compressed_wrapper(self, datatype, data, level=5):
972 raise NotImplementedError()
974 def transfer_progress_update(self, send=True, transfer_id=0, elapsed=0, position=0, total=0, error=None):
975 #this method is overriden in the gtk client:
976 filelog("transfer_progress_update%s", (send, transfer_id, elapsed, position, total, error))