root / pykota / trunk / bin / pknotify @ 3441

Revision 3441, 15.3 kB (checked in by jerome, 16 years ago)

Intermediate commit. Doesn't work yet. Clearly marked as such when launched.

  • 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 run
39from pykota.commandline import PyKotaOptionParser, \
40                               checkandset_positiveint
41from pykota.errors import PyKotaToolError, \
42                          PyKotaCommandLineError, \
43                          PyKotaTimeoutError
44from pykota.tool import Tool
45
46class PyKotaNotify(Tool) :
47    """A class for pknotify."""
48    def UTF8ToUserCharset(self, text) :
49        """Converts from UTF-8 to user's charset."""
50        if text is None :
51            return None
52        else :
53            return text.decode("UTF-8", "replace").encode(self.charset, "replace")
54
55    def userCharsetToUTF8(self, text) :
56        """Converts from user's charset to UTF-8."""
57        if text is None :
58            return None
59        else :
60            return text.decode(self.charset, "replace").encode("UTF-8", "replace")
61
62    def sanitizeMessage(self, msg) :
63        """Replaces \\n and returns a messagee in xmlrpclib Binary format."""
64        return xmlrpclib.Binary(self.userCharsetToUTF8(msg.replace("\\n", "\n")))
65
66    def convPAM(self, auth, queries=[], userdata=None) :
67        """Prepares PAM datas."""
68        response = []
69        for (query, qtype) in queries :
70            if qtype == PAM.PAM_PROMPT_ECHO_OFF :
71                response.append((self.password, 0))
72            elif qtype in (PAM.PAM_PROMPT_ECHO_ON, PAM.PAM_ERROR_MSG, PAM.PAM_TEXT_INFO) :
73                self.printInfo("Unexpected PAM query : %s (%s)" % (query, qtype), "warn")
74                response.append(('', 0))
75            else:
76                return None
77        return response
78
79    def checkAuth(self, username, password) :
80        """Checks if we could authenticate an username with a password."""
81        if not hasPAM :
82            raise PyKotaToolError, _("You MUST install PyPAM for this functionnality to work !")
83        else :
84            retcode = False
85            self.password = password
86            auth = PAM.pam()
87            auth.start("passwd")
88            auth.set_item(PAM.PAM_USER, username)
89            auth.set_item(PAM.PAM_CONV, self.convPAM)
90            try :
91                auth.authenticate()
92                auth.acct_mgmt()
93            except PAM.error, resp :
94                self.printInfo(_("Authentication error for user %s : %s") % (username, resp), "warn")
95            except :
96                self.printInfo(_("Internal error : can't authenticate user %s") % username, "error")
97            else :
98                self.logdebug(_("Password correct for user %s") % username)
99                retcode = True
100            return retcode
101
102    def alarmHandler(self, signum, frame) :
103        """Alarm handler."""
104        raise PyKotaTimeoutError, _("The end user at %s:%i didn't answer within %i seconds. The print job will be cancelled.") % (self.destination, self.port, self.timeout)
105
106    def main(self, arguments, options) :
107        """Notifies or asks questions to end users through PyKotIcon."""
108        try :
109            (self.destination, self.port) = options["destination"].split(":")
110            self.port = int(self.port)
111        except ValueError :
112            self.destination = options["destination"]
113            self.port = 7654
114
115        try :
116            denyafter = int(options["denyafter"])
117            if denyafter < 1 :
118                raise ValueError
119        except (ValueError, TypeError) :
120            denyafter = 1
121
122        try :
123            self.timeout = int(options["timeout"])
124            if self.timeout < 0 :
125                raise ValueError
126        except (ValueError, TypeError) :
127            self.timeout = 0
128
129        if self.timeout :
130            signal.signal(signal.SIGALRM, self.alarmHandler)
131            signal.alarm(self.timeout)
132
133        try :
134            try :
135                server = xmlrpclib.ServerProxy("http://%s:%s" % (self.destination, self.port))
136                if options["ask"] :
137                    try :
138                        denyafter = int(options["denyafter"])
139                        if denyafter < 1 :
140                            raise ValueError
141                    except (ValueError, TypeError) :
142                        denyafter = 1
143                    labels = []
144                    varnames = []
145                    varvalues = {}
146                    for arg in arguments :
147                        try :
148                            (label, varname, varvalue) = arg.split(":", 2)
149                        except ValueError :
150                            raise PyKotaCommandLineError, "argument '%s' is invalid !" % arg
151                        labels.append(self.sanitizeMessage(label))
152                        varname = varname.lower()
153                        varnames.append(varname)
154                        varvalues[varname] = self.sanitizeMessage(varvalue)
155
156                    passnumber = 1
157                    authok = None
158                    while (authok != "AUTH=YES") and (passnumber <= denyafter) :
159                        result = server.askDatas(labels, varnames, varvalues)
160                        if not options["checkauth"] :
161                            break
162                        if result["isValid"] :
163                            if ("username" in varnames) and ("password" in varnames) :
164                                if self.checkAuth(self.UTF8ToUserCharset(result["username"].data[:]),
165                                                  self.UTF8ToUserCharset(result["password"].data[:])) :
166                                    authok = "AUTH=YES"
167                                else :
168                                    authok = "AUTH=NO"
169                            else :
170                                authok = "AUTH=IMPOSSIBLE"
171                        passnumber += 1
172
173                    if options["checkauth"] and options["denyafter"] \
174                       and (passnumber > denyafter) \
175                       and (authok != "AUTH=YES") :
176                        print "DENY"
177                    if result["isValid"] :
178                        for varname in varnames :
179                            if (varname != "password") \
180                               and ((varname != "username") or (authok in (None, "AUTH=YES"))) :
181                                print "%s=%s" % (varname.upper(), self.UTF8ToUserCharset(result[varname].data[:]))
182                        if authok is not None :
183                            print authok
184                elif options["confirm"] :
185                    print server.showDialog(self.sanitizeMessage(arguments[0]), True)
186                elif options["notify"] :
187                    print server.showDialog(self.sanitizeMessage(arguments[0]), False)
188
189                if options["quit"] :
190                    server.quitApplication()
191            except (xmlrpclib.ProtocolError, socket.error, socket.gaierror), msg :
192                print options["noremote"]
193                #try :
194                #    errnum = msg.args[0]
195                #except (AttributeError, IndexError) :
196                #    pass
197                #else :
198                #    if errnum == errno.ECONNREFUSED :
199                #        raise PyKotaToolError, "%s : %s" % (str(msg), (_("Are you sure that PyKotIcon is running and accepting incoming connections on %s:%s ?") % (self.destination, self.port)))
200                self.printInfo("%s : %s" % (_("Connection error"), str(msg)), "warn")
201            except PyKotaTimeoutError, msg :
202                self.printInfo(msg, "warn")
203                print "CANCEL"      # Timeout occured : job is cancelled.
204        finally :
205            if self.timeout :
206                signal.alarm(0)
207
208if __name__ == "__main__" :
209    sys.stderr.write("The pknotify command line tool is currently broken in this development tree. Please use a stable release instead.\n")
210    sys.exit(-1)
211    parser = PyKotaOptionParser(description=_("Notifies end users who have launched the PyKotIcon client side graphical desktop helper."),
212                                usage="pknotify [options] [arguments]")
213    parser.add_option("-a", "--ask",
214                            action="store_const",
215                            const="ask",
216                            dest="action",
217                            help=_("Ask something to the remote user, then print the result."))
218    parser.add_option("-c", "--confirm",
219                            action="store_const",
220                            const="confirm",
221                            dest="action",
222                            help=_("Ask the remote user to confirm or abort, then print the result."))
223    parser.add_option("-C", "--checkauth",
224                            dest="checkauth",
225                            help=_("When --ask is used, if both an username and password are asked to the end user, pknotify tries to authenticate these username and password through PAM. If this is successful, both 'AUTH=YES' and 'USERNAME=theusername' are printed. If unsuccessful, 'AUTH=NO' is printed. Finally, if one field is missing, 'AUTH=IMPOSSIBLE' is printed."))
226    parser.add_option("-D", "--denyafter",
227                            type="int",
228                            action="callback",
229                            callback=checkandset_positiveint,
230                            default=1,
231                            dest="denyafter",
232                            help=_("When --checkauth is used, this option tell pknotify to loop up to this value times or until the password is correct for the returned username. If authentication was impossible with the username and password received from the remote user, 'DENY' is printed, rejecting the print job. The default value is %default, meaning pknotify asks a single time."))
233    parser.add_option("-d", "--destination",
234                            dest="destination",
235                            help=_("Indicate the mandatory remote hostname or IP address and optional TCP port where PyKotIcon is listening for incoming connections from pknotify. If not specified, the port defaults to 7654."))
236    parser.add_option("-n", "--notify",
237                            action="store_const",
238                            const="notify",
239                            dest="action",
240                            help=_("Send an informational message to the remote user."))
241    parser.add_option("-N", "--noremote",
242                            dest="noremote",
243                            default="CANCEL",
244                            help=_("Tell pknotify what to print if it can't connect to a remote PyKotIcon application. The default value is 'CANCEL', which tells PyKota to cancel the print job. The only other supported value is 'CONTINUE', which tells PyKota to continue the processing of the current job."))
245    parser.add_option("-q", "--quit",
246                            dest="quit",
247                            help=_("Ask the remote PyKotIcon application to quit. When combined with other command line options, any other action is performed first."))
248    parser.add_option("-t", "--timeout",
249                            type="int",
250                            action="callback",
251                            callback=checkandset_positiveint,
252                            default=0,
253                            dest="timeout",
254                            help=_("Ensure that pknotify won't wait more than timeout seconds for an answer from the remote user. This avoids end users stalling a print queue because they don't answer in time. The default value is %default, making pknotify wait indefinitely."))
255    run(parser, PyKotaNotify)
256
257"""
258  arguments :
259
260    -a | --ask : Several arguments are accepted, of the form
261                 "label:varname:defaultvalue". The result will
262                 be printed to stdout in the following format :
263                 VAR1NAME=VAR1VALUE
264                 VAR2NAME=VAR2VALUE
265                 ...
266                 If the dialog was cancelled, nothing will be
267                 printed. If one of the varname is 'password'
268                 then this field is asked as a password (you won't
269                 see what you type in), and is NOT printed. Although
270                 it is not printed, it will be used to check if
271                 authentication is valid if you specify --checkauth.
272
273    -c | --confirm : A single argument is expected, representing the
274                     message to display. If the dialog is confirmed
275                     then pknotify will print OK, else CANCEL.
276
277    -n | --notify : A single argument is expected, representing the
278                    message to display. In this case pknotify will
279                    always print OK.
280
281examples :
282
283  pknotify -d client:7654 --noremote CONTINUE --confirm "This job costs 10 credits"
284
285  Would display the cost of the print job and asks for confirmation.
286  If the end user doesn't have PyKotIcon running and accepting connections
287  from the print server, PyKota will consider that the end user accepted
288  to print this job.
289
290  pknotify --destination $PYKOTAJOBORIGINATINGHOSTNAME:7654 \\
291           --checkauth --ask "Your name:username:" "Your password:password:"
292
293  Asks an username and password, and checks if they are valid.
294  NB : The PYKOTAJOBORIGINATINGHOSTNAME environment variable is
295  only set if you launch pknotify from cupspykota through a directive
296  in ~pykota/pykota.conf
297
298  The TCP port you'll use must be reachable on the client from the
299  print server.
300"""
301
302"""
303        defaults = { \
304                     "timeout" : 0,
305                     "noremote" : "CANCEL",
306                   }
307        short_options = "vhd:acnqCD:t:N:"
308        long_options = ["help", "version", "destination=", "denyafter=", \
309                        "timeout=", "ask", "checkauth", "confirm", "notify", \
310                        "quit", "noremote=" ]
311
312        elif (options["ask"] and (options["confirm"] or options["notify"])) \
313             or (options["confirm"] and (options["ask"] or options["notify"])) \
314             or ((options["checkauth"] or options["denyafter"]) and not options["ask"]) \
315             or (options["notify"] and (options["ask"] or options["confirm"])) :
316            raise PyKotaCommandLineError, _("incompatible options, see help.")
317        elif (not options["destination"]) \
318             or not (options["quit"] or options["ask"] or options["confirm"] or options["notify"]) :
319            raise PyKotaCommandLineError, _("some options are mandatory, see help.")
320        elif options["noremote"] not in ("CANCEL", "CONTINUE") :
321            raise PyKotaCommandLineError, _("incorrect value for the --noremote command line switch, see help.")
322        elif (not args) and (not options["quit"]) :
323            raise PyKotaCommandLineError, _("some options require arguments, see help.")
324"""
Note: See TracBrowser for help on using the browser.