|  Web-Based 
              Printer Management
 Brett Lymn
              Some years ago, pundits predicted that there would soon be the 
              paperless office -- an office where all communications and reporting 
              would be performed electronically without consuming paper. At the 
              time of this writing, I see little evidence of this happening, but 
              I have seen the opposite -- more printers with more capabilities 
              are being added to the office. The burgeoning printer population 
              can be problematic for administrators when users want help removing 
              print jobs from the printer queue.
              On a UNIX system, users can kill their own print jobs if they 
              have the correct tools and know-how. Unfortunately for the administrators, 
              users sometimes can't kill the jobs without help (e.g., a user 
              may be locked into an application and have no way of removing the 
              print jobs). The printer problem becomes amplified when you have 
              back-end services running on multiple machines talking to various 
              printers, or a sys admin who may not be able to immediately kill 
              a print job.
              Background
              One approach to the problem is to use something such as sudo 
              (a program that can allow specific users to run specific programs 
              as root) and give select users the ability to remove jobs from the 
              print queues. This requires that the users with this access have 
              the technical skills to find the right machine, find the offending 
              job on the print queue, and kill it. Unfortunately, even when this 
              task is scripted, most people view the process as somewhat cumbersome. 
              Additionally, if you have many back-end database servers, you will 
              need to grant the people managing the print queues access to the 
              machines and also set up sudo to allow them to kill the print 
              jobs. The task of maintaining these privileged users and the associated 
              sudo setup becomes even more cumbersome as the number of 
              back-end servers grows.
              Most people are familiar and comfortable with a Web interface, 
              and it makes sense that being able to manage printer queues from 
              a Web interface would simplify printer management. The straightforward 
              approach is to wrap some CGI scripts around the standard lp 
              commands and leave it at that. However, this approach has some problems. 
              One approach is for the Web server hosting the printer management 
              scripts to know about all the printers on all the back-end database 
              servers so that the lp tools will be able to manage them. 
              This, in itself, is a burden because the admin must remember to 
              add the printer not only to the back-end database server, but also 
              to the WWW server. Another problem with this approach is that if 
              you want your Web interface to be able to kill print jobs, you must 
              have a setuid root script or run the Web server as the user 
              to which the back-end databases submit their print jobs. Both of 
              these approaches have major security problems (especially running 
              setuid root scripts from a Web server).
              Solaris lpd
              With these points in mind, I looked at how I could simplify printer 
              management. I knew from prior experience that the line printer daemon 
              (lpd) could be used to query a remote host about the status of a 
              printer. As stated before, the normal tools require you to have 
              a printer queue set up on the machine but reading RFC 1179 indicated 
              that you could send a specially crafted packet to the remote host's 
              lpd port to inquire about the status of a queue on a printer. This 
              meant that I could get around the requirement of having the printers 
              defined locally by simply talking directly to the printer port on 
              the remote host using the lpd protocol. This lead to another discovery 
              about Solaris.
              On Solaris, the standard port for the lpd to listen on 
              is defined as port 515. On a UNIX system, because this port is below 
              1024, it is considered a privileged port and can only be opened 
              by root. This really only affects the lpd itself; you can 
              connect to the port as a normal user. The quirk here is that some 
              implementations of lpd actually check the port the connection 
              is coming from and reject it if the source port is not the printer 
              port (i.e., port 515). I think this was originally a method of preventing 
              a normal user from connecting to the lpd and manipulating 
              the queue because only root could open the printer port and therefore 
              could be trusted. However, this scheme relied on everyone obeying 
              the rule that unprivileged users could not open tcp ports 
              below 1024. (This is not the case these days, which makes the source 
              port checking a bit pointless.)
              The Solaris lpd implementation does not check the source 
              port of the lpd connection and, hence, will allow a normal 
              user to manipulate a print queue via the lpd protocol. In 
              my case, this was an advantage because it meant that I did not need 
              to have any setuid root scripts to manipulate the print queues 
              on my Web server. This could be a nightmare for others because a 
              user with sufficient skills would be able to modify the print queue 
              remotely. Note that you cannot just telnet to the printer 
              port on the remote host and manipulate the queues due to the way 
              the lpd reads the data from the network connection.
              Writing the Script
              Given that I had now satisfied my criteria, (not requiring the 
              printers local on the Web server and being able to manipulate printer 
              queues without root), it was time to start coding. I already had 
              a Web server running Apache with the mod_perl extension, 
              so instead of doing a normal CGI script, I was able to write one 
              that worked with mod_perl. (See http://www.apache.org/ 
              for information on both the Apache Web server and the mod_perl 
              extension.) I wanted a script that took the machine and printer 
              names as arguments and would then query the remote machine for the 
              queue of the given printer. If there were jobs in the queue, then 
              these jobs would be presented in a scrolling list. The user could 
              select the jobs from the list and press a button to remove the jobs.
              Some classes are imported to provide the functionality we need. 
              The IO::Socket class is used for the network connection to the lpd 
              on the remote host. The other classes import the necessary parts 
              to deal with HTML and extracting parameters from the arguments passed 
              by the Web server. For the moment, skip over the get_status 
              and cancel_job functions and look at the mainline code. The 
              first important bit is:
              
             
my $r = Apache->request;
which creates a new Apache request object holding all the information 
            the server needs to handle requests. When the script has the request, 
            it sends out a standard text HTTP header using:  
             
$r->content_type('text/html');
$r->send_http_header;
The browser will now have been sent the header and be ready for the 
            rest of the page. The script then grabs the arguments that were given 
            to it, which is done by importing the names like this: 
             
CGI::import_names('PARAMS');
This imports the variables into the PARAMS name-space, which 
            is a security measure to ensure that command-line arguments cannot 
            be used to overwrite internal script variables. Using the PARAMS 
            name-space, we grab copies of the remote machine name ($print_host) 
            and the remote printer name ($printer). Once this is done, 
            the script outputs the start of the page and starts an HTML form. 
            The script then stashes a copy of the remote host name and the printer 
            name in hidden fields in the page by doing this: 
             
$r->print(hidden(-name => 'hidden_print_host', -default => "$print_host"));
$r->print(hidden(-name => 'hidden_printer', -default => "$printer"));
Apache normally forks multiple copies of itself, because you can't 
            be sure that you are talking to the same Apache process on a UNIX 
            machine when the user hits a form button. This means that keeping 
            state between the generation of the form and the post of the form 
            data cannot be done in the script itself, hence the hidden fields 
            on the form page. Hidden fields are not a good idea in a security 
            critical situation but do the job nicely in this case.  The script then checks whether it has received any values using 
              one of the query submission buttons; more on those later. The script 
              has two buttons to look for -- a delete button and a refresh 
              button. If either of these buttons has been pressed, the script 
              retrieves the value of the remote host name and the printer name 
              from the hidden fields where the script initially placed them. The 
              script just retrieved those parameters from the arguments a few 
              lines ago, but remember that if a button has been pressed, the script 
              is processing a form submission (not generating the original form). 
              Thus, the script cannot trust the original arguments, because they 
              may be for an entirely different machine and printer. When we know 
              the printer and host that the script are dealing with, the script 
              prints a nice heading at the top of the page using:
              
             
$r->print(h1("Printer Management For Printer $printer On $print_host"));
The script then checks whether the refresh button was pressed or whether 
            the delete button was not pressed; the latter implies this is the 
            first time the form has been generated, which happens when the user 
            clicks on the link to load the form page. In this case, the script 
            calls the get_status function, which simply opens a TCP/IP 
            connection to the remote host and sends a message to the remote lpd 
            to return a short queue status for the desired printer. The get_status 
            function reads the reply from the remote lpd and puts the results 
            into an array and returns it to the caller. In the mainline code, 
            the script checks the first array entry. If it is empty, there were 
            no jobs in the queue. Rather than printing a blank scrolling list, 
            the script simply prints a message telling the user there were no 
            jobs in the selected print queue. If there were jobs in the queue, 
            then the script generates a scrolling list using: 
             
$r->print(br, scrolling_list(-name => 'job_list', \
          -values => [@jobs], -size => 10, -multiple => 'true'));
The br is simply a line break and is used to position the scrolling 
            list; the actual list is done by the scrolling_list method. 
            We give the scrolling list a name so the script can retrieve the selections 
            later. "values" is the array of jobs that the script 
            fetched using get_status. The list-box has a size of ten lines, 
            and multiple list selections are allowed. If there are jobs, then 
            the script needs a delete button to delete them, which is done with:  
             
$r->print(p,p, submit(-name=>'Delete', -value => 'Delete'));
This inserts a couple of paragraph breaks to space the delete button 
            away from the scrolling list and creates a button labeled "Delete" 
            and a value of "Delete". This is the value passed in the 
            form submission if the button is hit. This is how the variable $button 
            gets the value "Delete" earlier in the script. After the 
            delete button is done, the script skips to the end and puts in a refresh 
            button in a similar manner. It then closes off the form and the HTML 
            page. The form is now rendered by the client browser, and the script 
            waits for the user to press a button. If the user presses the refresh 
            button, the script will go through the process just described, which 
            results in the presented queue entries being updated. If the user 
            hits the delete button then another code path is followed. In the 
            case of a delete being processed, the script first retrieves the selections 
            made by the user by getting the job_list parameter (the name 
            given to the scrolling list) and puts them into an array. The script 
            then processes the list of jobs and parses out the job identifier 
            from the queue entries that were selected. If no jobs were selected 
            for deletion, then the script simply complains and does nothing else. 
            If some jobs were selected for deletion, then the array of identifiers 
            is passed to the cancel_job function, which opens a TCP/IP 
            connection to the remote host and sends the cancel command for the 
            given job identifiers on the given printer queue.  One thing to note here is in the packet sent to the lpd 
              by the script lies about the sender's identity. The script 
              claims to be root, otherwise the lpd will refuse to kill 
              any print jobs that are not owned by the same identity passed in 
              the command packet. Root is allowed to kill any job. The lpd 
              accepts that the identity of the user running the script is root 
              because the lpd protocol assumes that it is talking to a 
              well-behaved client who never lies.
              The cancel_jobs function checks the reply from the cancel 
              request and if there is any reply, then the cancel did not work 
              and the function returns a false status. This is used back in the 
              mainline code to give the user confirmation that the cancel happened 
              or that it failed for some reason. Once the script has processed 
              the deletions, the script will output the code for a refresh button 
              so the user can refresh the view of the queue, and possibly delete 
              more jobs.
              Now that the script is done, we need to put it onto the Web server 
              so other people can access it. Configuring Apache to run a mod_perl 
              script is beyond the scope of this article. (Check out the Apache 
              Web site for detailed instructions: http://perl.apache.org.) 
              I placed the script into the appropriate directory and told Apache 
              that it was a mod_perl script and should be executed with 
              the CGI emulation. Once the script was installed, I set up another 
              script that automatically generated a Web page with links for all 
              the printers on the remote machines to the printer queue management 
              script like this:
              
             
<A HREF=http://webserver/perl/printer_queue.pl?print_host=server1 \
  &printer=headoffice>Manage the Head Office printer queue</A>
I then pointed our help-desk people and a few others to the Web page 
            and went on to the next task knowing that I would no longer get phone 
            calls requesting emergency print job deletions.  Note that it is necessary to create access to a lpd remote 
              printer on the remote machines that are going to have their printers 
              managed for this scheme to work. The lpd service on the remote 
              machine is not set up unless you have done this. I normally just 
              use the admintool to create access to an lpd printer; this 
              only needs to be done once on each remote host.
              Conclusion
              The script I have shown here can create a security risk because 
              it is used in a restricted environment by trustworthy users. Access 
              to the script should be restricted by using an access password to 
              prevent accidental print job deletion. The use of hidden fields 
              in the form can also create a security problem. In this case, there 
              is no real danger but a user could view the hidden fields simply 
              by viewing the HTML source and could possibly change the values 
              in the hidden fields before the form is submitted.
              Brett Lymn works for a global company helping to manage a bunch 
              of UNIX machines. He spends entirely too much time fiddling with 
              computers but cannot help himself.
           |