root / pykota / trunk / bin / pknotify @ 2806

Revision 2806, 15.3 kB (checked in by jerome, 18 years ago)

Now outputs DENY when --denyafter is used and the number of tries is reached.

  • 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 options["checkauth"] and options["denyafter"] \
265                   and (passnumber > denyafter) \
266                   and (authok != "AUTH=YES") :
267                    print "DENY"
268                if result["isValid"] :
269                    for varname in varnames :
270                        if (varname != "password") \
271                           and ((varname != "username") or (authok in (None, "AUTH=YES"))) :
272                            print "%s=%s" % (varname.upper(), self.UTF8ToUserCharset(result[varname].data[:]))
273                    if authok is not None :       
274                        print authok       
275            elif options["confirm"] :
276                print server.showDialog(self.sanitizeMessage(arguments[0]), True)
277            elif options["notify"] :
278                print server.showDialog(self.sanitizeMessage(arguments[0]), False)
279               
280            if options["quit"] :   
281                server.quitApplication()
282        except (socket.error, socket.gaierror), msg :
283            raise PyKotaCommandLineError, "%s : %s" % (_("Connection error"), str(msg))
284        except TimeoutError, msg :   
285            self.printInfo(msg, "warn")
286            print "CANCEL"      # Timeout occured : job is cancelled.
287           
288        if self.timeout :   
289            signal.alarm(0)   
290       
291if __name__ == "__main__" :
292    retcode = 0
293    try :
294        defaults = { \
295                     "timeout" : 0,
296                   }
297        short_options = "vhd:acnqCD:t:"
298        long_options = ["help", "version", "destination=", "denyafter=", \
299                        "timeout=", "ask", "checkauth", "confirm", "notify", \
300                        "quit" ]
301       
302        # Initializes the command line tool
303        notifier = PyKotaNotify(doc=__doc__)
304        notifier.deferredInit()
305       
306        # parse and checks the command line
307        (options, args) = notifier.parseCommandline(sys.argv[1:], short_options, long_options)
308       
309        # sets long options
310        options["help"] = options["h"] or options["help"]
311        options["version"] = options["v"] or options["version"]
312        options["destination"] = options["d"] or options["destination"]
313        options["ask"] = options["a"] or options["ask"]
314        options["confirm"] = options["c"] or options["confirm"]
315        options["notify"] = options["n"] or options["notify"]
316        options["quit"] = options["q"] or options["quit"]
317        options["checkauth"] = options["C"] or options["checkauth"]
318        options["denyafter"] = options["D"] or options["denyafter"]
319        options["timeout"] = options["t"] or options["timeout"] or defaults["timeout"]
320       
321        if options["help"] :
322            notifier.display_usage_and_quit()
323        elif options["version"] :
324            notifier.display_version_and_quit()
325        elif (options["ask"] and (options["confirm"] or options["notify"])) \
326             or (options["confirm"] and (options["ask"] or options["notify"])) \
327             or ((options["checkauth"] or options["denyafter"]) and not options["ask"]) \
328             or (options["notify"] and (options["ask"] or options["confirm"])) :
329            raise PyKotaCommandLineError, _("incompatible options, see help.")
330        elif (not options["destination"]) \
331             or not (options["quit"] or options["ask"] or options["confirm"] or options["notify"]) :
332            raise PyKotaCommandLineError, _("some options are mandatory, see help.")
333        elif (not args) and (not options["quit"]) :
334            raise PyKotaCommandLineError, _("some options require arguments, see help.")
335        else :
336            retcode = notifier.main(args, options)
337    except KeyboardInterrupt :       
338        sys.stderr.write("\nInterrupted with Ctrl+C !\n")
339        retcode = -3
340    except PyKotaCommandLineError, msg :   
341        sys.stderr.write("%s : %s\n" % (sys.argv[0], msg))
342        retcode = -2
343    except SystemExit :       
344        pass
345    except :
346        try :
347            notifier.crashed("%s failed" % sys.argv[0])
348        except :   
349            crashed("%s failed" % sys.argv[0])
350        retcode = -1
351       
352    sys.exit(retcode)   
Note: See TracBrowser for help on using the browser.