root / pykota / trunk / bin / pknotify @ 2805

Revision 2805, 15.1 kB (checked in by jerome, 18 years ago)

Implemented the denyafter command line option.

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