root / pykota / trunk / bin / pknotify @ 3262

Revision 3262, 17.0 kB (checked in by jerome, 16 years ago)

Fixed a problem where even with --noremote pknotify raised an exception.

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