Hide keyboard shortcuts

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. 

6 

7import os 

8import subprocess 

9import hashlib 

10import uuid 

11 

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 

19 

20printlog = Logger("printing") 

21filelog = Logger("file") 

22 

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 

29 

30MIMETYPE_EXTS = { 

31 "application/postscript" : "ps", 

32 "application/pdf" : "pdf", 

33 "raw" : "raw", 

34 } 

35 

36DENY = 0 

37ACCEPT = 1 #the file / URL will be sent 

38OPEN = 2 #don't send, open on sender 

39 

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) 

47 

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 

65 

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 

93 

94def s(v): 

95 try: 

96 return v.decode("utf8") 

97 except (AttributeError, UnicodeDecodeError): 

98 return bytestostr(v) 

99 

100def utf8_decode(url): 

101 try: 

102 return strtobytes(url).decode("utf8") 

103 except UnicodeDecodeError: 

104 return bytestostr(url) 

105 

106 

107class FileTransferAttributes: 

108 

109 def __init__(self): 

110 self.init_attributes() 

111 

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) 

116 

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()) 

146 

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 } 

163 

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 } 

180 

181 

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 """ 

187 

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 

211 

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() 

227 

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() 

241 

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) 

251 

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 

268 

269 

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) 

274 

275 

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) 

287 

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)) 

295 

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) 

322 

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 

386 

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) 

393 

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) 

418 

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) 

510 

511 

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) 

529 

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) 

542 

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) 

600 

601 

602 def get_open_env(self): 

603 env = os.environ.copy() 

604 #prevent loops: 

605 env["XPRA_XDG_OPEN"] = "1" 

606 return env 

607 

608 def _open_file(self, url): 

609 filelog("_open_file(%s)", url) 

610 self.exec_open_command(url) 

611 

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) 

622 

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) 

647 

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)) 

652 

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 

662 

663 

664 def send_request_file(self, filename, openit=True): 

665 self.send("request-file", filename, openit) 

666 self.files_requested[filename] = openit 

667 

668 

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) 

680 

681 

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 

691 

692 def do_send_open_url(self, url, send_id=""): 

693 self.send("open-url", url, send_id) 

694 

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 

740 

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 

755 

756 

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)) 

767 

768 

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) 

813 

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) 

819 

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) 

854 

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 

866 

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 

910 

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) 

920 

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) 

929 

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)) 

967 

968 def send(self, *parts): 

969 raise NotImplementedError() 

970 

971 def compressed_wrapper(self, datatype, data, level=5): 

972 raise NotImplementedError() 

973 

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))