root / pykota / trunk / bin / pknotify @ 3288

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

Moved all exceptions definitions to a dedicated module.

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