root / pykota / trunk / bin / pknotify @ 3411

Revision 3411, 17.5 kB (checked in by jerome, 16 years ago)

Minor change to please emacs...

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