#!/usr/bin/python3
## Copyright (C) 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2015 Red Hat, Inc.
## Copyright (C) 2008 Novell, Inc.
## Author: Tim Waugh <twaugh@redhat.com>
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details.
## You should have received a copy of the GNU General Public License
## along with this program; if not, write to the Free Software
## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
import threading
import config
import cups
from gi.repository import GObject
from gi.repository import GLib
from gi.repository import Gdk
from gi.repository import Gtk
import queue
cups.require ("1.9.60")
import authconn
from debug import *
import debug
import gettext
gettext.install(domain=config.PACKAGE, localedir=config.localedir)
######
###### An asynchronous libcups API using IPP with a separate worker
###### thread.
######
###
### This is the worker thread.
###
class _IPPConnectionThread(threading.Thread):
def __init__ (self, myqueue, conn, reply_handler=None, error_handler=None,
auth_handler=None, user=None, host=None, port=None,
encryption=None):
threading.Thread.__init__ (self)
self.setDaemon (True)
self._queue = myqueue
self._conn = conn
self.host = host
self.port = port
self._encryption = encryption
self._reply_handler = reply_handler
self._error_handler = error_handler
self._auth_handler = auth_handler
self._auth_queue = queue.Queue(1)
self.user = user
self._destroyed = False
debugprint ("+%s" % self)
def __del__ (self):
debug.debugprint ("-%s" % self)
def set_auth_info (self, password):
self._auth_queue.put (password)
def run (self):
if self.host is None:
self.host = cups.getServer ()
if self.port is None:
self.port = cups.getPort ()
if self._encryption is None:
self._encryption = cups.getEncryption ()
if self.user:
cups.setUser (self.user)
else:
self.user = cups.getUser ()
cups.setPasswordCB2 (self._auth)
try:
conn = cups.Connection (host=self.host,
port=self.port,
encryption=self._encryption)
self._reply (None)
except RuntimeError as e:
conn = None
self._error (e)
while True:
# Wait to find out what operation to try.
debugprint ("Awaiting further instructions")
self.idle = self._queue.empty ()
item = self._queue.get ()
debugprint ("Next task: %s" % repr (item))
if item is None:
# Our signal to quit.
self._queue.task_done ()
break
elif self._destroyed:
# Just mark all tasks done
self._queue.task_done ()
continue
self.idle = False
(fn, args, kwds, rh, eh, ah) = item
if rh != False:
self._reply_handler = rh
if eh != False:
self._error_handler = eh
if ah != False:
self._auth_handler = ah
if fn == True:
# Our signal to change user and reconnect.
self.user = args[0]
cups.setUser (self.user)
debugprint ("Set user=%s; reconnecting..." % self.user)
cups.setPasswordCB2 (self._auth)
try:
conn = cups.Connection (host=self.host,
port=self.port,
encryption=self._encryption)
debugprint ("...reconnected")
self._queue.task_done ()
self._reply (None)
except RuntimeError as e:
debugprint ("...failed")
self._queue.task_done ()
self._error (e)
continue
# Normal IPP operation. Try to perform it.
try:
debugprint ("Call %s" % fn)
result = fn (conn, *args, **kwds)
if fn == cups.Connection.adminGetServerSettings.__call__:
# Special case for a rubbish bit of API.
if result == {}:
# Authentication failed, but we aren't told that.
raise cups.IPPError (cups.IPP_NOT_AUTHORIZED, '')
debugprint ("...success")
self._reply (result)
except Exception as e:
debugprint ("...failure (%s)" % repr (e))
self._error (e)
self._queue.task_done ()
debugprint ("Thread exiting")
del self._conn # already destroyed
del self._reply_handler
del self._error_handler
del self._auth_handler
del self._queue
del self._auth_queue
del conn
cups.setPasswordCB2 (None)
def stop (self):
self._destroyed = True
self._queue.put (None)
def _auth (self, prompt, conn=None, method=None, resource=None):
def prompt_auth (prompt):
Gdk.threads_enter ()
if conn is None:
self._auth_handler (prompt, self._conn)
else:
self._auth_handler (prompt, self._conn, method, resource)
Gdk.threads_leave ()
return False
if self._auth_handler is None:
return ""
GLib.idle_add (prompt_auth, prompt)
password = self._auth_queue.get ()
return password
def _reply (self, result):
def send_reply (handler, result):
if not self._destroyed:
Gdk.threads_enter ()
handler (self._conn, result)
Gdk.threads_leave ()
return False
if not self._destroyed and self._reply_handler:
GLib.idle_add (send_reply, self._reply_handler, result)
def _error (self, exc):
def send_error (handler, exc):
if not self._destroyed:
Gdk.threads_enter ()
handler (self._conn, exc)
Gdk.threads_leave ()
return False
if not self._destroyed and self._error_handler:
debugprint ("Add %s to idle" % self._error_handler)
GLib.idle_add (send_error, self._error_handler, exc)
###
### This is the user-visible class. Although it does not inherit from
### cups.Connection it implements the same functions.
###
class IPPConnection:
"""
This class starts a new thread to handle IPP operations.
Each IPP operation method takes optional reply_handler,
error_handler and auth_handler parameters.
If an operation requires a password to proceed, the auth_handler
function will be called. The operation will continue once
set_auth_info (in this class) is called.
Once the operation has finished either reply_handler or
error_handler will be called.
"""
def __init__ (self, reply_handler=None, error_handler=None,
auth_handler=None, user=None, host=None, port=None,
encryption=None, parent=None):
debugprint ("New IPPConnection")
self._parent = parent
self.queue = queue.Queue ()
self.thread = _IPPConnectionThread (self.queue, self,
reply_handler=reply_handler,
error_handler=error_handler,
auth_handler=auth_handler,
user=user, host=host, port=port,
encryption=encryption)
self.thread.start ()
methodtype = type (cups.Connection.getPrinters)
bindings = []
for fname in dir (cups.Connection):
if fname[0] == ' ':
continue
fn = getattr (cups.Connection, fname)
if type (fn) != methodtype:
continue
setattr (self, fname, self._make_binding (fn))
bindings.append (fname)
self.bindings = bindings
debugprint ("+%s" % self)
def __del__ (self):
debug.debugprint ("-%s" % self)
def destroy (self):
debugprint ("DESTROY: %s" % self)
for binding in self.bindings:
delattr (self, binding)
if self.thread.isAlive ():
debugprint ("Stopping worker thread")
self.thread.stop ()
GLib.timeout_add_seconds (1, self._reap_thread)
def _reap_thread (self):
if self.thread.idle:
self.queue.join ()
return False
debugprint ("Thread %s still processing tasks" % self.thread)
return True
def set_auth_info (self, password):
"""Call this from your auth_handler function."""
self.thread.set_auth_info (password)
def reconnect (self, user, reply_handler=None, error_handler=None):
debugprint ("Reconnect...")
self.queue.put ((True, (user,), {},
reply_handler, error_handler, False))
def _make_binding (self, fn):
return lambda *args, **kwds: self._call_function (fn, *args, **kwds)
def _call_function (self, fn, *args, **kwds):
reply_handler = error_handler = auth_handler = False
if "reply_handler" in kwds:
reply_handler = kwds["reply_handler"]
del kwds["reply_handler"]
if "error_handler" in kwds:
error_handler = kwds["error_handler"]
del kwds["error_handler"]
if "auth_handler" in kwds:
auth_handler = kwds["auth_handler"]
del kwds["auth_handler"]
self.queue.put ((fn, args, kwds,
reply_handler, error_handler, auth_handler))
######
###### An asynchronous libcups API with graphical authentication and
###### retrying.
######
###
### A class to take care of an individual operation.
###
class _IPPAuthOperation:
def __init__ (self, reply_handler, error_handler, conn,
user=None, fn=None, args=None, kwds=None):
self._auth_called = False
self._dialog_shown = False
self._use_password = ''
self._cancel = False
self._reconnect = False
self._reconnected = False
self._user = user
self._conn = conn
self._try_as_root = self._conn.try_as_root
self._client_fn = fn
self._client_args = args
self._client_kwds = kwds
self._client_reply_handler = reply_handler
self._client_error_handler = error_handler
debugprint ("+%s" % self)
def __del__ (self):
debug.debugprint ("-%s" % self)
def _destroy (self):
del self._conn
del self._client_fn
del self._client_args
del self._client_kwds
del self._client_reply_handler
del self._client_error_handler
def error_handler (self, conn, exc):
if self._client_fn is None:
# This is the initial "connection" operation, or a
# subsequent reconnection attempt.
debugprint ("Connection/reconnection failed")
return self._reconnect_error (conn, exc)
if self._cancel:
debugprint ("%s (_error_handler): canceled so chaining up" % self)
return self._error (exc)
if self._reconnect:
self._reconnect = False
self._reconnected = True
debugprint ("%s (_error_handler): reconnecting (as %s)..." %
(self, self._user))
conn.reconnect (self._user,
reply_handler=self._reconnect_reply,
error_handler=self._reconnect_error)
return
forbidden = False
if type (exc) == cups.IPPError:
(e, m) = exc.args
if (e == cups.IPP_NOT_AUTHORIZED or
e == cups.IPP_FORBIDDEN or
e == cups.IPP_AUTHENTICATION_CANCELED):
forbidden = (e == cups.IPP_FORBIDDEN)
elif e == cups.IPP_SERVICE_UNAVAILABLE:
return self._reconnect_error (conn, exc)
else:
return self._error (exc)
elif type (exc) == cups.HTTPError:
(s,) = exc.args
if (s == cups.HTTP_UNAUTHORIZED or
s == cups.HTTP_FORBIDDEN):
forbidden = (s == cups.HTTP_FORBIDDEN)
else:
return self._error (exc)
else:
return self._error (exc)
# Not authorized.
if forbidden:
debugprint ("%s (_error_handler): forbidden" % self)
else:
debugprint ("%s (_error_handler): not authorized" % self)
if (self._try_as_root and
self._user != 'root' and
(self._conn.thread.host[0] == '/' or forbidden)):
# This is a UNIX domain socket connection so we should
# not have needed a password (or it is not a UDS but
# we got an HTTP_FORBIDDEN response), and so the
# operation must not be something that the current
# user is authorised to do. They need to try as root,
# and supply the password. However, to get the right
# prompt, we need to try as root but with no password
# first.
debugprint ("Authentication: Try as root")
self._user = "root"
conn.reconnect (self._user,
reply_handler=self._reconnect_reply,
error_handler=self._reconnect_error)
# Don't submit the task until we've connected.
return
if not self._auth_called:
# We aren't even getting a chance to supply credentials.
return self._error (exc)
# Now reconnect and retry.
host = conn.thread.host
port = conn.thread.port
authconn.global_authinfocache.remove_auth_info (host=host,
port=port)
self._use_password = ''
debugprint ("%s (_error_handler): reconnecting (as %s)..." %
(self, self._user))
conn.reconnect (self._user,
reply_handler=self._reconnect_reply,
error_handler=self._reconnect_error)
def auth_handler (self, prompt, conn, method=None, resource=None):
if self._auth_called == False:
if self._user is None:
self._user = cups.getUser()
if self._user:
host = conn.thread.host
port = conn.thread.port
creds = authconn.global_authinfocache.lookup_auth_info (host=host,
port=port)
if creds:
if creds[0] == self._user:
self._use_password = creds[1]
self._reconnected = True
del creds
else:
host = conn.thread.host
port = conn.thread.port
authconn.global_authinfocache.remove_auth_info (host=host,
port=port)
self._use_password = ''
self._auth_called = True
if self._reconnected:
debugprint ("Supplying password after reconnection")
self._reconnected = False
conn.set_auth_info (self._use_password)
return
self._reconnected = False
if not conn.prompt_allowed:
conn.set_auth_info (self._use_password)
return
# If we've previously prompted, explain why we're prompting again.
if self._dialog_shown:
d = Gtk.MessageDialog (parent=self._conn.parent,
modal=True, destroy_with_parent=True,
message_type=Gtk.MessageType.ERROR,
buttons=Gtk.ButtonsType.CLOSE,
text=_("Not authorized"))
d.format_secondary_text (_("The password may be incorrect."))
d.run ()
d.destroy ()
op = None
if conn.semantic:
op = conn.semantic.current_operation ()
if op is None:
d = authconn.AuthDialog (parent=conn.parent)
else:
title = _("Authentication (%s)") % op
d = authconn.AuthDialog (title=title,
parent=conn.parent)
d.set_prompt ('')
if self._user is None:
self._user = cups.getUser()
d.set_auth_info (['', ''])
d.field_grab_focus ('username')
d.set_keep_above (True)
d.show_all ()
d.connect ("response", self._on_auth_dialog_response)
self._dialog_shown = True
def submit_task (self):
self._auth_called = False
self._conn.queue.put ((self._client_fn, self._client_args,
self._client_kwds,
self._client_reply_handler,
# Use our own error and auth handlers.
self.error_handler,
self.auth_handler))
def _on_auth_dialog_response (self, dialog, response):
(user, password) = dialog.get_auth_info ()
if user == '':
user = self._user;
authconn.global_authinfocache.cache_auth_info ((user,
password),
host=self._conn.thread.host,
port=self._conn.thread.port)
self._dialog = dialog
dialog.hide ()
if (response == Gtk.ResponseType.CANCEL or
response == Gtk.ResponseType.DELETE_EVENT):
self._cancel = True
self._conn.set_auth_info ('')
authconn.global_authinfocache.remove_auth_info (host=self._conn.thread.host,
port=self._conn.thread.port)
debugprint ("Auth canceled")
return
if user == self._user:
self._use_password = password
self._conn.set_auth_info (password)
debugprint ("Password supplied.")
return
self._user = user
self._use_password = password
self._reconnect = True
self._conn.set_auth_info ('')
debugprint ("Will try as %s" % self._user)
def _reconnect_reply (self, conn, result):
# A different username was given in the authentication dialog,
# so we've reconnected as that user. Alternatively, the
# connection has failed and we're retrying.
debugprint ("Connected as %s" % self._user)
if self._client_fn is not None:
self.submit_task ()
def _reconnect_error (self, conn, exc):
debugprint ("Failed to connect as %s" % self._user)
if not self._conn.prompt_allowed:
self._error (exc)
return
op = None
if conn.semantic:
op = conn.semantic.current_operation ()
if op is None:
msg = _("CUPS server error")
else:
msg = _("CUPS server error (%s)") % op
d = Gtk.MessageDialog (parent=self._conn.parent,
modal=True, destroy_with_parent=True,
message_type=Gtk.MessageType.ERROR,
buttons=Gtk.ButtonsType.NONE,
text=msg)
if self._client_fn is None and type (exc) == RuntimeError:
# This was a connection failure.
message = 'service-error-service-unavailable'
elif type (exc) == cups.IPPError:
message = exc.args[1]
else:
message = repr (exc)
d.format_secondary_text (_("There was an error during the "
"CUPS operation: '%s'." % message))
d.add_buttons (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
_("Retry"), Gtk.ResponseType.OK)
d.set_default_response (Gtk.ResponseType.OK)
d.connect ("response", self._on_retry_server_error_response)
debugprint ("%s (_reconnect_error): presenting error dialog (%s; %s)" %
(self, msg, message))
d.show ()
def _on_retry_server_error_response (self, dialog, response):
dialog.destroy ()
if response == Gtk.ResponseType.OK:
debugprint ("%s: got retry response, reconnecting (as %s)..." %
(self, self._conn.thread.user))
self._conn.reconnect (self._conn.thread.user,
reply_handler=self._reconnect_reply,
error_handler=self._reconnect_error)
else:
debugprint ("%s: got cancel response" % self)
self._error (cups.IPPError (0, _("Operation canceled")))
def _error (self, exc):
debugprint ("%s (_error): handling %s" % (self, repr (exc)))
if self._client_error_handler:
debugprint ("%s (_error): calling %s" %
(self, self._client_error_handler))
self._client_error_handler (self._conn, exc)
self._destroy ()
else:
debugprint ("%s (_error): no client error handler set" % self)
###
### The user-visible class.
###
class IPPAuthConnection(IPPConnection):
def __init__ (self, reply_handler=None, error_handler=None,
auth_handler=None, host=None, port=None, encryption=None,
parent=None, try_as_root=True, prompt_allowed=True,
semantic=None):
self.parent = parent
self.prompt_allowed = prompt_allowed
self.try_as_root = try_as_root
self.semantic = semantic
user = None
creds = authconn.global_authinfocache.lookup_auth_info (host=host,
port=port)
if creds:
if creds[0] != 'root' or try_as_root:
user = creds[0]
del creds
# The "connect" operation.
op = _IPPAuthOperation (reply_handler, error_handler, self)
IPPConnection.__init__ (self, reply_handler=reply_handler,
error_handler=op.error_handler,
auth_handler=op.auth_handler, user=user,
host=host, port=port, encryption=encryption)
def destroy (self):
self.semantic = None
IPPConnection.destroy (self)
def _call_function (self, fn, *args, **kwds):
reply_handler = error_handler = auth_handler = False
if "reply_handler" in kwds:
reply_handler = kwds["reply_handler"]
del kwds["reply_handler"]
if "error_handler" in kwds:
error_handler = kwds["error_handler"]
del kwds["error_handler"]
if "auth_handler" in kwds:
auth_handler = kwds["auth_handler"]
del kwds["auth_handler"]
# Store enough information about the current operation to
# restart it if necessary.
op = _IPPAuthOperation (reply_handler, error_handler, self,
self.thread.user, fn, args, kwds)
# Run the operation but use our own error and auth handlers.
op.submit_task ()
if __name__ == "__main__":
# Demo
set_debugging (True)
class UI:
def __init__ (self):
w = Gtk.Window ()
w.connect ("destroy", self.destroy)
b = Gtk.Button.new_with_label ("Connect")
b.connect ("clicked", self.connect_clicked)
vbox = Gtk.VBox ()
vbox.pack_start (b, False, False, 0)
w.add (vbox)
self.get_devices_button = Gtk.Button.new_with_label ("Get Devices")
self.get_devices_button.connect ("clicked", self.get_devices)
self.get_devices_button.set_sensitive (False)
vbox.pack_start (self.get_devices_button, False, False, 0)
self.conn = None
w.show_all ()
def destroy (self, window):
try:
self.conn.destroy ()
except AttributeError:
pass
Gtk.main_quit ()
def connect_clicked (self, button):
if self.conn:
self.conn.destroy ()
self.conn = IPPAuthConnection (reply_handler=self.connected,
error_handler=self.connect_failed)
def connected (self, conn, result):
debugprint ("Success: %s" % repr (result))
self.get_devices_button.set_sensitive (True)
def connect_failed (self, conn, exc):
debugprint ("Exc %s" % repr (exc))
self.get_devices_button.set_sensitive (False)
self.conn.destroy ()
def get_devices (self, button):
button.set_sensitive (False)
debugprint ("Getting devices")
self.conn.getDevices (reply_handler=self.get_devices_reply,
error_handler=self.get_devices_error)
def get_devices_reply (self, conn, result):
if conn != self.conn:
debugprint ("Ignoring stale reply")
return
debugprint ("Got devices: %s" % repr (result))
self.get_devices_button.set_sensitive (True)
def get_devices_error (self, conn, exc):
if conn != self.conn:
debugprint ("Ignoring stale error")
return
debugprint ("Error getting devices: %s" % repr (exc))
self.get_devices_button.set_sensitive (True)
UI ()
Gtk.main ()