root / pykota / trunk / bin / pknotify @ 2804

Revision 2804, 14.6 kB (checked in by jerome, 19 years ago)

Ensures that all texts sent by pknotify to a remote pykoticon server
are UTF-8 encoded.

  • 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                labels = []
229                varnames = []
230                varvalues = {}
231                for arg in arguments :
232                    try :
233                        (label, varname, varvalue) = arg.split(":", 2)
234                    except ValueError :   
235                        raise PyKotaCommandLineError, "argument '%s' is invalid !" % arg
236                    labels.append(self.sanitizeMessage(label))
237                    varname = varname.lower()
238                    varnames.append(varname)
239                    varvalues[varname] = self.sanitizeMessage(varvalue)
240                result = server.askDatas(labels, varnames, varvalues)   
241                if result["isValid"] :
242                    authok = None
243                    if options["checkauth"] :
244                        if ("username" in varnames) and ("password" in varnames) :
245                            if self.checkAuth(self.UTF8ToUserCharset(result["username"].data[:]), 
246                                              self.UTF8ToUserCharset(result["password"].data[:])) :
247                                authok = "AUTH=YES"
248                            else :   
249                                authok = "AUTH=NO"
250                        else :       
251                            authok = "AUTH=IMPOSSIBLE"       
252                    for varname in varnames :
253                        if (varname != "password") \
254                           and ((varname != "username") or (authok in (None, "AUTH=YES"))) :
255                            print "%s=%s" % (varname.upper(), self.UTF8ToUserCharset(result[varname].data[:]))
256                    if authok is not None :       
257                        print authok       
258            elif options["confirm"] :
259                print server.showDialog(self.sanitizeMessage(arguments[0]), True)
260            elif options["notify"] :
261                print server.showDialog(self.sanitizeMessage(arguments[0]), False)
262               
263            if options["quit"] :   
264                server.quitApplication()
265        except (socket.error, socket.gaierror), msg :
266            raise PyKotaCommandLineError, "%s : %s" % (_("Connection error"), str(msg))
267        except TimeoutError, msg :   
268            self.printInfo(msg, "warn")
269            print "CANCEL"      # Timeout occured : job is cancelled.
270           
271        if self.timeout :   
272            signal.alarm(0)   
273       
274if __name__ == "__main__" :
275    retcode = 0
276    try :
277        defaults = { \
278                     "denyafter" : 0,
279                     "timeout" : 0,
280                   }
281        short_options = "vhd:acnqCD:t:"
282        long_options = ["help", "version", "destination=", "denyafter=", \
283                        "timeout=", "ask", "checkauth", "confirm", "notify", \
284                        "quit" ]
285       
286        # Initializes the command line tool
287        notifier = PyKotaNotify(doc=__doc__)
288        notifier.deferredInit()
289       
290        # parse and checks the command line
291        (options, args) = notifier.parseCommandline(sys.argv[1:], short_options, long_options)
292       
293        # sets long options
294        options["help"] = options["h"] or options["help"]
295        options["version"] = options["v"] or options["version"]
296        options["destination"] = options["d"] or options["destination"]
297        options["ask"] = options["a"] or options["ask"]
298        options["confirm"] = options["c"] or options["confirm"]
299        options["notify"] = options["n"] or options["notify"]
300        options["quit"] = options["q"] or options["quit"]
301        options["checkauth"] = options["C"] or options["checkauth"]
302        options["denyafter"] = options["D"] or options["denyafter"] or defaults["denyafter"]
303        options["timeout"] = options["t"] or options["timeout"] or defaults["timeout"]
304       
305        if options["help"] :
306            notifier.display_usage_and_quit()
307        elif options["version"] :
308            notifier.display_version_and_quit()
309        elif (options["ask"] and (options["confirm"] or options["notify"])) \
310             or (options["confirm"] and (options["ask"] or options["notify"])) \
311             or ((options["checkauth"] or options["denyafter"]) and not options["ask"]) \
312             or (options["notify"] and (options["ask"] or options["confirm"])) :
313            raise PyKotaCommandLineError, _("incompatible options, see help.")
314        elif (not options["destination"]) \
315             or not (options["quit"] or options["ask"] or options["confirm"] or options["notify"]) :
316            raise PyKotaCommandLineError, _("some options are mandatory, see help.")
317        elif (not args) and (not options["quit"]) :
318            raise PyKotaCommandLineError, _("some options require arguments, see help.")
319        else :
320            retcode = notifier.main(args, options)
321    except KeyboardInterrupt :       
322        sys.stderr.write("\nInterrupted with Ctrl+C !\n")
323        retcode = -3
324    except PyKotaCommandLineError, msg :   
325        sys.stderr.write("%s : %s\n" % (sys.argv[0], msg))
326        retcode = -2
327    except SystemExit :       
328        pass
329    except :
330        try :
331            notifier.crashed("%s failed" % sys.argv[0])
332        except :   
333            crashed("%s failed" % sys.argv[0])
334        retcode = -1
335       
336    sys.exit(retcode)   
Note: See TracBrowser for help on using the browser.