root / pykota / trunk / bin / pknotify @ 3276

Revision 3276, 16.9 kB (checked in by jerome, 16 years ago)

Doesn't drop and regain priviledges anymore : no added security since we could regain them (we needed to regain them for PAM and some end user scripts). This is also more consistent.
Removed SGTERM handling stuff in cupspykota : now only SIGINT can be used.
Now outputs an error message when printing (but doesn't fail) if CUPS is
not v1.3.4 or higher : we need 1.3.4 or higher because it fixes some
problematic charset handling bugs (by only accepting ascii and utf-8,
but this is a different story...)
Now ensures only the supported exit codes are returned by cupspykota :
we used to exit -1 in some cases (see man backend for details).

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