#! /usr/bin/env python # -*- coding: ISO-8859-15 -*- # A notifier for PyKota # # PyKota - Print Quotas for CUPS and LPRng # # (c) 2003, 2004, 2005, 2006 Jerome Alet # 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. # # $Id$ # # import sys import os import popen2 import socket import signal import xmlrpclib try : import PAM except ImportError : hasPAM = 0 else : hasPAM = 1 from pykota.tool import Tool, PyKotaToolError, PyKotaCommandLineError, crashed, N_ __doc__ = N_("""pknotify v%(__version__)s (c) %(__years__)s %(__author__)s Notifies or ask questions to end users who launched the PyKotIcon application. command line usage : pknotify [options] [arguments] options : -v | --version Prints pkbanner's version number then exits. -h | --help Prints this message then exits. -d | --destination h[:p] Sets the destination hostname and optional port onto which contact the remote PyKotIcon application. This option is mandatory. When not specified, the port defaults to 7654. -a | --ask Tells pknotify to ask something to the end user. Then pknotify will output the result. -C | --checkauth When --ask is used and both an 'username' and a 'password' are asked to the end user, then pknotify will try to authenticate the user through PAM. If authentified, this program will print "AUTH=YES", else "AUTH=NO". If a field is missing, "AUTH=IMPOSSIBLE" will be printed. If the user is authenticated, then "USERNAME=xxxx" will be printed as well. -c | --confirm Tells pknotify to ask for either a confirmation or abortion. -D | --denyafter N With --checkauth above, makes pknotify loop up to N times if the password is incorrect. After having reached the limit, "DENY" will be printed, which effectively rejects the job. The default value of N is 1, meaning the job is denied after the first unsuccessful try. -n | --notify Tells pknotify to send an informational message message to the end user. -q | --quit Tells pknotify to send a message asking the PyKotIcon application to exit. This option can be combined with the other ones to make PyKotIcon exit after having sent the answer from the dialog. -t | --timeout T Tells pknotify to ignore the end user's answer if it comes pas T seconds after the dialog box being opened. The default value is 0 seconds, which tells pknotify to wait indefinitely. Use this option to avoid having an user who leaved his computer stall a whole print queue. You MUST specify either --ask, --confirm, --notify or --quit. arguments : -a | --ask : Several arguments are accepted, or the form "label:varname:defaultvalue". The result will be printed to stdout in the following format : VAR1NAME=VAR1VALUE VAR2NAME=VAR2VALUE ... If the dialog was cancelled, nothing will be printed. If one of the varname is 'password' then this field is asked as a password (you won't see what you type in), and is NOT printed. Although it is not printed, it will be used to check if authentication is valid if you specify --checkauth. -c | --confirm : A single argument is expected, representing the message to display. If the dialog is confirmed then pknotify will print OK, else CANCEL. -n | --notify : A single argument is expected, representing the message to display. In this case pknotify will always print OK. examples : pknotify -d client:7654 --confirm "This job costs :\n10 credits !" Would display the cost of a print job and asks for confirmation. pknotify --destination $PYKOTAJOBORIGINATINGHOSTNAME:7654 \\ --checkauth --ask "Your name:username:" "Your password:password:" Asks an username and password, and checks if they are valid. NB : The PYKOTAJOBORIGINATINGHOSTNAME environment variable is only set if you launch pknotify from cupspykota through a directive in ~pykota/pykota.conf The TCP port you'll use must be reachable on the client from the print server. """) class TimeoutError(Exception) : """An exception for timeouts.""" def __init__(self, message = ""): self.message = message Exception.__init__(self, message) def __repr__(self): return self.message __str__ = __repr__ class PyKotaNotify(Tool) : """A class for pknotify.""" def sanitizeMessage(self, msg) : """Replaces \\n and returns a messagee in xmlrpclib Binary format.""" return xmlrpclib.Binary(msg.replace("\\n", "\n")) def convPAM(self, auth, queries=[], userdata=None) : """Prepares PAM datas.""" response = [] for (query, qtype) in queries : if qtype == PAM.PAM_PROMPT_ECHO_OFF : response.append((self.password, 0)) elif qtype in (PAM.PAM_PROMPT_ECHO_ON, PAM.PAM_ERROR_MSG, PAM.PAM_TEXT_INFO) : self.printInfo("Unexpected PAM query : %s (%s)" % (query, qtype), "warn") response.append(('', 0)) else: return None return response def checkAuth(self, username, password) : """Checks if we could authenticate an username with a password.""" if not hasPAM : raise PyKotaCommandLineError, _("You MUST install PyPAM for this functionnality to work !") else : retcode = False self.password = password self.regainPriv() auth = PAM.pam() auth.start("passwd") auth.set_item(PAM.PAM_USER, username) auth.set_item(PAM.PAM_CONV, self.convPAM) try : auth.authenticate() auth.acct_mgmt() except PAM.error, resp : self.printInfo(_("Authentication error for user %s : %s") % (username, resp), "warn") except : self.printInfo(_("Internal error : can't authenticate user %s") % username, "error") else : self.logdebug(_("Password correct for user %s") % username) retcode = True self.dropPriv() return retcode def alarmHandler(self, signum, frame) : """Alarm handler.""" raise TimeoutError, _("The end user at %s:%i didn't answer within %i seconds. The print job will be cancelled.") % (self.destination, self.port, self.timeout) def main(self, arguments, options) : """Notifies or asks questions to end users through PyKotIcon.""" try : (self.destination, self.port) = options["destination"].split(":") self.port = int(self.port) except ValueError : self.destination = options["destination"] self.port = 7654 try : denyafter = int(options["denyafter"]) if denyafter < 1 : raise ValueError except (ValueError, TypeError) : denyafter = 1 try : self.timeout = int(options["timeout"]) if self.timeout < 0 : raise ValueError except (ValueError, TypeError) : self.timeout = 0 if self.timeout : signal.signal(signal.SIGALRM, self.alarmHandler) signal.alarm(self.timeout) try : server = xmlrpclib.ServerProxy("http://%s:%s" % (self.destination, self.port)) if options["ask"] : labels = [] varnames = [] varvalues = {} for arg in arguments : try : (label, varname, varvalue) = arg.split(":", 2) except ValueError : raise PyKotaCommandLineError, "argument '%s' is invalid !" % arg labels.append(self.sanitizeMessage(label)) varname = varname.lower() varnames.append(varname) varvalues[varname] = self.sanitizeMessage(varvalue) result = server.askDatas(labels, varnames, varvalues) if result["isValid"] : authok = None if options["checkauth"] : if ("username" in varnames) and ("password" in varnames) : if self.checkAuth(result["username"].data, result["password"].data) : authok = "AUTH=YES" else : authok = "AUTH=NO" else : authok = "AUTH=IMPOSSIBLE" for varname in varnames : if (varname != "password") \ and ((varname != "username") or (authok in (None, "AUTH=YES"))) : print "%s=%s" % (varname.upper(), result[varname].data) if authok is not None : print authok elif options["confirm"] : print server.showDialog(self.sanitizeMessage(arguments[0]), True) elif options["notify"] : print server.showDialog(self.sanitizeMessage(arguments[0]), False) if options["quit"] : server.quitApplication() except (socket.error, socket.gaierror), msg : raise PyKotaCommandLineError, "%s : %s" % (_("Connection error"), str(msg)) except TimeoutError, msg : self.printInfo(msg, "warn") print "CANCEL" # Timeout occured : job is cancelled. if self.timeout : signal.alarm(0) if __name__ == "__main__" : retcode = 0 try : defaults = { \ "denyafter" : 1, "timeout" : 0, } short_options = "vhd:acnqCD:t:" long_options = ["help", "version", "destination=", "denyafter=", \ "timeout=", "ask", "checkauth", "confirm", "notify", \ "quit" ] # Initializes the command line tool notifier = PyKotaNotify(doc=__doc__) notifier.deferredInit() # parse and checks the command line (options, args) = notifier.parseCommandline(sys.argv[1:], short_options, long_options) # sets long options options["help"] = options["h"] or options["help"] options["version"] = options["v"] or options["version"] options["destination"] = options["d"] or options["destination"] options["ask"] = options["a"] or options["ask"] options["confirm"] = options["c"] or options["confirm"] options["notify"] = options["n"] or options["notify"] options["quit"] = options["q"] or options["quit"] options["checkauth"] = options["C"] or options["checkauth"] options["denyafter"] = options["D"] or options["denyafter"] or defaults["denyafter"] options["timeout"] = options["t"] or options["timeout"] or defaults["timeout"] if options["help"] : notifier.display_usage_and_quit() elif options["version"] : notifier.display_version_and_quit() elif (options["ask"] and (options["confirm"] or options["notify"])) \ or (options["confirm"] and (options["ask"] or options["notify"])) \ or ((options["checkauth"] or options["denyafter"]) and not options["ask"]) \ or (options["notify"] and (options["ask"] or options["confirm"])) : raise PyKotaCommandLineError, _("incompatible options, see help.") elif (not options["destination"]) \ or not (options["quit"] or options["ask"] or options["confirm"] or options["notify"]) : raise PyKotaCommandLineError, _("some options are mandatory, see help.") elif (not args) and (not options["quit"]) : raise PyKotaCommandLineError, _("some options require arguments, see help.") else : retcode = notifier.main(args, options) except KeyboardInterrupt : sys.stderr.write("\nInterrupted with Ctrl+C !\n") retcode = -3 except PyKotaCommandLineError, msg : sys.stderr.write("%s : %s\n" % (sys.argv[0], msg)) retcode = -2 except SystemExit : pass except : try : notifier.crashed("%s failed" % sys.argv[0]) except : crashed("%s failed" % sys.argv[0]) retcode = -1 sys.exit(retcode)