root / pykota / branches / 1.26_fixes / bin / pknotify @ 3403

Revision 3403, 17.1 kB (checked in by jerome, 16 years ago)

Backported another fix from the development tree.

  • 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, 2007 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            try :   
236                server = xmlrpclib.ServerProxy("http://%s:%s" % (self.destination, self.port))
237                if options["ask"] :
238                    try :
239                        denyafter = int(options["denyafter"])
240                        if denyafter < 1 :
241                            raise ValueError
242                    except (ValueError, TypeError) :   
243                        denyafter = 1
244                    labels = []
245                    varnames = []
246                    varvalues = {}
247                    for arg in arguments :
248                        try :
249                            (label, varname, varvalue) = arg.split(":", 2)
250                        except ValueError :   
251                            raise PyKotaCommandLineError, "argument '%s' is invalid !" % arg
252                        labels.append(self.sanitizeMessage(label))
253                        varname = varname.lower()
254                        varnames.append(varname)
255                        varvalues[varname] = self.sanitizeMessage(varvalue)
256                       
257                    passnumber = 1   
258                    authok = None
259                    while (authok != "AUTH=YES") and (passnumber <= denyafter) :
260                        result = server.askDatas(labels, varnames, varvalues)   
261                        if not options["checkauth"] :
262                            break
263                        if result["isValid"] :
264                            if ("username" in varnames) and ("password" in varnames) :
265                                if self.checkAuth(self.UTF8ToUserCharset(result["username"].data[:]), 
266                                                  self.UTF8ToUserCharset(result["password"].data[:])) :
267                                    authok = "AUTH=YES"
268                                else :   
269                                    authok = "AUTH=NO"
270                            else :       
271                                authok = "AUTH=IMPOSSIBLE"       
272                        passnumber += 1       
273                                   
274                    if options["checkauth"] and options["denyafter"] \
275                       and (passnumber > denyafter) \
276                       and (authok != "AUTH=YES") :
277                        print "DENY"
278                    if result["isValid"] :
279                        for varname in varnames :
280                            if (varname != "password") \
281                               and ((varname != "username") or (authok in (None, "AUTH=YES"))) :
282                                print "%s=%s" % (varname.upper(), self.UTF8ToUserCharset(result[varname].data[:]))
283                        if authok is not None :       
284                            print authok       
285                elif options["confirm"] :
286                    print server.showDialog(self.sanitizeMessage(arguments[0]), True)
287                elif options["notify"] :
288                    print server.showDialog(self.sanitizeMessage(arguments[0]), False)
289                   
290                if options["quit"] :   
291                    server.quitApplication()
292            except (xmlrpclib.ProtocolError, socket.error, socket.gaierror), msg :
293                print options["noremote"]
294                #try :
295                #    errnum = msg.args[0]
296                #except (AttributeError, IndexError) :
297                #    pass
298                #else :   
299                #    if errnum == errno.ECONNREFUSED :
300                #        raise PyKotaToolError, "%s : %s" % (str(msg), (_("Are you sure that PyKotIcon is running and accepting incoming connections on %s:%s ?") % (self.destination, self.port)))
301                self.printInfo("%s : %s" % (_("Connection error"), str(msg)), "warn")
302            except TimeoutError, msg :   
303                self.printInfo(msg, "warn")
304                print "CANCEL"      # Timeout occured : job is cancelled.
305        finally :   
306            if self.timeout :   
307                signal.alarm(0)   
308       
309if __name__ == "__main__" :
310    retcode = 0
311    try :
312        defaults = { \
313                     "timeout" : 0,
314                     "noremote" : "CANCEL",
315                   }
316        short_options = "vhd:acnqCD:t:N:"
317        long_options = ["help", "version", "destination=", "denyafter=", \
318                        "timeout=", "ask", "checkauth", "confirm", "notify", \
319                        "quit", "noremote=" ]
320       
321        # Initializes the command line tool
322        notifier = PyKotaNotify(doc=__doc__)
323        notifier.deferredInit()
324       
325        # parse and checks the command line
326        (options, args) = notifier.parseCommandline(sys.argv[1:], short_options, long_options)
327       
328        # sets long options
329        options["help"] = options["h"] or options["help"]
330        options["version"] = options["v"] or options["version"]
331        options["destination"] = options["d"] or options["destination"]
332        options["ask"] = options["a"] or options["ask"]
333        options["confirm"] = options["c"] or options["confirm"]
334        options["notify"] = options["n"] or options["notify"]
335        options["quit"] = options["q"] or options["quit"]
336        options["checkauth"] = options["C"] or options["checkauth"]
337        options["denyafter"] = options["D"] or options["denyafter"]
338        options["timeout"] = options["t"] or options["timeout"] or defaults["timeout"]
339        options["noremote"] = (options["N"] or options["noremote"] or defaults["noremote"]).upper()
340       
341        if options["help"] :
342            notifier.display_usage_and_quit()
343        elif options["version"] :
344            notifier.display_version_and_quit()
345        elif (options["ask"] and (options["confirm"] or options["notify"])) \
346             or (options["confirm"] and (options["ask"] or options["notify"])) \
347             or ((options["checkauth"] or options["denyafter"]) and not options["ask"]) \
348             or (options["notify"] and (options["ask"] or options["confirm"])) :
349            raise PyKotaCommandLineError, _("incompatible options, see help.")
350        elif (not options["destination"]) \
351             or not (options["quit"] or options["ask"] or options["confirm"] or options["notify"]) :
352            raise PyKotaCommandLineError, _("some options are mandatory, see help.")
353        elif options["noremote"] not in ("CANCEL", "CONTINUE") :
354            raise PyKotaCommandLineError, _("incorrect value for the --noremote command line switch, see help.")
355        elif (not args) and (not options["quit"]) :
356            raise PyKotaCommandLineError, _("some options require arguments, see help.")
357        else :
358            retcode = notifier.main(args, options)
359    except KeyboardInterrupt :       
360        sys.stderr.write("\nInterrupted with Ctrl+C !\n")
361        retcode = -3
362    except PyKotaCommandLineError, msg :   
363        sys.stderr.write("%s : %s\n" % (sys.argv[0], msg))
364        print "CANCEL"  # Forces the cancellation of the print job if a command line switch is incorrect
365        retcode = -2
366    except SystemExit :       
367        pass
368    except :
369        try :
370            notifier.crashed("%s failed" % sys.argv[0])
371        except :   
372            crashed("%s failed" % sys.argv[0])
373        retcode = -1
374       
375    sys.exit(retcode)   
Note: See TracBrowser for help on using the browser.