root / pykota / trunk / bin / pknotify @ 3118

Revision 3077, 16.7 kB (checked in by jerome, 18 years ago)

Improved error handling.

  • Property svn:executable set to *
  • Property svn:keywords set to Author Date Id Revision
Line 
1#! /usr/bin/env python
2# -*- coding: ISO-8859-15 -*-
3
4# A notifier for PyKota
5#
6# PyKota - Print Quotas for CUPS and LPRng
7#
8# (c) 2003, 2004, 2005, 2006 Jerome Alet <alet@librelogiciel.com>
9# This program is free software; you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation; either version 2 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program; if not, write to the Free Software
21# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
22#
23# $Id$
24#
25#
26
27import sys
28import socket
29import errno
30import signal
31import xmlrpclib
32
33try :
34    import PAM
35except ImportError :   
36    hasPAM = 0
37else :   
38    hasPAM = 1
39
40from pykota.tool import Tool, PyKotaToolError, PyKotaCommandLineError, crashed, N_
41
42__doc__ = N_("""pknotify v%(__version__)s (c) %(__years__)s %(__author__)s
43
44Notifies or ask questions to end users who launched the PyKotIcon application.
45
46command line usage :
47
48  pknotify  [options]  [arguments]
49
50options :
51
52  -v | --version             Prints pknotify's version number then exits.
53  -h | --help                Prints this message then exits.
54 
55  -d | --destination h[:p]   Sets the destination hostname and optional
56                             port onto which contact the remote PyKotIcon
57                             application. This option is mandatory.
58                             When not specified, the port defaults to 7654.
59                             
60  -a | --ask                 Tells pknotify to ask something to the end
61                             user. Then pknotify will output the result.
62                       
63  -C | --checkauth           When --ask is used and both an 'username' and a
64                             'password' are asked to the end user, then
65                             pknotify will try to authenticate the user
66                             through PAM. If authentified, this program
67                             will print "AUTH=YES", else "AUTH=NO".
68                             If a field is missing, "AUTH=IMPOSSIBLE" will
69                             be printed. If the user is authenticated, then
70                             "USERNAME=xxxx" will be printed as well.
71                             
72  -c | --confirm             Tells pknotify to ask for either a confirmation                       
73                             or abortion.
74                             
75  -D | --denyafter N         With --checkauth above, makes pknotify loop                           
76                             up to N times if the password is incorrect.
77                             After having reached the limit, "DENY" will
78                             be printed, which effectively rejects the job.
79                             The default value of N is 1, meaning the job
80                             is denied after the first unsuccessful try.
81                             
82  -N | --noremote action     If it's impossible to connect to the remote
83                             PyKotIcon machine, do this action instead.
84                             Allowed actions are 'CONTINUE' and 'CANCEL',
85                             which will respectively allow the processing
86                             of the print job to continue, or the job to
87                             be cancelled. The default value is CANCEL.
88                             
89  -n | --notify              Tells pknotify to send an informational message
90                             to the end user.
91                             
92  -q | --quit                Tells pknotify to send a message asking the
93                             PyKotIcon application to exit. This option can
94                             be combined with the other ones to make PyKotIcon
95                             exit after having sent the answer from the dialog.
96                             
97  -t | --timeout T           Tells pknotify to ignore the end user's answer if
98                             it comes past T seconds after the dialog box being
99                             opened. The default value is 0 seconds, which
100                             tells pknotify to wait indefinitely.
101                             Use this option to avoid having an user who
102                             leaved his computer stall a whole print queue.
103                             
104  You MUST specify either --ask, --confirm, --notify or --quit.
105
106  arguments :             
107 
108    -a | --ask : Several arguments are accepted, of the form
109                 "label:varname:defaultvalue". The result will
110                 be printed to stdout in the following format :
111                 VAR1NAME=VAR1VALUE
112                 VAR2NAME=VAR2VALUE
113                 ...
114                 If the dialog was cancelled, nothing will be
115                 printed. If one of the varname is 'password'
116                 then this field is asked as a password (you won't
117                 see what you type in), and is NOT printed. Although
118                 it is not printed, it will be used to check if
119                 authentication is valid if you specify --checkauth.
120                 
121    -c | --confirm : A single argument is expected, representing the
122                     message to display. If the dialog is confirmed
123                     then pknotify will print OK, else CANCEL.
124                     
125    -n | --notify : A single argument is expected, representing the                 
126                    message to display. In this case pknotify will
127                    always print OK.
128                   
129examples :                   
130
131  pknotify -d client:7654 --noremote CONTINUE --confirm "This job costs 10 credits"
132 
133  Would display the cost of the print job and asks for confirmation.
134  If the end user doesn't have PyKotIcon running and accepting connections
135  from the print server, PyKota will consider that the end user accepted
136  to print this job.
137 
138  pknotify --destination $PYKOTAJOBORIGINATINGHOSTNAME:7654 \\
139           --checkauth --ask "Your name:username:" "Your password:password:"
140           
141  Asks an username and password, and checks if they are valid.         
142  NB : The PYKOTAJOBORIGINATINGHOSTNAME environment variable is
143  only set if you launch pknotify from cupspykota through a directive
144  in ~pykota/pykota.conf
145 
146  The TCP port you'll use must be reachable on the client from the
147  print server.
148""")
149       
150class TimeoutError(Exception) :       
151    """An exception for timeouts."""
152    def __init__(self, message = ""):
153        self.message = message
154        Exception.__init__(self, message)
155    def __repr__(self):
156        return self.message
157    __str__ = __repr__
158   
159class PyKotaNotify(Tool) :       
160    """A class for pknotify."""
161    def sanitizeMessage(self, msg) :
162        """Replaces \\n and returns a messagee in xmlrpclib Binary format."""
163        return xmlrpclib.Binary(self.userCharsetToUTF8(msg.replace("\\n", "\n")))
164       
165    def convPAM(self, auth, queries=[], userdata=None) :
166        """Prepares PAM datas."""
167        response = []
168        for (query, qtype) in queries :
169            if qtype == PAM.PAM_PROMPT_ECHO_OFF :
170                response.append((self.password, 0))
171            elif qtype in (PAM.PAM_PROMPT_ECHO_ON, PAM.PAM_ERROR_MSG, PAM.PAM_TEXT_INFO) :
172                self.printInfo("Unexpected PAM query : %s (%s)" % (query, qtype), "warn")
173                response.append(('', 0))
174            else:
175                return None
176        return response
177
178    def checkAuth(self, username, password) :   
179        """Checks if we could authenticate an username with a password."""
180        if not hasPAM :   
181            raise PyKotaToolError, _("You MUST install PyPAM for this functionnality to work !")
182        else :   
183            retcode = False
184            self.password = password
185            self.regainPriv()
186            auth = PAM.pam()
187            auth.start("passwd")
188            auth.set_item(PAM.PAM_USER, username)
189            auth.set_item(PAM.PAM_CONV, self.convPAM)
190            try :
191                auth.authenticate()
192                auth.acct_mgmt()
193            except PAM.error, resp :
194                self.printInfo(_("Authentication error for user %s : %s") % (username, resp), "warn")
195            except :
196                self.printInfo(_("Internal error : can't authenticate user %s") % username, "error")
197            else :
198                self.logdebug(_("Password correct for user %s") % username)
199                retcode = True
200            self.dropPriv()   
201            return retcode
202           
203    def alarmHandler(self, signum, frame) :       
204        """Alarm handler."""
205        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)
206       
207    def main(self, arguments, options) :
208        """Notifies or asks questions to end users through PyKotIcon."""
209        try :
210            (self.destination, self.port) = options["destination"].split(":")
211            self.port = int(self.port)
212        except ValueError :
213            self.destination = options["destination"]
214            self.port = 7654
215           
216        try :
217            denyafter = int(options["denyafter"])
218            if denyafter < 1 :
219                raise ValueError
220        except (ValueError, TypeError) :       
221            denyafter = 1
222           
223        try :   
224            self.timeout = int(options["timeout"])
225            if self.timeout < 0 :
226                raise ValueError
227        except (ValueError, TypeError) :
228            self.timeout = 0
229           
230        if self.timeout :
231            signal.signal(signal.SIGALRM, self.alarmHandler)
232            signal.alarm(self.timeout)
233           
234        try :   
235            server = xmlrpclib.ServerProxy("http://%s:%s" % (self.destination, self.port))
236            if options["ask"] :
237                try :
238                    denyafter = int(options["denyafter"])
239                    if denyafter < 1 :
240                        raise ValueError
241                except (ValueError, TypeError) :   
242                    denyafter = 1
243                labels = []
244                varnames = []
245                varvalues = {}
246                for arg in arguments :
247                    try :
248                        (label, varname, varvalue) = arg.split(":", 2)
249                    except ValueError :   
250                        raise PyKotaCommandLineError, "argument '%s' is invalid !" % arg
251                    labels.append(self.sanitizeMessage(label))
252                    varname = varname.lower()
253                    varnames.append(varname)
254                    varvalues[varname] = self.sanitizeMessage(varvalue)
255                   
256                passnumber = 1   
257                authok = None
258                while (authok != "AUTH=YES") and (passnumber <= denyafter) :
259                    result = server.askDatas(labels, varnames, varvalues)   
260                    if not options["checkauth"] :
261                        break
262                    if result["isValid"] :
263                        if ("username" in varnames) and ("password" in varnames) :
264                            if self.checkAuth(self.UTF8ToUserCharset(result["username"].data[:]), 
265                                              self.UTF8ToUserCharset(result["password"].data[:])) :
266                                authok = "AUTH=YES"
267                            else :   
268                                authok = "AUTH=NO"
269                        else :       
270                            authok = "AUTH=IMPOSSIBLE"       
271                    passnumber += 1       
272                               
273                if options["checkauth"] and options["denyafter"] \
274                   and (passnumber > denyafter) \
275                   and (authok != "AUTH=YES") :
276                    print "DENY"
277                if result["isValid"] :
278                    for varname in varnames :
279                        if (varname != "password") \
280                           and ((varname != "username") or (authok in (None, "AUTH=YES"))) :
281                            print "%s=%s" % (varname.upper(), self.UTF8ToUserCharset(result[varname].data[:]))
282                    if authok is not None :       
283                        print authok       
284            elif options["confirm"] :
285                print server.showDialog(self.sanitizeMessage(arguments[0]), True)
286            elif options["notify"] :
287                print server.showDialog(self.sanitizeMessage(arguments[0]), False)
288               
289            if options["quit"] :   
290                server.quitApplication()
291        except (socket.error, socket.gaierror), msg :
292            print options["noremote"]
293            try :
294                errnum = msg.args[0]
295            except (AttributeError, IndexError) :
296                pass
297            else :   
298                if errnum == errno.ECONNREFUSED :
299                    raise PyKotaToolError, "%s : %s" % (str(msg), (_("Are you sure that PyKotIcon is running and accepting incoming connections on %s:%s ?") % (self.destination, self.port)))
300            raise PyKotaToolError, "%s : %s" % (_("Connection error"), str(msg))
301        except TimeoutError, msg :   
302            self.printInfo(msg, "warn")
303            print "CANCEL"      # Timeout occured : job is cancelled.
304           
305        if self.timeout :   
306            signal.alarm(0)   
307       
308if __name__ == "__main__" :
309    retcode = 0
310    try :
311        defaults = { \
312                     "timeout" : 0,
313                     "noremote" : "CANCEL",
314                   }
315        short_options = "vhd:acnqCD:t:N:"
316        long_options = ["help", "version", "destination=", "denyafter=", \
317                        "timeout=", "ask", "checkauth", "confirm", "notify", \
318                        "quit", "noremote=" ]
319       
320        # Initializes the command line tool
321        notifier = PyKotaNotify(doc=__doc__)
322        notifier.deferredInit()
323       
324        # parse and checks the command line
325        (options, args) = notifier.parseCommandline(sys.argv[1:], short_options, long_options)
326       
327        # sets long options
328        options["help"] = options["h"] or options["help"]
329        options["version"] = options["v"] or options["version"]
330        options["destination"] = options["d"] or options["destination"]
331        options["ask"] = options["a"] or options["ask"]
332        options["confirm"] = options["c"] or options["confirm"]
333        options["notify"] = options["n"] or options["notify"]
334        options["quit"] = options["q"] or options["quit"]
335        options["checkauth"] = options["C"] or options["checkauth"]
336        options["denyafter"] = options["D"] or options["denyafter"]
337        options["timeout"] = options["t"] or options["timeout"] or defaults["timeout"]
338        options["noremote"] = (options["N"] or options["noremote"] or defaults["noremote"]).upper()
339       
340        if options["help"] :
341            notifier.display_usage_and_quit()
342        elif options["version"] :
343            notifier.display_version_and_quit()
344        elif (options["ask"] and (options["confirm"] or options["notify"])) \
345             or (options["confirm"] and (options["ask"] or options["notify"])) \
346             or ((options["checkauth"] or options["denyafter"]) and not options["ask"]) \
347             or (options["notify"] and (options["ask"] or options["confirm"])) :
348            raise PyKotaCommandLineError, _("incompatible options, see help.")
349        elif (not options["destination"]) \
350             or not (options["quit"] or options["ask"] or options["confirm"] or options["notify"]) :
351            raise PyKotaCommandLineError, _("some options are mandatory, see help.")
352        elif options["noremote"] not in ("CANCEL", "CONTINUE") :
353            raise PyKotaCommandLineError, _("incorrect value for the --noremote command line switch, see help.")
354        elif (not args) and (not options["quit"]) :
355            raise PyKotaCommandLineError, _("some options require arguments, see help.")
356        else :
357            retcode = notifier.main(args, options)
358    except KeyboardInterrupt :       
359        sys.stderr.write("\nInterrupted with Ctrl+C !\n")
360        retcode = -3
361    except PyKotaCommandLineError, msg :   
362        sys.stderr.write("%s : %s\n" % (sys.argv[0], msg))
363        print "CANCEL"  # Forces the cancellation of the print job if a command line switch is incorrect
364        retcode = -2
365    except SystemExit :       
366        pass
367    except :
368        try :
369            notifier.crashed("%s failed" % sys.argv[0])
370        except :   
371            crashed("%s failed" % sys.argv[0])
372        retcode = -1
373       
374    sys.exit(retcode)   
Note: See TracBrowser for help on using the browser.