1#!/usr/local/bin/python3.8 2 3## Copyright (C) 2007, 2008, 2009, 2010, 2012, 2013, 2014 Red Hat, Inc. 4## Copyright (C) 2008 Novell, Inc. 5## Authors: Tim Waugh <twaugh@redhat.com>, Vincent Untz 6 7## This program is free software; you can redistribute it and/or modify 8## it under the terms of the GNU General Public License as published by 9## the Free Software Foundation; either version 2 of the License, or 10## (at your option) any later version. 11 12## This program is distributed in the hope that it will be useful, 13## but WITHOUT ANY WARRANTY; without even the implied warranty of 14## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15## GNU General Public License for more details. 16 17## You should have received a copy of the GNU General Public License 18## along with this program; if not, write to the Free Software 19## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 20 21import cups 22import dbus 23from functools import reduce 24try: 25 gi.require_version('Gdk', '3.0') 26 from gi.repository import Gdk 27 gi.require_version('Gtk', '3.0') 28 from gi.repository import Gtk 29except: 30 pass 31import os 32import sys 33import tempfile 34import xml.etree.ElementTree 35 36import asyncipp 37from debug import * 38import debug 39 40CUPS_PK_NAME = 'org.opensuse.CupsPkHelper.Mechanism' 41CUPS_PK_PATH = '/' 42CUPS_PK_IFACE = 'org.opensuse.CupsPkHelper.Mechanism' 43CUPS_PK_NEED_AUTH = 'org.opensuse.CupsPkHelper.Mechanism.NotPrivileged' 44 45###### 46###### A polkit-1 based asynchronous CupsPkHelper interface made to 47###### look just like a normal IPPAuthConnection class. For method 48###### calls that have no equivalent in the CupsPkHelper API, IPP 49###### authentication is used over a CUPS connection in a separate 50###### thread. 51###### 52 53_DevicesGet_uses_new_api = None 54 55### 56### A class to handle an asynchronous method call. 57### 58class _PK1AsyncMethodCall: 59 def __init__ (self, bus, conn, pk_method_name, pk_args, 60 reply_handler, error_handler, unpack_fn, 61 fallback_fn, args, kwds): 62 self._bus = bus 63 self._conn = conn 64 self._pk_method_name = pk_method_name 65 self._pk_args = pk_args 66 self._client_reply_handler = reply_handler 67 self._client_error_handler = error_handler 68 self._unpack_fn = unpack_fn 69 self._fallback_fn = fallback_fn 70 self._fallback_args = args 71 self._fallback_kwds = kwds 72 self._destroyed = False 73 debugprint ("+_PK1AsyncMethodCall: %s" % self) 74 75 def __del__ (self): 76 debug.debugprint ("-_PK1AsyncMethodCall: %s" % self) 77 78 def call (self): 79 object = self._bus.get_object(CUPS_PK_NAME, CUPS_PK_PATH) 80 proxy = dbus.Interface (object, CUPS_PK_IFACE) 81 pk_method = proxy.get_dbus_method (self._pk_method_name) 82 83 try: 84 debugprint ("%s: calling %s" % (self, pk_method)) 85 pk_method (*self._pk_args, 86 reply_handler=self._pk_reply_handler, 87 error_handler=self._pk_error_handler, 88 timeout=3600) 89 except TypeError as e: 90 debugprint ("Type error in PK call: %s" % repr (e)) 91 self.call_fallback_fn () 92 93 def _destroy (self): 94 debugprint ("DESTROY: %s" % self) 95 self._destroyed = True 96 del self._bus 97 del self._conn 98 del self._pk_method_name 99 del self._pk_args 100 del self._client_reply_handler 101 del self._client_error_handler 102 del self._unpack_fn 103 del self._fallback_fn 104 del self._fallback_args 105 del self._fallback_kwds 106 107 def _pk_reply_handler (self, error, *args): 108 if self._destroyed: 109 return 110 111 if str (error) == '': 112 try: 113 Gdk.threads_enter () 114 except: 115 pass 116 debugprint ("%s: no error, calling reply handler %s" % 117 (self, self._client_reply_handler)) 118 self._client_reply_handler (self._conn, self._unpack_fn (*args)) 119 try: 120 Gdk.threads_leave () 121 except: 122 pass 123 self._destroy () 124 return 125 126 debugprint ("PolicyKit method failed with: %s" % repr (error)) 127 self.call_fallback_fn () 128 129 def _pk_error_handler (self, exc): 130 if self._destroyed: 131 return 132 133 if exc.get_dbus_name () == CUPS_PK_NEED_AUTH: 134 exc = cups.IPPError (cups.IPP_NOT_AUTHORIZED, 'pkcancel') 135 try: 136 Gdk.threads_enter () 137 except: 138 pass 139 debugprint ("%s: no auth, calling error handler %s" % 140 (self, self._client_error_handler)) 141 self._client_error_handler (self._conn, exc) 142 try: 143 Gdk.threads_leave () 144 except: 145 pass 146 self._destroy () 147 return 148 149 debugprint ("PolicyKit call to %s did not work: %s" % 150 (self._pk_method_name, repr (exc))) 151 self.call_fallback_fn () 152 153 def call_fallback_fn (self): 154 # Make the 'connection' parameter consistent with PK callbacks. 155 self._fallback_kwds["reply_handler"] = self._ipp_reply_handler 156 self._fallback_kwds["error_handler"] = self._ipp_error_handler 157 debugprint ("%s: calling %s" % (self, self._fallback_fn)) 158 self._fallback_fn (*self._fallback_args, **self._fallback_kwds) 159 160 def _ipp_reply_handler (self, conn, *args): 161 if self._destroyed: 162 return 163 164 debugprint ("%s: chaining up to %s" % (self, 165 self._client_reply_handler)) 166 self._client_reply_handler (self._conn, *args) 167 self._destroy () 168 169 def _ipp_error_handler (self, conn, *args): 170 if self._destroyed: 171 return 172 173 debugprint ("%s: chaining up to %s" % (self, 174 self._client_error_handler)) 175 self._client_error_handler (self._conn, *args) 176 self._destroy () 177 178### 179### A class for handling FileGet when a temporary file is needed. 180### 181class _WriteToTmpFile: 182 def __init__ (self, kwds, reply_handler, error_handler): 183 self._reply_handler = reply_handler 184 self._error_handler = error_handler 185 186 # Create the temporary file in /tmp to ensure that 187 # cups-pk-helper-mechanism is able to write to it. 188 (tmpfd, tmpfname) = tempfile.mkstemp (dir="/tmp") 189 os.close (tmpfd) 190 self._filename = tmpfname 191 debugprint ("Created tempfile %s" % tmpfname) 192 self._kwds = kwds 193 194 def __del__ (self): 195 try: 196 os.unlink (self._filename) 197 debug.debugprint ("Removed tempfile %s" % self._filename) 198 except: 199 debug.debugprint ("No tempfile to remove") 200 201 def get_filename (self): 202 return self._filename 203 204 def reply_handler (self, conn, none): 205 tmpfd = os.open (self._filename, os.O_RDONLY) 206 tmpfile = os.fdopen (tmpfd, 'rt') 207 if "fd" in self._kwds: 208 fd = self._kwds["fd"] 209 os.lseek (fd, 0, os.SEEK_SET) 210 line = tmpfile.readline () 211 while line != '': 212 os.write (fd, line.encode('UTF-8')) 213 line = tmpfile.readline () 214 else: 215 file_object = self._kwds["file"] 216 file_object.seek (0) 217 line = tmpfile.readline () 218 while line != '': 219 file_object.write (line.encode('UTF-8')) 220 line = tmpfile.readline () 221 222 tmpfile.close () 223 self._reply_handler (conn, none) 224 225 def error_handler (self, conn, exc): 226 self._error_handler (conn, exc) 227 228### 229### The user-visible class. 230### 231class PK1Connection: 232 def __init__(self, reply_handler=None, error_handler=None, 233 host=None, port=None, encryption=None, parent=None): 234 self._conn = asyncipp.IPPAuthConnection (reply_handler=reply_handler, 235 error_handler=error_handler, 236 host=host, port=port, 237 encryption=encryption, 238 parent=parent) 239 240 try: 241 self._system_bus = dbus.SystemBus() 242 except (dbus.exceptions.DBusException, AttributeError): 243 # No system D-Bus. 244 self._system_bus = None 245 246 global _DevicesGet_uses_new_api 247 if _DevicesGet_uses_new_api is None and self._system_bus: 248 try: 249 obj = self._system_bus.get_object(CUPS_PK_NAME, CUPS_PK_PATH) 250 proxy = dbus.Interface (obj, dbus.INTROSPECTABLE_IFACE) 251 api = proxy.Introspect () 252 top = xml.etree.ElementTree.XML (api) 253 for interface in top.findall ("interface"): 254 if interface.attrib.get ("name") != CUPS_PK_IFACE: 255 continue 256 257 for method in interface.findall ("method"): 258 if method.attrib.get ("name") != "DevicesGet": 259 continue 260 261 num_args = 0 262 for arg in method.findall ("arg"): 263 direction = arg.attrib.get ("direction") 264 if direction != "in": 265 continue 266 267 num_args += 1 268 269 _DevicesGet_uses_new_api = num_args == 4 270 debugprint ("DevicesGet new API: %s" % (num_args == 4)) 271 break 272 273 break 274 275 except Exception as e: 276 debugprint ("Exception assessing DevicesGet API: %s" % repr (e)) 277 278 methodtype = type (self._conn.getPrinters) 279 bindings = [] 280 for fname in dir (self._conn): 281 if fname.startswith ('_'): 282 continue 283 fn = getattr (self._conn, fname) 284 if type (fn) != methodtype: 285 continue 286 if not hasattr (self, fname): 287 setattr (self, fname, self._make_binding (fn)) 288 bindings.append (fname) 289 290 self._bindings = bindings 291 debugprint ("+%s" % self) 292 293 def __del__ (self): 294 debug.debugprint ("-%s" % self) 295 296 def _make_binding (self, fn): 297 def binding (*args, **kwds): 298 op = _PK1AsyncMethodCall (None, self, None, None, 299 kwds.get ("reply_handler"), 300 kwds.get ("error_handler"), 301 None, fn, args, kwds) 302 op.call_fallback_fn () 303 304 return binding 305 306 def destroy (self): 307 debugprint ("DESTROY: %s" % self) 308 self._conn.destroy () 309 310 for binding in self._bindings: 311 delattr (self, binding) 312 313 def _coerce (self, typ, val): 314 return typ (val) 315 316 def _args_kwds_to_tuple (self, types, params, args, kwds): 317 """Collapse args and kwds into a single tuple.""" 318 leftover_kwds = kwds.copy () 319 reply_handler = leftover_kwds.get ("reply_handler") 320 error_handler = leftover_kwds.get ("error_handler") 321 if "reply_handler" in leftover_kwds: 322 del leftover_kwds["reply_handler"] 323 if "error_handler" in leftover_kwds: 324 del leftover_kwds["error_handler"] 325 if "auth_handler" in leftover_kwds: 326 del leftover_kwds["auth_handler"] 327 328 result = [True, reply_handler, error_handler, ()] 329 if self._system_bus is None: 330 return result 331 332 tup = [] 333 argindex = 0 334 for arg in args: 335 try: 336 val = self._coerce (types[argindex], arg) 337 except IndexError: 338 # More args than types. 339 kw, default = params[argindex] 340 if default != arg: 341 return result 342 343 # It's OK, this is the default value anyway and can be 344 # ignored. Skip to the next one. 345 argindex += 1 346 continue 347 except TypeError as e: 348 debugprint ("Error converting %s to %s" % 349 (repr (arg), types[argindex])) 350 return result 351 352 tup.append (val) 353 argindex += 1 354 355 for kw, default in params[argindex:]: 356 if kw in leftover_kwds: 357 try: 358 val = self._coerce (types[argindex], leftover_kwds[kw]) 359 except TypeError as e: 360 debugprint ("Error converting %s to %s" % 361 (repr (leftover_kwds[kw]), types[argindex])) 362 return result 363 364 tup.append (val) 365 del leftover_kwds[kw] 366 else: 367 tup.append (default) 368 369 argindex += 1 370 371 if leftover_kwds: 372 debugprint ("Leftover keywords: %s" % repr (list(leftover_kwds.keys ()))) 373 return result 374 375 result[0] = False 376 result[3] = tuple (tup) 377 debugprint ("Converted %s/%s to %s" % (args, kwds, tuple (tup))) 378 return result 379 380 def _call_with_pk (self, use_pycups, pk_method_name, pk_args, 381 reply_handler, error_handler, unpack_fn, 382 fallback_fn, args, kwds): 383 asyncmethodcall = _PK1AsyncMethodCall (self._system_bus, self, 384 pk_method_name, pk_args, 385 reply_handler, 386 error_handler, 387 unpack_fn, fallback_fn, 388 args, kwds) 389 390 if not use_pycups: 391 try: 392 debugprint ("Calling PK method %s" % pk_method_name) 393 asyncmethodcall.call () 394 except dbus.DBusException as e: 395 debugprint ("D-Bus call failed: %s" % repr (e)) 396 use_pycups = True 397 398 if use_pycups: 399 return asyncmethodcall.call_fallback_fn () 400 401 def _nothing_to_unpack (self): 402 return None 403 404 def getDevices (self, *args, **kwds): 405 global _DevicesGet_uses_new_api 406 if _DevicesGet_uses_new_api: 407 (use_pycups, reply_handler, error_handler, 408 tup) = self._args_kwds_to_tuple ([int, int, list, list], 409 [("timeout", 0), 410 ("limit", 0), 411 ("include_schemes", []), 412 ("exclude_schemes", [])], 413 args, kwds) 414 else: 415 (use_pycups, reply_handler, error_handler, 416 tup) = self._args_kwds_to_tuple ([int, list, list], 417 [("limit", 0), 418 ("include_schemes", []), 419 ("exclude_schemes", [])], 420 args, kwds) 421 422 if not use_pycups: 423 # Special handling for include_schemes/exclude_schemes. 424 # Convert from list to ","-separated string. 425 newtup = list (tup) 426 for paramindex in [1, 2]: 427 if len (newtup[paramindex]) > 0: 428 newtup[paramindex] = reduce (lambda x, y: 429 x + "," + y, 430 newtup[paramindex]) 431 else: 432 newtup[paramindex] = "" 433 434 tup = tuple (newtup) 435 436 self._call_with_pk (use_pycups, 437 'DevicesGet', tup, reply_handler, error_handler, 438 self._unpack_getDevices_reply, 439 self._conn.getDevices, args, kwds) 440 441 def _unpack_getDevices_reply (self, dbusdict): 442 result_str = dict() 443 for key, value in dbusdict.items (): 444 if type (key) == dbus.String: 445 result_str[str (key)] = str (value) 446 else: 447 result_str[key] = value 448 449 # cups-pk-helper returns all devices in one dictionary. 450 # Keys of different devices are distinguished by ':n' postfix. 451 452 devices = dict() 453 n = 0 454 affix = ':' + str (n) 455 device_keys = [x for x in result_str.keys () if x.endswith (affix)] 456 while len (device_keys) > 0: 457 device_uri = None 458 device_dict = dict() 459 for keywithaffix in device_keys: 460 key = keywithaffix[:len (keywithaffix) - len (affix)] 461 if key != 'device-uri': 462 device_dict[key] = result_str[keywithaffix] 463 else: 464 device_uri = result_str[keywithaffix] 465 466 if device_uri is not None: 467 devices[device_uri] = device_dict 468 469 n += 1 470 affix = ':' + str (n) 471 device_keys = [x for x in result_str.keys () if x.endswith (affix)] 472 473 return devices 474 475 def cancelJob (self, *args, **kwds): 476 (use_pycups, reply_handler, error_handler, 477 tup) = self._args_kwds_to_tuple ([int, bool], 478 [(None, None), 479 (None, False)], # purge_job 480 args, kwds) 481 482 self._call_with_pk (use_pycups, 483 'JobCancelPurge', tup, reply_handler, error_handler, 484 self._nothing_to_unpack, 485 self._conn.cancelJob, args, kwds) 486 487 def setJobHoldUntil (self, *args, **kwds): 488 (use_pycups, reply_handler, error_handler, 489 tup) = self._args_kwds_to_tuple ([int, str], 490 [(None, None), 491 (None, None)], 492 args, kwds) 493 494 self._call_with_pk (use_pycups, 495 'JobSetHoldUntil', tup, reply_handler, 496 error_handler, self._nothing_to_unpack, 497 self._conn.setJobHoldUntil, args, kwds) 498 499 def restartJob (self, *args, **kwds): 500 (use_pycups, reply_handler, error_handler, 501 tup) = self._args_kwds_to_tuple ([int], 502 [(None, None)], 503 args, kwds) 504 505 self._call_with_pk (use_pycups, 506 'JobRestart', tup, reply_handler, 507 error_handler, self._nothing_to_unpack, 508 self._conn.restartJob, args, kwds) 509 510 def getFile (self, *args, **kwds): 511 (use_pycups, reply_handler, error_handler, 512 tup) = self._args_kwds_to_tuple ([str, str], 513 [("resource", None), 514 ("filename", None)], 515 args, kwds) 516 517 # getFile(resource, filename=None, fd=-1, file=None) -> None 518 if use_pycups: 519 if ((len (args) == 0 and 'resource' in kwds) or 520 (len (args) == 1)): 521 can_use_tempfile = True 522 for each in kwds.keys (): 523 if each not in ['resource', 'fd', 'file', 524 'reply_handler', 'error_handler']: 525 can_use_tempfile = False 526 break 527 528 if can_use_tempfile: 529 # We can still use PackageKit for this. 530 if len (args) == 0: 531 resource = kwds["resource"] 532 else: 533 resource = args[0] 534 535 wrapper = _WriteToTmpFile (kwds, 536 reply_handler, 537 error_handler) 538 self._call_with_pk (False, 539 'FileGet', 540 (resource, wrapper.get_filename ()), 541 wrapper.reply_handler, 542 wrapper.error_handler, 543 self._nothing_to_unpack, 544 self._conn.getFile, args, kwds) 545 return 546 547 self._call_with_pk (use_pycups, 548 'FileGet', tup, reply_handler, 549 error_handler, self._nothing_to_unpack, 550 self._conn.getFile, args, kwds) 551 552 ## etc 553 ## Still to implement: 554 ## putFile 555 ## addPrinter 556 ## setPrinterDevice 557 ## setPrinterInfo 558 ## setPrinterLocation 559 ## setPrinterShared 560 ## setPrinterJobSheets 561 ## setPrinterErrorPolicy 562 ## setPrinterOpPolicy 563 ## setPrinterUsersAllowed 564 ## setPrinterUsersDenied 565 ## addPrinterOptionDefault 566 ## deletePrinterOptionDefault 567 ## deletePrinter 568 ## addPrinterToClass 569 ## deletePrinterFromClass 570 ## deleteClass 571 ## setDefault 572 ## enablePrinter 573 ## disablePrinter 574 ## acceptJobs 575 ## rejectJobs 576 ## adminGetServerSettings 577 ## adminSetServerSettings 578 ## ... 579 580if __name__ == '__main__': 581 from gi.repository import GObject 582 from debug import set_debugging 583 set_debugging (True) 584 class UI: 585 def __init__ (self): 586 w = Gtk.Window () 587 v = Gtk.VBox () 588 w.add (v) 589 b = Gtk.Button.new_with_label ("Go") 590 v.pack_start (b, False, False, 0) 591 b.connect ("clicked", self.button_clicked) 592 b = Gtk.Button.new_with_label ("Fetch") 593 v.pack_start (b, False, False, 0) 594 b.connect ("clicked", self.fetch_clicked) 595 b.set_sensitive (False) 596 self.fetch_button = b 597 b = Gtk.Button.new_with_label ("Cancel job") 598 v.pack_start (b, False, False, 0) 599 b.connect ("clicked", self.cancel_clicked) 600 b.set_sensitive (False) 601 self.cancel_button = b 602 b = Gtk.Button.new_with_label ("Get file") 603 v.pack_start (b, False, False, 0) 604 b.connect ("clicked", self.get_file_clicked) 605 b.set_sensitive (False) 606 self.get_file_button = b 607 b = Gtk.Button.new_with_label ("Something harmless") 608 v.pack_start (b, False, False, 0) 609 b.connect ("clicked", self.harmless_clicked) 610 b.set_sensitive (False) 611 self.harmless_button = b 612 w.connect ("destroy", self.destroy) 613 w.show_all () 614 self.conn = None 615 debugprint ("+%s" % self) 616 617 def __del__ (self): 618 debug.debugprint ("-%s" % self) 619 620 def destroy (self, window): 621 debugprint ("DESTROY: %s" % self) 622 try: 623 self.conn.destroy () 624 del self.conn 625 except AttributeError: 626 pass 627 628 Gtk.main_quit () 629 630 def button_clicked (self, button): 631 if self.conn: 632 self.conn.destroy () 633 634 self.conn = PK1Connection (reply_handler=self.connected, 635 error_handler=self.connection_error) 636 637 def connected (self, conn, result): 638 print("Connected") 639 self.fetch_button.set_sensitive (True) 640 self.cancel_button.set_sensitive (True) 641 self.get_file_button.set_sensitive (True) 642 self.harmless_button.set_sensitive (True) 643 644 def connection_error (self, conn, error): 645 print("Failed to connect") 646 raise error 647 648 def fetch_clicked (self, button): 649 print ("fetch devices...") 650 self.conn.getDevices (reply_handler=self.got_devices, 651 error_handler=self.get_devices_error) 652 653 def got_devices (self, conn, devices): 654 if conn != self.conn: 655 print("Ignoring stale reply") 656 return 657 658 print("got devices: %s" % devices) 659 660 def get_devices_error (self, conn, exc): 661 if conn != self.conn: 662 print("Ignoring stale error") 663 return 664 665 print("devices error: %s" % repr (exc)) 666 667 def cancel_clicked (self, button): 668 print("Cancel job...") 669 self.conn.cancelJob (1, 670 reply_handler=self.job_canceled, 671 error_handler=self.cancel_job_error) 672 673 def job_canceled (self, conn, none): 674 if conn != self.conn: 675 print("Ignoring stale reply for %s" % conn) 676 return 677 678 print("Job canceled") 679 680 def cancel_job_error (self, conn, exc): 681 if conn != self.conn: 682 print("Ignoring stale error for %s" % conn) 683 return 684 685 print("cancel error: %s" % repr (exc)) 686 687 def get_file_clicked (self, button): 688 self.my_file = open ("cupsd.conf", "w") 689 self.conn.getFile ("/admin/conf/cupsd.conf", file=self.my_file, 690 reply_handler=self.got_file, 691 error_handler=self.get_file_error) 692 693 def got_file (self, conn, none): 694 if conn != self.conn: 695 print("Ignoring stale reply for %s" % conn) 696 return 697 698 print("Got file") 699 700 def get_file_error (self, conn, exc): 701 if conn != self.conn: 702 print("Ignoring stale error") 703 return 704 705 print("get file error: %s" % repr (exc)) 706 707 def harmless_clicked (self, button): 708 self.conn.getJobs (reply_handler=self.got_jobs, 709 error_handler=self.get_jobs_error) 710 711 def got_jobs (self, conn, result): 712 if conn != self.conn: 713 print("Ignoring stale reply from %s" % repr (conn)) 714 return 715 print(result) 716 717 def get_jobs_error (self, exc): 718 print("get jobs error: %s" % repr (exc)) 719 720 UI () 721 from dbus.mainloop.glib import DBusGMainLoop 722 DBusGMainLoop (set_as_default=True) 723 Gtk.main () 724