636 | | class PyKotaFilterOrBackend(PyKotaTool) : |
637 | | """Class for the PyKota filter or backend.""" |
638 | | def __init__(self) : |
639 | | """Initialize local datas from current environment.""" |
640 | | # We begin with ignoring signals, we may de-ignore them later on. |
641 | | self.gotSigTerm = 0 |
642 | | signal.signal(signal.SIGTERM, signal.SIG_IGN) |
643 | | # signal.signal(signal.SIGCHLD, signal.SIG_IGN) |
644 | | signal.signal(signal.SIGPIPE, signal.SIG_IGN) |
645 | | |
646 | | PyKotaTool.__init__(self) |
647 | | (self.printingsystem, \ |
648 | | self.printerhostname, \ |
649 | | self.printername, \ |
650 | | self.username, \ |
651 | | self.jobid, \ |
652 | | self.inputfile, \ |
653 | | self.copies, \ |
654 | | self.title, \ |
655 | | self.options, \ |
656 | | self.originalbackend) = self.extractInfoFromCupsOrLprng() |
657 | | |
658 | | def deferredInit(self) : |
659 | | """Deferred initialization.""" |
660 | | PyKotaTool.deferredInit(self) |
661 | | |
662 | | arguments = " ".join(['"%s"' % arg for arg in sys.argv]) |
663 | | self.logdebug(_("Printing system %s, args=%s") % (str(self.printingsystem), arguments)) |
664 | | |
665 | | self.username = self.username or pwd.getpwuid(os.geteuid())[0] # use CUPS' user when printing test page from CUPS web interface, otherwise username is empty |
666 | | |
667 | | if self.printingsystem == "CUPS" : |
668 | | self.extractDatasFromCups() |
669 | | |
670 | | (newusername, newbillingcode, newaction) = self.overwriteJobTicket() |
671 | | if newusername : |
672 | | self.printInfo(_("Job ticket overwritten : new username = [%s]") % newusername) |
673 | | self.username = newusername |
674 | | if newbillingcode : |
675 | | self.printInfo(_("Job ticket overwritten : new billing code = [%s]") % newbillingcode) |
676 | | self.overwrittenBillingCode = newbillingcode |
677 | | else : |
678 | | self.overwrittenBillingCode = None |
679 | | if newaction : |
680 | | self.printInfo(_("Job ticket overwritten : job will be denied (but a bit later).")) |
681 | | self.mustDeny = 1 |
682 | | else : |
683 | | self.mustDeny = 0 |
684 | | |
685 | | # do we want to strip out the Samba/Winbind domain name ? |
686 | | separator = self.config.getWinbindSeparator() |
687 | | if separator is not None : |
688 | | self.username = self.username.split(separator)[-1] |
689 | | |
690 | | # do we want to lowercase usernames ? |
691 | | if self.config.getUserNameToLower() : |
692 | | self.username = self.username.lower() |
693 | | |
694 | | # do we want to strip some prefix off of titles ? |
695 | | stripprefix = self.config.getStripTitle(self.printername) |
696 | | if stripprefix : |
697 | | if fnmatch.fnmatch(self.title[:len(stripprefix)], stripprefix) : |
698 | | self.logdebug("Prefix [%s] removed from job's title [%s]." % (stripprefix, self.title)) |
699 | | self.title = self.title[len(stripprefix):] |
700 | | |
701 | | self.preserveinputfile = self.inputfile |
702 | | try : |
703 | | self.accounter = accounter.openAccounter(self) |
704 | | except (config.PyKotaConfigError, accounter.PyKotaAccounterError), msg : |
705 | | self.crashed(msg) |
706 | | raise |
707 | | self.exportJobInfo() |
708 | | self.jobdatastream = self.openJobDataStream() |
709 | | self.checksum = self.computeChecksum() |
710 | | self.softwareJobSize = self.precomputeJobSize() |
711 | | os.environ["PYKOTAPRECOMPUTEDJOBSIZE"] = str(self.softwareJobSize) |
712 | | os.environ["PYKOTAJOBSIZEBYTES"] = str(self.jobSizeBytes) |
713 | | self.logdebug("Job size is %s bytes on %s pages." % (self.jobSizeBytes, self.softwareJobSize)) |
714 | | self.logdebug("Capturing SIGTERM events.") |
715 | | signal.signal(signal.SIGTERM, self.sigterm_handler) |
716 | | |
717 | | def overwriteJobTicket(self) : |
718 | | """Should we overwrite the job's ticket (username and billingcode) ?""" |
719 | | jobticketcommand = self.config.getOverwriteJobTicket(self.printername) |
720 | | if jobticketcommand is not None : |
721 | | username = billingcode = action = None |
722 | | self.logdebug("Launching subprocess [%s] to overwrite the job's ticket." % jobticketcommand) |
723 | | inputfile = os.popen(jobticketcommand, "r") |
724 | | for line in inputfile.xreadlines() : |
725 | | line = line.strip() |
726 | | if line == "DENY" : |
727 | | self.logdebug("Seen DENY command.") |
728 | | action = "DENY" |
729 | | elif line.startswith("USERNAME=") : |
730 | | username = line.split("=", 1)[1].strip() |
731 | | self.logdebug("Seen new username [%s]" % username) |
732 | | action = None |
733 | | elif line.startswith("BILLINGCODE=") : |
734 | | billingcode = line.split("=", 1)[1].strip() |
735 | | self.logdebug("Seen new billing code [%s]" % billingcode) |
736 | | action = None |
737 | | inputfile.close() |
738 | | return (username, billingcode, action) |
739 | | else : |
740 | | return (None, None, None) |
741 | | |
742 | | def sendBackChannelData(self, message, level="info") : |
743 | | """Sends an informational message to CUPS via back channel stream (stderr).""" |
744 | | sys.stderr.write("%s: PyKota (PID %s) : %s\n" % (level.upper(), os.getpid(), message.strip())) |
745 | | sys.stderr.flush() |
746 | | |
747 | | def computeChecksum(self) : |
748 | | """Computes the MD5 checksum of the job's datas, to be able to detect and forbid duplicate jobs.""" |
749 | | self.logdebug("Computing MD5 checksum for job %s" % self.jobid) |
750 | | MEGABYTE = 1024*1024 |
751 | | checksum = md5.new() |
752 | | while 1 : |
753 | | data = self.jobdatastream.read(MEGABYTE) |
754 | | if not data : |
755 | | break |
756 | | checksum.update(data) |
757 | | self.jobdatastream.seek(0) |
758 | | digest = checksum.hexdigest() |
759 | | self.logdebug("MD5 checksum for job %s is %s" % (self.jobid, digest)) |
760 | | os.environ["PYKOTAMD5SUM"] = digest |
761 | | return digest |
762 | | |
763 | | def openJobDataStream(self) : |
764 | | """Opens the file which contains the job's datas.""" |
765 | | if self.preserveinputfile is None : |
766 | | # Job comes from sys.stdin, but this is not |
767 | | # seekable and complexifies our task, so create |
768 | | # a temporary file and use it instead |
769 | | self.logdebug("Duplicating data stream from stdin to temporary file") |
770 | | dummy = 0 |
771 | | MEGABYTE = 1024*1024 |
772 | | self.jobSizeBytes = 0 |
773 | | infile = tempfile.TemporaryFile() |
774 | | while 1 : |
775 | | data = sys.stdin.read(MEGABYTE) |
776 | | if not data : |
777 | | break |
778 | | self.jobSizeBytes += len(data) |
779 | | if not (dummy % 10) : |
780 | | self.logdebug("%s bytes read..." % self.jobSizeBytes) |
781 | | dummy += 1 |
782 | | infile.write(data) |
783 | | self.logdebug("%s bytes read total." % self.jobSizeBytes) |
784 | | infile.flush() |
785 | | infile.seek(0) |
786 | | return infile |
787 | | else : |
788 | | # real file, just open it |
789 | | self.regainPriv() |
790 | | self.logdebug("Opening data stream %s" % self.preserveinputfile) |
791 | | self.jobSizeBytes = os.stat(self.preserveinputfile)[6] |
792 | | infile = open(self.preserveinputfile, "rb") |
793 | | self.dropPriv() |
794 | | return infile |
795 | | |
796 | | def closeJobDataStream(self) : |
797 | | """Closes the file which contains the job's datas.""" |
798 | | self.logdebug("Closing data stream.") |
799 | | try : |
800 | | self.jobdatastream.close() |
801 | | except : |
802 | | pass |
803 | | |
804 | | def precomputeJobSize(self) : |
805 | | """Computes the job size with a software method.""" |
806 | | self.logdebug("Precomputing job's size with generic PDL analyzer...") |
807 | | self.jobdatastream.seek(0) |
808 | | try : |
809 | | parser = analyzer.PDLAnalyzer(self.jobdatastream) |
810 | | jobsize = parser.getJobSize() |
811 | | except pdlparser.PDLParserError, msg : |
812 | | # Here we just log the failure, but |
813 | | # we finally ignore it and return 0 since this |
814 | | # computation is just an indication of what the |
815 | | # job's size MAY be. |
816 | | self.printInfo(_("Unable to precompute the job's size with the generic PDL analyzer : %s") % msg, "warn") |
817 | | return 0 |
818 | | else : |
819 | | if ((self.printingsystem == "CUPS") \ |
820 | | and (self.preserveinputfile is not None)) \ |
821 | | or (self.printingsystem != "CUPS") : |
822 | | return jobsize * self.copies |
823 | | else : |
824 | | return jobsize |
825 | | |
826 | | def sigterm_handler(self, signum, frame) : |
827 | | """Sets an attribute whenever SIGTERM is received.""" |
828 | | self.gotSigTerm = 1 |
829 | | os.environ["PYKOTASTATUS"] = "CANCELLED" |
830 | | self.printInfo(_("SIGTERM received, job %s cancelled.") % self.jobid) |
831 | | |
832 | | def exportJobInfo(self) : |
833 | | """Exports job information to the environment.""" |
834 | | os.environ["PYKOTAUSERNAME"] = str(self.username) |
835 | | os.environ["PYKOTAPRINTERNAME"] = str(self.printername) |
836 | | os.environ["PYKOTAJOBID"] = str(self.jobid) |
837 | | os.environ["PYKOTATITLE"] = self.title or "" |
838 | | os.environ["PYKOTAFILENAME"] = self.preserveinputfile or "" |
839 | | os.environ["PYKOTACOPIES"] = str(self.copies) |
840 | | os.environ["PYKOTAOPTIONS"] = self.options or "" |
841 | | os.environ["PYKOTAPRINTERHOSTNAME"] = self.printerhostname or "localhost" |
842 | | |
843 | | def exportUserInfo(self, userpquota) : |
844 | | """Exports user information to the environment.""" |
845 | | os.environ["PYKOTAOVERCHARGE"] = str(userpquota.User.OverCharge) |
846 | | os.environ["PYKOTALIMITBY"] = str(userpquota.User.LimitBy) |
847 | | os.environ["PYKOTABALANCE"] = str(userpquota.User.AccountBalance or 0.0) |
848 | | os.environ["PYKOTALIFETIMEPAID"] = str(userpquota.User.LifeTimePaid or 0.0) |
849 | | os.environ["PYKOTAPAGECOUNTER"] = str(userpquota.PageCounter or 0) |
850 | | os.environ["PYKOTALIFEPAGECOUNTER"] = str(userpquota.LifePageCounter or 0) |
851 | | os.environ["PYKOTASOFTLIMIT"] = str(userpquota.SoftLimit) |
852 | | os.environ["PYKOTAHARDLIMIT"] = str(userpquota.HardLimit) |
853 | | os.environ["PYKOTADATELIMIT"] = str(userpquota.DateLimit) |
854 | | os.environ["PYKOTAWARNCOUNT"] = str(userpquota.WarnCount) |
855 | | |
856 | | # not really an user information, but anyway |
857 | | # exports the list of printers groups the current |
858 | | # printer is a member of |
859 | | os.environ["PYKOTAPGROUPS"] = ",".join([p.Name for p in self.storage.getParentPrinters(userpquota.Printer)]) |
860 | | |
861 | | def prehook(self, userpquota) : |
862 | | """Allows plugging of an external hook before the job gets printed.""" |
863 | | prehook = self.config.getPreHook(userpquota.Printer.Name) |
864 | | if prehook : |
865 | | self.logdebug("Executing pre-hook [%s]" % prehook) |
866 | | os.system(prehook) |
867 | | |
868 | | def posthook(self, userpquota) : |
869 | | """Allows plugging of an external hook after the job gets printed and/or denied.""" |
870 | | posthook = self.config.getPostHook(userpquota.Printer.Name) |
871 | | if posthook : |
872 | | self.logdebug("Executing post-hook [%s]" % posthook) |
873 | | os.system(posthook) |
874 | | |
875 | | def printInfo(self, message, level="info") : |
876 | | """Sends a message to standard error.""" |
877 | | self.logger.log_message("%s" % message, level) |
878 | | |
879 | | def printMoreInfo(self, user, printer, message, level="info") : |
880 | | """Prefixes the information printed with 'user@printer(jobid) =>'.""" |
881 | | self.printInfo("%s@%s(%s) => %s" % (getattr(user, "Name", None), getattr(printer, "Name", None), self.jobid, message), level) |
882 | | |
883 | | def extractInfoFromCupsOrLprng(self) : |
884 | | """Returns a tuple (printingsystem, printerhostname, printername, username, jobid, filename, title, options, backend). |
885 | | |
886 | | Returns (None, None, None, None, None, None, None, None, None, None) if no printing system is recognized. |
887 | | """ |
888 | | # Try to detect CUPS |
889 | | if os.environ.has_key("CUPS_SERVERROOT") and os.path.isdir(os.environ.get("CUPS_SERVERROOT", "")) : |
890 | | if len(sys.argv) == 7 : |
891 | | inputfile = sys.argv[6] |
892 | | else : |
893 | | inputfile = None |
894 | | |
895 | | # check that the DEVICE_URI environment variable's value is |
896 | | # prefixed with "cupspykota:" otherwise don't touch it. |
897 | | # If this is the case, we have to remove the prefix from |
898 | | # the environment before launching the real backend in cupspykota |
899 | | device_uri = os.environ.get("DEVICE_URI", "") |
900 | | if device_uri.startswith("cupspykota:") : |
901 | | fulldevice_uri = device_uri[:] |
902 | | device_uri = fulldevice_uri[len("cupspykota:"):] |
903 | | if device_uri.startswith("//") : # lpd (at least) |
904 | | device_uri = device_uri[2:] |
905 | | os.environ["DEVICE_URI"] = device_uri # TODO : side effect ! |
906 | | # TODO : check this for more complex urls than ipp://myprinter.dot.com:631/printers/lp |
907 | | try : |
908 | | (backend, destination) = device_uri.split(":", 1) |
909 | | except ValueError : |
910 | | raise PyKotaToolError, "Invalid DEVICE_URI : %s\n" % device_uri |
911 | | while destination.startswith("/") : |
912 | | destination = destination[1:] |
913 | | checkauth = destination.split("@", 1) |
914 | | if len(checkauth) == 2 : |
915 | | destination = checkauth[1] |
916 | | printerhostname = destination.split("/")[0].split(":")[0] |
917 | | return ("CUPS", \ |
918 | | printerhostname, \ |
919 | | os.environ.get("PRINTER"), \ |
920 | | sys.argv[2].strip(), \ |
921 | | sys.argv[1].strip(), \ |
922 | | inputfile, \ |
923 | | int(sys.argv[4].strip()), \ |
924 | | sys.argv[3], \ |
925 | | sys.argv[5], \ |
926 | | backend) |
927 | | else : |
928 | | # Try to detect LPRng |
929 | | # TODO : try to extract filename, options if available |
930 | | jseen = Jseen = Pseen = nseen = rseen = Kseen = None |
931 | | for arg in sys.argv : |
932 | | if arg.startswith("-j") : |
933 | | jseen = arg[2:].strip() |
934 | | elif arg.startswith("-n") : |
935 | | nseen = arg[2:].strip() |
936 | | elif arg.startswith("-P") : |
937 | | Pseen = arg[2:].strip() |
938 | | elif arg.startswith("-r") : |
939 | | rseen = arg[2:].strip() |
940 | | elif arg.startswith("-J") : |
941 | | Jseen = arg[2:].strip() |
942 | | elif arg.startswith("-K") or arg.startswith("-#") : |
943 | | Kseen = int(arg[2:].strip()) |
944 | | if Kseen is None : |
945 | | Kseen = 1 # we assume the user wants at least one copy... |
946 | | if (rseen is None) and jseen and Pseen and nseen : |
947 | | lparg = [arg for arg in "".join(os.environ.get("PRINTCAP_ENTRY", "").split()).split(":") if arg.startswith("rm=") or arg.startswith("lp=")] |
948 | | try : |
949 | | rseen = lparg[0].split("=")[-1].split("@")[-1].split("%")[0] |
950 | | except : |
951 | | # Not found |
952 | | self.printInfo(_("Printer hostname undefined, set to 'localhost'"), "warn") |
953 | | rseen = "localhost" |
954 | | |
955 | | spooldir = os.environ.get("SPOOL_DIR", ".") |
956 | | df_name = os.environ.get("DATAFILES") |
957 | | if not df_name : |
958 | | try : |
959 | | df_name = [line[10:] for line in os.environ.get("HF", "").split() if line.startswith("datafiles=")][0] |
960 | | except IndexError : |
961 | | try : |
962 | | df_name = [line[8:] for line in os.environ.get("HF", "").split() if line.startswith("df_name=")][0] |
963 | | except IndexError : |
964 | | try : |
965 | | cftransfername = [line[15:] for line in os.environ.get("HF", "").split() if line.startswith("cftransfername=")][0] |
966 | | except IndexError : |
967 | | try : |
968 | | df_name = [line[1:] for line in os.environ.get("CONTROL", "").split() if line.startswith("fdf") or line.startswith("Udf")][0] |
969 | | except IndexError : |
970 | | raise PyKotaToolError, "Unable to find the file which holds the job's datas. Please file a bug report for PyKota." |
971 | | else : |
972 | | inputfile = os.path.join(spooldir, df_name) # no need to strip() |
973 | | else : |
974 | | inputfile = os.path.join(spooldir, "d" + cftransfername[1:]) # no need to strip() |
975 | | else : |
976 | | inputfile = os.path.join(spooldir, df_name) # no need to strip() |
977 | | else : |
978 | | inputfile = os.path.join(spooldir, df_name) # no need to strip() |
979 | | else : |
980 | | inputfile = os.path.join(spooldir, df_name.strip()) |
981 | | |
982 | | if jseen and Pseen and nseen and rseen : |
983 | | options = os.environ.get("HF", "") or os.environ.get("CONTROL", "") |
984 | | return ("LPRNG", rseen, Pseen, nseen, jseen, inputfile, Kseen, Jseen, options, None) |
985 | | self.printInfo(_("Printing system unknown, args=%s") % " ".join(sys.argv), "warn") |
986 | | return (None, None, None, None, None, None, None, None, None, None) # Unknown printing system |
987 | | |
988 | | def getPrinterUserAndUserPQuota(self) : |
989 | | """Returns a tuple (policy, printer, user, and user print quota) on this printer. |
990 | | |
991 | | "OK" is returned in the policy if both printer, user and user print quota |
992 | | exist in the Quota Storage. |
993 | | Otherwise, the policy as defined for this printer in pykota.conf is returned. |
994 | | |
995 | | If policy was set to "EXTERNAL" and one of printer, user, or user print quota |
996 | | doesn't exist in the Quota Storage, then an external command is launched, as |
997 | | defined in the external policy for this printer in pykota.conf |
998 | | This external command can do anything, like automatically adding printers |
999 | | or users, for example, and finally extracting printer, user and user print |
1000 | | quota from the Quota Storage is tried a second time. |
1001 | | |
1002 | | "EXTERNALERROR" is returned in case policy was "EXTERNAL" and an error status |
1003 | | was returned by the external command. |
1004 | | """ |
1005 | | for passnumber in range(1, 3) : |
1006 | | printer = self.storage.getPrinter(self.printername) |
1007 | | user = self.storage.getUser(self.username) |
1008 | | userpquota = self.storage.getUserPQuota(user, printer) |
1009 | | if printer.Exists and user.Exists and userpquota.Exists : |
1010 | | policy = "OK" |
1011 | | break |
1012 | | (policy, args) = self.config.getPrinterPolicy(self.printername) |
1013 | | if policy == "EXTERNAL" : |
1014 | | commandline = self.formatCommandLine(args, user, printer) |
1015 | | if not printer.Exists : |
1016 | | self.printInfo(_("Printer %s not registered in the PyKota system, applying external policy (%s) for printer %s") % (self.printername, commandline, self.printername)) |
1017 | | if not user.Exists : |
1018 | | self.printInfo(_("User %s not registered in the PyKota system, applying external policy (%s) for printer %s") % (self.username, commandline, self.printername)) |
1019 | | if not userpquota.Exists : |
1020 | | self.printInfo(_("User %s doesn't have quota on printer %s in the PyKota system, applying external policy (%s) for printer %s") % (self.username, self.printername, commandline, self.printername)) |
1021 | | if os.system(commandline) : |
1022 | | self.printInfo(_("External policy %s for printer %s produced an error. Job rejected. Please check PyKota's configuration files.") % (commandline, self.printername), "error") |
1023 | | policy = "EXTERNALERROR" |
1024 | | break |
1025 | | else : |
1026 | | if not printer.Exists : |
1027 | | self.printInfo(_("Printer %s not registered in the PyKota system, applying default policy (%s)") % (self.printername, policy)) |
1028 | | if not user.Exists : |
1029 | | self.printInfo(_("User %s not registered in the PyKota system, applying default policy (%s) for printer %s") % (self.username, policy, self.printername)) |
1030 | | if not userpquota.Exists : |
1031 | | self.printInfo(_("User %s doesn't have quota on printer %s in the PyKota system, applying default policy (%s)") % (self.username, self.printername, policy)) |
1032 | | break |
1033 | | if policy == "EXTERNAL" : |
1034 | | if not printer.Exists : |
1035 | | self.printInfo(_("Printer %s still not registered in the PyKota system, job will be rejected") % self.printername) |
1036 | | if not user.Exists : |
1037 | | self.printInfo(_("User %s still not registered in the PyKota system, job will be rejected on printer %s") % (self.username, self.printername)) |
1038 | | if not userpquota.Exists : |
1039 | | self.printInfo(_("User %s still doesn't have quota on printer %s in the PyKota system, job will be rejected") % (self.username, self.printername)) |
1040 | | return (policy, printer, user, userpquota) |
1041 | | |
1042 | | def mainWork(self) : |
1043 | | """Main work is done here.""" |
1044 | | (policy, printer, user, userpquota) = self.getPrinterUserAndUserPQuota() |
1045 | | # TODO : check for last user's quota in case pykota filter is used with querying |
1046 | | if policy == "EXTERNALERROR" : |
1047 | | # Policy was 'EXTERNAL' and the external command returned an error code |
1048 | | return self.removeJob() |
1049 | | elif policy == "EXTERNAL" : |
1050 | | # Policy was 'EXTERNAL' and the external command wasn't able |
1051 | | # to add either the printer, user or user print quota |
1052 | | return self.removeJob() |
1053 | | elif policy == "DENY" : |
1054 | | # Either printer, user or user print quota doesn't exist, |
1055 | | # and the job should be rejected. |
1056 | | return self.removeJob() |
1057 | | else : |
1058 | | if policy not in ("OK", "ALLOW") : |
1059 | | self.printInfo(_("Invalid policy %s for printer %s") % (policy, self.printername)) |
1060 | | return self.removeJob() |
1061 | | else : |
1062 | | return self.doWork(policy, printer, user, userpquota) |