jbreeden@netcom.COM (John Breeden) (06/04/91)
Does anyone know of a piece of code that will allow a Unix print queue to be redirected to a terminal server via telnet (like the peice of code that xyplex supplies with their terminal servers?). I'm trying to provide the same type of print services with a non-xyplex terminal server, hoping that someone has already invented this wheel). Thanx in advance; -- John Robert Breeden, jbreeden@netcom.com, apple!netcom!jbreeden, ATTMAIL:!jbreeden ------------------------------------------------------------------- "The nice thing about standards is that you have so many to choose from. If you don't like any of them, you just wait for next year's model."
poorman@convex.com (Peter W. Poorman) (06/04/91)
In <1991Jun3.183905.12329@netcom.COM> jbreeden@netcom.COM (John Breeden) writes: >Does anyone know of a piece of code that will allow a Unix print queue >to be redirected to a terminal server via telnet [...] Check out "tcpf". Available as C source for UNIX from "ftp.cisco.com". This generally relies upon the capability of CISCO routers to open a raw TCP stream to a terminal port. It ought to work with other brands as well, assuming they have a similar capability. --Pete poorman@convex.com
c_bstratton@HNS.COM (Bob Stratton) (06/04/91)
Date: 3 Jun 91 18:39:05 GMT From: netcomsv!jbreeden@apple.com (John Breeden) Does anyone know of a piece of code that will allow a Unix print queue to be redirected to a terminal server via telnet (like the peice of code that xyplex supplies with their terminal servers?). I'm trying to provide the same type of print services with a non-xyplex terminal server, hoping that someone has already invented this wheel). Look in cisco's anonymous FTP area...I seem to remember some non-system specific hacks to do similar things in there... Bob Stratton | Stratton Systems Design| SMTP: strat@gnu.ai.mit.edu, c_bstratton@hns.com Alexandria, Virginia | PSTN: +1 301 409 2703 "Personally, I think the DNS administrative interface was designed by the IRS." --Mark Beyer
rlstewart@eng.xyplex.com (Bob Stewart) (06/05/91)
Sorry, list. I tried to reply directly to John, but the mailers couldn't sort out his return address. Besides, this is sort of general... You may get close, but you won't quite be able to duplicate what we do as we have a registration and call-back protocol that is unique to us (as far as I know). You might be able to set up something that will connect from the Unix end and send, if the server is available, or you might be able to get it to work with a permanent type connection. The dynamic sharing with queued call back is a bit harder, requiring cooperation from the terminal server. I probably can't help you more than this, as I've pretty well exhausted my store of knowledge on the subject, and we can only go so far in supporting somebody else's terminal server. :-) Bob
rosen@rosen.enet.dec.com ("Eric C. Rosen 13-Jun-1991 0926") (06/13/91)
This is in response to John Breeden's question about Unix software that facilitates printing via telnet to a terminal server port. I've recently written a print filter that does just that, and I am including its source code here for anyone that would like to try it out. I wrote this with the DECserver 300 in mind, but it should work with any terminal server that is capable of accepting incoming connections and mapping them to particular physical ports. It does not use any proprietary or vendor-specific protocols. The filter does assume that the terminal server will initiate at least one Telnet negotiation after connection setup, but this would be easy to change if necessary. The introductory comments below explain how to set up the printcap file so as to invoke this filter properly. I've tested this on Ultrix, on MIPS' RISC/os (running with BSD compatibility), and on a 1986 version of SUN/os. However, I haven't tested it with other vendors' terminal servers. If anyone cares to try this out, please contact me directly with any comments or problems. Eric Rosen /* This program is used to enable the "lpr" command to cause data to be sent to a printer that is attached to a Telnet terminal server port. The program is offered only as a "proof of concept"; its testing has not been extensive, it cannot be claimed to be "production quality software", and its author's Unix network programming skills are somewhat rusty. The procedures for using this program are somewhat different, depending on whether it is to be run on an Ultrix system or on a different kind of Unix system. NON-ULTRIX SYSTEMS On non-Ultrix systems, this program is intended to be called both as an output filter and an input filter from lpd. It should be specified as the argument to both the "of" and the "if" parameter of a printcap entry. The program requires command line arguments which are not passed by lpd. Many Unix systems do not allow command line arguments to be passed explicitly in the "if" and "of" lines of a printcap entry. The work-around is to have the "if" and "of" entries refer to shell scripts. Within the shell scripts, any arguments can be passed. For example, the following printcap entry has been used successfully: telprt:\ :lp=/dev/null:\ :sd=/usr/spool/lpd5:\ :of=/pathname/output_filter_script:\ :if=/pathname/input_filter_script: The shell script "/pathname/output_filter_script" should be something like the following: #! /bin/sh exec /pathname/thisprogram o printer_host printer_port relay_port except that: "/pathname/thisprogram" would be replaced by the actual pathname and file name of the executable of this program, "printer_host" would be replaced by the name or IP address of the DECserver 300 to which the printer is attached (if a name is used, it can be in /etc/hosts or in the Domain Name Service), "printer_port" would be replaced by the TCP Port Number which has been assigned to the Telnet Listener on the DS300 which is associated with the printer (note that this the the TCP Port Number, not the physical port number); this can be a decimal number like 2010, or a "service name" which appears in /etc/services "relay_port" would be replaced by a TCP Port Number which is not otherwise in use on this host system. A GIVEN PRINTCAP ENTRY MUST HAVE A UNIQUE RELAY_PORT NUMBER. The shell script "/pathname/input_filter_script" should be something like the following: #! /bin/sh exec /pathname/thisprogram i this_host relay_port except that: "/pathname/thisprogram" would be replaced by the actual pathname and file name of the executable of this program, "this_host" would be replaced by the name or IP address of the host on which this program is to run (i.e., the host from which we are printing, NOT the host to which the printer is attached) "relay_port" would be replaced with the SAME VALUE that appears for relay_port in output_filter_script. So an actual output_filter_script might be: #! /bin/sh exec /usr/users/rosen/bin/betterfilter o tantn1 2010 3000 and an actual input_filter_script might be: #! /bin/sh exec /usr/users/rosen/bin/betterfilter i dsuser 3000 I've used these scripts to print from the host called "dsuser" to a printer on the DS300 called "tantn1", where Telnet Listner TCP Port Number 2010 on that DS300 has been assigned to that printer. When you install these shell scripts, there are a few things to remember: Make them executable (with "chmod ugo+x" )! Get the pathnames right, both in the shell scripts and in the printcap entry! Don't forget the first command line argument ("i" or "o"). Make sure the same relay_port number is provided in both shell scripts, and that it is not otherwise in use on your host (at least check that it does not appear in /etc/services). Getting the printcap entry exactly right may take some experimenting. Some Unix systems like :lp=/dev/null:, some like :lp=:, and some like it if you just omit the "lp" entry entirely. Once this is all setup, the command "lpr -Ptelprt" will send data to the printer which is attached to the terminal server whose name is "tantn1", and which has been associated with a Telnet Listener at TCP Port 2010 on that terminal server. It will use the directory /usr/spool/lpd5 as the spooling directory. Of course, you will replace the "sd" entry in the printcap file with one that specifies the spooling directory you actually want to use for this printer, and you will make sure that that directory really exists before you try to print anything. This printcap entry can appear on any number of non-Ultrix Unix systems, and those systems will contend for the use of the printer. While any one system is using the printer, the others will not be able to connect to it. In this case, they will retry periodically, using exponential backoff. The parameters: BASE_RETRY_INTERVAL, MAX_RETRY_INTERVAL, and MAX_RETRY_TIMES_TO_PRINTER in the source code govern the retry algorithm. These default to 10 seconds, with infinite retries at a maximum retry interval of one hour. As an alternative, you can set up one system to be the main spooler for the printer, and have other systems send their printjobs to the main spooler. This is entirely a matter of customer preference. If the main spooler is the system whose name is "prowlr", the other systems would have printcap entries like: telprt:\ :lp=:\ :sd=/usr/spool/lpd3:\ :rm=prowlr:\ :rp=telprt: The very same printer can also be accessed, using LAT, from VMS systems. Once any of the Unix systems succeeds in opening a connection to the printer, all files queued on that system will be printed, and then the connection will be closed. ULTRIX SYSTEMS Setting the system up to use this program is a bit simpler if you have an Ultrix V4 system. In this case, the program is only invoked as an output filter; however, several additional entries in the printcap file are needed. It has been used successfully with printcap entries such as the following: telprt:\ :sd=/usr/spool/lpd5:\ :ct=network:\ :uv=psv1.0:\ :of=/pathname/thisprogram x tantn1 2010: With this printcap entry, the command "lpr -Ptelprt" will send data to the printer which is attached to the terminal server whose name is "tantn1", and which has been associated with a Telnet Listener at TCP Port 2010 on that terminal server. It will use the directory /usr/spool/lpd5 as the spooling directory. (We assume in this example that "/pathname/thisprogram" is the executable resulting from compiling this program.) Of course, you will replace the "sd" entry in the printcap file with one that specifies the spooling directory you actually want to use for this printer, and you will make sure that that directory really exists before you try to print anything. Don't forget the "x" as the first argument to the program in the "of" line of the printcap entry, and make sure you get the pathname right. This printcap entry can appear on any number of Ultrix systems, and those systems will contend for the use of the printer. While any one system is using the printer, the others will not be able to connect to it. In this case, they will retry periodically, using exponential backoff. The algorithms and parameters are the same as those described above for non-Ultrix systems. Any number of systems should be able to contend for the use of the printer, whether they are Ultrix systems, other Unix systems, or VMS systems using LAT. PROGRAM OPERATION To describe the way in which this program works, it is necessary to first describe the principles of operation of the Unix Line Printer Daemon (lpd). First, we will describe its operation on non-Ultrix systems. When lpd sees that there are files on its queue for the printer, it forks an output filter. It creates a pipe to the output filter, and binds it to the output filter's standard input. It then creates a banner page for the first file on the queue, and sends the banner page down the pipe to the output filter. It indicates the end of the banner page by sending a two-byte sequence consisting of ascii code 25 followed by ascii code 1. The daemon then waits for the output filter to suspend itself. When the output filter detects the end of the banner page, it must suspend itself by sending itself SIGSTOP (the equivalent of ^Z in the csh). When lpd detects that the output filter has suspended itself, it forks an input filter. It creates a pipe to the input filter, and binds it to the input filter's standard input. It then reads the first file on the queue from the spooling directory, and sends it down the pipe to the input filter. When the file has been completely transmitted, lpd closes the pipe. It then waits for the input filter to exit. The input filter knows that it has received the complete file when it detects that its standard input has been closed. When the input filter has finished processing the file, it must exit. When lpd detects that the input filter has exited, it causes the output filter to resume, by sending it SIGCONT. It then creates a banner page for the next file on the queue, and sends it to the output filter. The above procedures are repeated until the queue is empty. When the queue is empty, lpd closes the pipe to the output filter. This causes the output filter to see that its standard input has been closed, at which point the output filter exits. Note that one output filter process is used for an entire queue's worth of files, but a separate input filter process is used for each individual file. Each filter indicates a normal exit by supplying an exit status of 0. This indicates to lpd that the data it gave to the filter was printed success- fully. If either filter supplies an exit status of 1, lpd will leave the current file at the head of the queue, and try again later to print it. If the filter which exited with a non-zero status was the input filter, lpd will send SIGINT to the output filter. For the purpose of printing via Telnet to a DS300, we have adopted the following design. Only the output filter actually communicates with the printer. That is, only the output filter opens a Telnet connection to the DS300. Since the output filter process exists until the print queue is emptied, the Telnet connection is opened once, and remains open until all the files are queued. The input filter does not communicate directly with the printer. Instead, any data which it receives from lpd is passed via a "back door" to the output filter. The output filter takes this data from the input filter and passes it to the printer via the exissting Telnet connection. The input filter uses a TCP connection to pass the data to the output filter. That is, the input filter makes a TCP connection to its own host, at the specified "relay_port". Actually, at the time that the input filter wants to send the data to the output filter, the output filter is supposed to be suspended. So before the output filter suspends itself, it forks a child process, and it is this child to which the input filter actually sends the data. The child and its parent share the Telnet connection to the DS300 with no problems. When the output filter first opens the Telnet Connection, it waits for the DS300 to initiate Telnet Negotiations. It then completes these negotiations successfully. It then obtains data, one character at a time, from either lpd or from the input filter. It "telnetizes" that character if necessary, and then sends it to the printer. By "Telnetizing" a character, we mean that it doubles characters with ascii code 255, and that it replaces linefeeds with carriage return, line feed pairs, as required by the Telnet protocol. Any Telnet negotiations which the terminal server may initiate at any time will be properly responded to by the output filter. Most of the TCP packets generated will be maximum size, for most efficient use of the network. If the Telnet connection breaks while printing is in progress, the output filter will exist with a status of 1, thereby informing lpd that the printing was not successful. The daemon will then retry the same file later; files will not be lost when the Telnet connection breaks. The output filter tries to keep the Telnet connection open until all the data has been printed. This maximizes the probably of getting the file reprinted automatically if the terminal server or printer fails after the last of the data has been transferred, but before printing is complete. However, with the DECserver 300, a failure while the last 500 bytes of data are being printed may not be recoverable. We do assume here that the attached printer needs no processing of the data. If it does, that processing would need to be done first, and the result piped into this program. It should be possible to do this with a suitable combination of shell scripts and pipelines. On Ultrix systems, with the printcap entry set up as described above for Ultrix, the situation is quite a bit simpler. The Ultrix lpd will invoke this program only as an output filter, and will invoke it independently for each file. Both the banner page and the data for each file gets sent to a single process. This program writes error messages to the syslog, using the "debug" priority. If the symbol DEBUG1 is defined, messages about the inter-process communication are logged. If the symbol DEBUG2 is defined, messages about the number of characters read, written, and internally buffered are written. */ #define _BSD #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netinet/tcp.h> #include <syslog.h> #include <stdio.h> #include <netdb.h> #include <ctype.h> #include <sys/file.h> #include <sys/time.h> #include <signal.h> #include <fcntl.h> #include <errno.h> #include <sys/wait.h> #define DEBUG1 #define FOREVER while(1) /* booleans */ #define FALSE 0 #define TRUE 1 #define DONTFLUSH FALSE /* exit status values */ #define REPRINT 1 #define OKAY 0 #define ENDOFBANNER 2 /* How many seconds we will wait before exiting after we close the Telnet connection to the printer. This period should allow all outstanding data to be printed, unless the printer is currently XOFF'ing the DS300. */ #define DISCONNECTTIMER 10 /* Parameters governing retries, if the input filter has trouble connecting up with the output filter */ #define BASE_RETRY_INTERVAL 10 #define MAX_RETRY_TIMES_TO_PRINTER 0 #define MAX_RETRY_TIMES_TO_OF 10 #define MAX_RETRY_INTERVAL 3600 /* Input and Output Buffer Sizes */ #define INBUFSIZE 1024 #define OUTBUFSIZE (2*INBUFSIZE) /* are we running as an input filter or as an output filter */ #define IF 0 #define OF 1 #define UF 2 #define INPUT_FILTER (filtertype == IF) #define OUTPUT_FILTER (filtertype == OF) #define ULTRIX_FILTER (filtertype == UF) /* ascii codes with special significance to Telnet */ #define CR '\r' #define LF '\n' #define IAC 255 #define WILL 251 #define WONT 252 #define DO 253 #define DONT 254 #define TIMINGMARK 6 #define BINARY 0 /* state values for responding to Telnet Option Negotiations */ #define DATA 0 #define IACSCAN 1 #define OPTION 2 /* state values for lpd "end of banner" detection */ #define CTRLY 1 #define CAN_READ_FROM_LPD (can_read & lpd_mask) #define CAN_READ_FROM_TCP (can_read & tcp_mask) extern int errno; char *signalnames[] = { "", "SIGHUP", "SIGINT", "SIGQUIT", "SIGILL", "SIGTRAP", "SIGIOT", "SIGEMT", "SIGFPE", "SIGKILL", "SIGBUS", "SIGSEGV", "SIGSYS", "SIGPIPE","SIGALRM", "SIGTERM", "SIGURG", "SIGSTOP", "SIGTSTP", "SIGCONT", "SIGCHLD", "SIGTTIN", "SIGTTOU", "SIGIO", "SIGXCPU", "SIGXFSZ", "SIGVTALRM","SIGPROF", "SIGWINCH", "SIGLOST", "SIGUSR1", "SIGUSR2" }; int tcp_connection = 0, /* file descriptor representing socket for making outgoing tcp connection; for output filter, connection is to printer, for input filter, connection is to output filter */ listener, /* file descriptor representing socket for receiving incoming tcp connection (used only by output filter, while waiting for input filter to make connection */ lpd_fd = 0, /* file descriptor for reading characers from lpd; of course, this is the standard input */ flags, /* control flags for socket */ anyaddr = INADDR_ANY, /* for listener, address on which we will receive incoming connections */ addrlen, /* for listener, length of anyaddr */ printer_addr; int lpdstate = DATA, /* lpd "end of banner detected" state variable */ telnetstate = DATA, /* Telnet Option Negotiations state variable */ binarytransmit = FALSE; /* whether the DS300 has put the Telnet connection into binary transmission mode */ int extra_chars; /* keeps track of extra characters inserted by "Telnetizing" the data */ struct sockaddr_in listen_addr, /* the output filter is listening for connections to this address */ incoming_addr, /* the address of the input filter, when it make a TCP connection to the output filter */ outgoing_addr; /* in the output filter, this is the address of the printer; in the input filter, this is the address of the output filter */ int retrycount, /* keeps track of number of tries to open TCP connection; used for exponential backoff */ max_retries, /* maximum number of attempts to open TCP connection, 0 means no maximum */ newfile, /* this is TRUE if we haven't yet sent the printer any characters from the file we are currently trying to print */ printjobnumber, /* the number of files that have been printed */ filtertype; /* are we running as input filter or as output filter? */ unsigned char answer[3]; /* construct answer to Telnet Option Negotiations */ unsigned char from_net[INBUFSIZE], /* buffer for holding data received from net */ from_lpd[INBUFSIZE], /* buffer for holding data received from lpd */ to_tcp[OUTBUFSIZE]; /* buffer for holding data to be sent to printer */ int to_tcp_start = 0, /* start of unsent data in to_tcp */ to_tcp_end = 0, /* offset for placing NEXT character in to_tcp */ tcp_data = 0, /* amount of internally buffered data for TCP */ tcp_maxseg; /* TCP Maximum Segment Size; used to ensure that we send maximum size packets when possible */ int tcp_blocking = FALSE; struct timeval poll; /* used as arg to "select" when we want to poll rather than block */ int lpd_mask, /* file descriptor mask (for select routine) for data from lpd (standard input) */ tcp_mask; /* file description mask (for select routine) for data to/from the tcp connection */ struct servent *sp_printer, /* TCP "Service" (Port Number) for outgoing connections (used by output filter to connect to printer, or by input filter to connect to output filter */ *sp_relay; /* TCP "Service" (Port Number) for incoming connections (used by output filter to accept connection from input filter */ struct hostent *hp; /* Host Address for outgoing connections */ struct protoent *pp; /* Signal Handling Routines */ int interruption(); int disconnection(); int child; /* Used by output filter to hold PID of child process which receives data from input filter */ unsigned char iac = IAC, cr = CR, lf = LF; main(argc, argv) int argc; char *argv[]; { int n_read; /* return value from read call, # of bytes read */ int n_from_lpd; int i; int pid; int pr_chr_result, firstchar, lpd_data; /* Used by output filter while waiting for child process to terminate */ union wait child_status; int *exit_status = (int *)&child_status; /* masks for the select routine */ int can_read; char *host, *service, *relayservice; openlog(argv[0], LOG_PID); #ifdef DEBUG1 syslog(LOG_DEBUG, "Telnet Print Filter Starting Up"); #endif /* These signals cause termination with the REPRINT status, so that lpd will try again. */ signal(SIGHUP, interruption); signal(SIGINT, interruption); signal(SIGQUIT, interruption); signal(SIGTERM, interruption); signal(SIGILL, interruption); signal(SIGTRAP, interruption); signal(SIGIOT, interruption); signal(SIGEMT, interruption); signal(SIGFPE, interruption); signal(SIGBUS, interruption); signal(SIGSEGV, interruption); signal(SIGSYS, interruption); signal(SIGXCPU, interruption); /* Signals to ignore. */ signal(SIGCONT, SIG_IGN); signal(SIGURG, SIG_IGN); signal(SIGALRM, SIG_IGN); signal(SIGVTALRM, SIG_IGN); signal(SIGWINCH, SIG_IGN); signal(SIGPROF, SIG_IGN); /* We get this signal if the TCP connection breaks and we then try to write to it. This causes termination with REPRINT status. We really just ignore this signal (except for debugging purposes). */ signal(SIGPIPE, disconnection); poll.tv_sec = 0; poll.tv_usec = 0; if (argc < 2) printhelp(); switch(argv[1][0]) { case 'i': filtertype = IF; break; case 'o': filtertype = OF; break; case 'x': filtertype = UF; break; default: printhelp(); /* terminates program, never returns */ } switch(filtertype) { case IF: case UF: if (argc < 4) printhelp(); host = argv[2]; service = argv[3]; break; case OF: if (argc < 5) printhelp(); host = argv[2]; service = argv[3]; relayservice = argv[4]; break; } closelog(); openlog(INPUT_FILTER ? "Telnet IF" : (OUTPUT_FILTER ? "Telnet OF" : "Telnet UF"), LOG_PID); #ifdef DEBUG1 syslog(LOG_DEBUG, "Started as %s Filter", INPUT_FILTER ? "Input" : (OUTPUT_FILTER ? "Output" : "Ultrix")); #endif /* Look up host name and service name for outgoing TCP connection, and convert them into a destination IP address and a remote TCP Port Number */ init(host, service); /* If running as Output Filter, must set up TCP Port Number on which to listen for connection from Input Filter */ if (OUTPUT_FILTER) init_relay(relayservice); /* Initialize some globals */ retrycount = 0; newfile = TRUE; extra_chars = 0; printjobnumber = 0; max_retries = INPUT_FILTER ? MAX_RETRY_TIMES_TO_OF : MAX_RETRY_TIMES_TO_PRINTER; lpd_mask = 1 << lpd_fd; firstchar = 0; lpd_data = FALSE; /* Create socket for outgoing TCP connection, set it up with destination address, and open the TCP connection. */ reconnect: make_connection(); /* If running as Output Filter or as Ultrix Filter, wait for peer to initiate Telnet Option Negotiations. We're assuming here that this will happen, which is true in the case of the DS300 and most other systems. This prevents us from sending any data in the case where the peer's TCP has accepted the connection, but its Telnet is going to refuse it (perhaps because the printer is busy). */ if (OUTPUT_FILTER || ULTRIX_FILTER) { can_read = tcp_mask; select(16, &can_read, NULL, NULL, NULL); } /* Now we're ready to enter the main loop. */ FOREVER { /* Output Filter or Ultrix Filter should hang until there is something to read from net; Input Filter shouldn't try to read from net, since it's really talking to the output filter. */ can_read = lpd_mask; if (OUTPUT_FILTER || ULTRIX_FILTER) can_read |= tcp_mask; select(16, &can_read, NULL, NULL, NULL); if (CAN_READ_FROM_TCP) { n_read = read(tcp_connection, from_net, 1024); if (n_read <= 0) /* Select says that there is data to read, but when we try to read it, we get back nothing; this means that the TCP connection has closed unexpectedly. If the printing of the file has no yet begun, keep trying to reconnect. If we are in the middle of printing, terminate, and signal lpd that the file needs to be reprinted. */ { syslog(LOG_DEBUG, "TCP Read Error: %m"); if (INPUT_FILTER || !newfile) terminate(REPRINT); goto reconnect; } /* We have data from the net. We will ignore everything except Telnet Option negotiations. */ for (i = 0; i < n_read; ++i) do_options(from_net[i]); /* Continue in this loop until there is no more data to read from the net. */ continue; } /* If we're here, there is no data to read from net, but there is data to read from lpd. Before we take it, let's see if we can write it to the outgoing TCP connection. We'll hang in a select until either (a) we can write to the outgoing TCP connection, or (b) there's more to read from the net. */ can_read = wait_on_tcp(OUTPUT_FILTER || ULTRIX_FILTER); /* If there's more to read from the net, let's go handle it immediately. */ if (can_read) continue; /* If we get here, then we know: a) There is nothing to read from the net. b) The lpd daemon has something for us to read. c) It is possible to write to the outgoing TCP connection. So let's get the data from lpd and write it to the TCP connection. We read up to 1024 bytes at a time from lpd. We try to write them out to the TCP connection so that maximum size TCP segments are used; this makes the most efficient use of network bandwidth. We get a character from lpd by reading from standard input, i.e., file descriptor 0. If read returns 0, then lpd has closed the standard input, and the print job is complete. However, if there is already something in the from_lpd buffer, don't read anything new from lpd until we empty out what we already ahve. */ if (lpd_data) goto datafromlpd; n_from_lpd = read(lpd_fd, from_lpd, 1024); if (n_from_lpd == 0) { #ifdef DEBUG1 syslog(LOG_DEBUG, "Standard Input Closed"); #endif #ifdef DEBUG2 if (INPUT_FILTER || ULTRIX_FILTER) syslog(LOG_DEBUG, "Telnetizing used %d extra chars", extra_chars); #endif if (OUTPUT_FILTER || ULTRIX_FILTER) handshake_and_finish(); /* Before we let the Input Filter terminate, make sure it's delivered all characters to the Output Filter */ flush_tcp(); terminate(OKAY); } if (n_from_lpd < 0) { syslog(LOG_DEBUG, "LPD Read Error: %m"); terminate(REPRINT); } /* We've got characters from lpd. Go process each character. */ #ifdef DEBUG2 syslog(LOG_DEBUG, "Read %d chars from lpd", n_from_lpd); #endif lpd_data = TRUE; newfile = FALSE; datafromlpd: can_read = NULL; for (pr_chr_result = OKAY; (firstchar < n_from_lpd) && (pr_chr_result == OKAY); ++firstchar) { can_read = wait_on_tcp(OUTPUT_FILTER || ULTRIX_FILTER); if (can_read) break; pr_chr_result = process_character(from_lpd[firstchar]); } /* If we haven't processed all the data from lpd yet, but there's more to read from the net; go take care of it. In this case, firstchar is set to the place in from_lpd where we want to resume. If (firstchar == n_read), we've emptied the from_lpd buffer, so reset firstchar back to 0. */ if (firstchar == n_from_lpd) { firstchar = 0; lpd_data = FALSE; } if (can_read) continue; /* If pr_chr_result returns ENDOFBANNER (which should happen only when running as an Output Filter),then we have finished printing the banner page. Now we will have to suspend for awhile so that lpd can give the actual file data to the Input Filter. (Of course, the Input Filter will relay it back to us via the child process we are about to fork, but lpd doesn't know that.) If pr_chr_result returns anything else, then we just resume getting more characters from lpd. */ if (pr_chr_result != ENDOFBANNER) continue; /* Before we fork the child process, make sure we've really given TCP all the data we've seen so far. */ flush_tcp(); #ifdef DEBUG1 syslog(LOG_DEBUG, "Suspending Job %d", ++printjobnumber); #endif /* Fork a child process to receive the data from the input filter. The communication from the input filter is completely handled in the relay subroutine. N.B.: The child process NEVER returns from the relay subroutine. */ child = relay(); /* If we're here, we must be the parent process. So we suspend ourself, until lpd sends us a SIGCONT */ kill(getpid(), SIGSTOP); /* Now that we're here, we know that lpd has sent us SIGCONT */ #ifdef DEBUG1 syslog(LOG_DEBUG, "Resuming after job %d", printjobnumber); #endif /* Don't do anything until we are sure that the child process has completed */ pid = wait(&child_status); if (pid != child) { #ifdef DEBUG1 syslog(LOG_DEBUG, "Wrong Child!"); #endif terminate(REPRINT); } #ifdef DEBUG1 syslog(LOG_DEBUG, "Child has Terminated"); #endif /* If the child's exit status is OKAY, we can go on to the next file to be printed. Otherwise, we exit too, passing the child's exit status up to lpd. */ *exit_status /= 256; *exit_status &= 0xff; if (!WIFEXITED(child_status)) { syslog(LOG_DEBUG, "Abnormal Child Termination"); terminate(REPRINT); } if (*exit_status != OKAY) terminate(*exit_status); /* Whew, that file is printed, now we can continue with the next file */ flush_tcp(); newfile = TRUE; #ifdef DEBUG2 syslog(LOG_DEBUG, "Telnetizing generated %d extra chars.", extra_chars); #endif extra_chars = 0; if (lpd_data) goto datafromlpd; } /* FOREVER */ } delay(interval, max_interval, retries, max_retries) int interval, /* base retry interval */ max_interval; /* don't let the interval get longer than this */ int *retries, /* the number of attempts we've already made */ max_retries; /* maximum number of attempts to make, or 0 for no limit */ { int later; if ((*retries)++ == 0) return; /* first attempt, no delay */ if (max_retries && (*retries > max_retries)) { syslog(LOG_DEBUG, "Exceeded max retries (%d), exiting", max_retries); exit(REPRINT); } later = interval << (*retries - 2); if (later > max_interval) later = max_interval; syslog(LOG_DEBUG, "Will try again in %d seconds", later); sleep(later); } /* This subroutine is called when we read something from the net. It ignores everything except Telnet Option Negotiations. */ do_options(byte_from_net) unsigned char byte_from_net; { int i, can_write_to_net, n_written; switch(telnetstate) { case DATA: /* If IAC found, enter IACSCAN state */ if (byte_from_net == IAC) telnetstate = IACSCAN; break; case IACSCAN: /* If WILL, WONT, DO, DONT found, prepare to send back an agreeable answer: enter OPTION state. Otherwise, we will ignore the Telnet Command, and reenter DATA state. */ switch(byte_from_net) { case WILL: answer[1] = DO; break; case DO: answer[1] = WILL; break; case WONT: answer[1] = DONT; break; case DONT: answer[1] = WONT; break; default: telnetstate = DATA; break; } if (telnetstate == IACSCAN) { answer[0] = IAC; telnetstate = OPTION; } break; case OPTION: /* Answer is complete, send it back. */ telnetstate = DATA; answer[2] = byte_from_net; if (answer[2] == BINARY) { switch(answer[1]) { case WILL: binarytransmit = TRUE; break; case WONT: binarytransmit = FALSE; break; default: break; } } for (i = 0; i < 3; ++i) { wait_on_tcp(FALSE); /* We block until we can write the reply */ n_written = write_tcp(answer[i], DONTFLUSH); /* terminate if connection closes */ if (n_written != 1) { syslog(LOG_DEBUG, "TCP Write Error 3: %m"); terminate(REPRINT); } } break; } } /* This subroutine is invoked when we try to write to a dead TCP connection */ disconnection() { #ifdef DEBUG1 syslog(LOG_DEBUG, "SIGPIPE received"); #endif } /* This routine is called in order to force all internally buffered data out to TCP. It blocks until successful. */ flush_tcp() { int can_write; while(tcp_data) { #ifdef DEBUG2 can_write = tcp_mask; select(16, NULL, &can_write, NULL, &poll); if (!can_write) syslog(LOG_DEBUG, "Waiting to flush TCP (%d)",tcp_data); #endif can_write = tcp_mask; select(16, NULL, &can_write, NULL, NULL); #ifdef DEBUG2 syslog(LOG_DEBUG, "Attempting to flush TCP"); #endif push_tcp(); } } /* This routine is called when lpd has finished sending us data (i.e, standard input is closed). We make an attempt to keep the connection open until all the data has actually been printed. To do this, we first handshake with the terminal server by sending a "DO TIMING-MARK" command, and we wait to receive its "WONT TIMING-MARK" command back in response. There can still be about 500 characters left in the terminal server which haven't yet printed out, so we wait 10 more seconds, which will usually be enough. */ handshake_and_finish() { unsigned char byte_from_net, cmd; int i, n; flags &= ~FNDELAY; fcntl(tcp_connection, F_SETFL, flags); /* make socket blocking */ answer[0] = IAC; answer[1] = DO; answer[2] = TIMINGMARK; for (i = 0; i < 3; ++i) { wait_on_tcp(FALSE); write_tcp(answer[i], i == 2); } #ifdef DEBUG1 syslog(LOG_DEBUG, "Starting Handshake"); #endif FOREVER { n = read(tcp_connection, &byte_from_net, 1); if (n < 1) { syslog(LOG_DEBUG, "TCP Read Error 1: %m"); terminate(REPRINT); } if (byte_from_net != IAC) continue; n = read(tcp_connection, &cmd, 1); if (n < 1) { syslog(LOG_DEBUG, "TCP Read Error 2: %m"); terminate(REPRINT); } if ((cmd != DO) && (cmd != DONT) && (cmd != WILL) && (cmd != WONT)) continue; n = read(tcp_connection, &byte_from_net, 1); if (n < 1) { syslog(LOG_DEBUG, "TCP Read Error 3: %m"); terminate(REPRINT); } if (byte_from_net != TIMINGMARK) continue; if ((cmd != WONT) && (cmd != WILL)) continue; #ifdef DEBUG1 syslog(LOG_DEBUG, "Handshake Complete"); #endif break; } /* Now, in a DS300, there can be at most 512 characters backed up at the printer. Let's wait 10 seconds and hope that the printer is not XOFFing us */ close(tcp_connection); sleep(DISCONNECTTIMER); } /* This subroutine sets up the host address and port number data structures */ init(host, service) char *host, *service; { pp = getprotobyname("tcp"); set_port(service, &sp_printer); /* If a host name was given, this will find it either in /etc/hosts or via the Domain Name Service. If an IP address was given, it is used. */ if (isalpha(host[0])) { hp = gethostbyname(host); if (!hp) { syslog(LOG_DEBUG, "Can't Find Host %s: %m", host); exit(REPRINT); } return; } printer_addr = inet_addr(host); hp = gethostbyaddr(&printer_addr, sizeof(printer_addr), AF_INET); if (hp) return; sethostent(NULL); hp = gethostent(); hp -> h_addrtype = AF_INET; hp -> h_length = 4; hp -> h_addr = (char *) &printer_addr; } /* This subroutine initializes the socket structure used for outgoing TCP connections. */ init_outgoing() { bzero((char *)&outgoing_addr, sizeof(outgoing_addr)); bcopy(hp->h_addr, (char *)&outgoing_addr.sin_addr, hp->h_length); outgoing_addr.sin_family = hp->h_addrtype; outgoing_addr.sin_port = sp_printer -> s_port; if (tcp_connection) close(tcp_connection); tcp_connection = socket(AF_INET, SOCK_STREAM, 0); if (tcp_connection < 0) { syslog(LOG_DEBUG, "Can't Create Outgoing TCP Socket: %m"); exit(REPRINT); } tcp_mask = 1 << tcp_connection; flags = fcntl(tcp_connection, F_GETFL); flags &= ~FNDELAY; /* make socket blocking, for connect */ fcntl(tcp_connection, F_SETFL, flags); } /* This subroutine is used by the Output Filter to listen for a connection from the Input Filter */ init_relay(service) char *service; { char *any = (char *)&anyaddr; int bind_retries = 0; listener = socket(AF_INET, SOCK_STREAM, 0); if (listener < 0) { syslog(LOG_DEBUG, "Can't create socket for Listener: %m"); exit(REPRINT); } bzero((char *)&listen_addr, sizeof(listen_addr)); bcopy(any, (char *)&listen_addr.sin_addr, 4); listen_addr.sin_family = AF_INET; set_port(service, &sp_relay); listen_addr.sin_port = sp_relay -> s_port; while (bind(listener, &listen_addr, sizeof(listen_addr)) < 0) { syslog(LOG_DEBUG, "Can't bind address to listener: %m"); delay(BASE_RETRY_INTERVAL, MAX_RETRY_INTERVAL, &bind_retries, 0); } if (listen(listener, 1) < 0) { syslog(LOG_DEBUG, "Can't set up listener: %m"); exit(REPRINT); } } /* This subroutine handles those signals which terminate the process prematurely. */ interruption(signo) int signo; { syslog(LOG_DEBUG, "interrupted by signal %d (%s)", signo, signalnames[signo]); terminate(REPRINT); } /* This subroutine makes outgoing TCP connections. */ make_connection() { int optlen = sizeof(tcp_maxseg); int maxseg_opt; init_outgoing(); delay(BASE_RETRY_INTERVAL, MAX_RETRY_INTERVAL, &retrycount, max_retries); while (connect(tcp_connection, (char *)&outgoing_addr, sizeof(outgoing_addr)) < 0) { syslog(LOG_DEBUG, "Can't Open Outgoing TCP Connection: %m"); delay(BASE_RETRY_INTERVAL, MAX_RETRY_INTERVAL, &retrycount, max_retries); init_outgoing(); } syslog(LOG_DEBUG, "Outgoing TCP Connection Made"); flags |= FNDELAY; fcntl(tcp_connection, F_SETFL, flags); /* make socket non-blocking */ maxseg_opt = 0; #ifdef TCP_MAXSEG maxseg_opt = TCP_MAXSEG; #endif #ifdef TCPOPT_MAXSEG maxseg_opt = TCPOPT_MAXSEG; #endif if (!maxseg_opt || (getsockopt(tcp_connection, pp -> p_proto, maxseg_opt, (char *) &tcp_maxseg, &optlen) < 0)) { tcp_maxseg = 512; #ifdef DEBUG1 syslog(LOG_DEBUG, "TCP MSS = %d (default)", tcp_maxseg); #endif } #ifdef DEBUG1 else syslog(LOG_DEBUG,"TCP MSS = %d (dynamic)", tcp_maxseg); #endif } printhelp() { syslog(LOG_DEBUG, "Invalid Command Line Arguments"); syslog(LOG_DEBUG, "\ti thishost relayport"); syslog(LOG_DEBUG, "\to printerhost printerport relayport"); syslog(LOG_DEBUG, "\tx printerhost printerport"); exit(REPRINT); } /* This subroutine is called to process each character received from lpd. If its ascii value is 255, Telnet protocol requires that it be doubled. If it is a linefeed, Telnet protocol requires that it be converted into a carriage return followed by a linefeed (unless the binary option has been enabled by the peer). This subroutine also checks for the "end of banner" sequence ^Y^A from lpd. */ /* We've got a character from lpd. */ process_character(byte_from_lpd) int byte_from_lpd; { int n_written; switch(byte_from_lpd) /* Process the Character */ { case IAC: /* Double the IACs. */ n_written = write_tcp(iac, DONTFLUSH); ++extra_chars; break; case LF: /* LF --> CR LF, unless binary transmission enabled */ if (!binarytransmit) { n_written = write_tcp(cr, DONTFLUSH); ++extra_chars; } break; case CR: /* CR --> CR NUL, unless binary transmission enabled */ if (!binarytransmit) { n_written = write_tcp(cr, DONTFLUSH); byte_from_lpd = '\0'; ++extra_chars; } break; case '\031': if (OUTPUT_FILTER) { lpdstate = CTRLY; return(OKA push_tcp() { int n_written, n_to_write, last_to_write, can_write; if (!tcp_data) return(0); last_to_write = to_tcp_start + tcp_data - 1; if (last_to_write >= OUTBUFSIZE) last_to_write = OUTBUFSIZE - 1; n_to_write = last_to_write - to_tcp_start + 1; can_write = tcp_mask; select(16, NULL, &can_write, NULL, &poll); if (!can_write) { n_written = 0; if (!tcp_blocking) { tcp_blocking = TRUE; #ifdef DEBUG2 syslog(LOG_DEBUG, "TCP Blocked"); #endif } } else { n_written = write(tcp_connection, &to_tcp[to_tcp_start], n_to_write); if (tcp_blocking) { tcp_blocking = FALSE; #ifdef DEBUG2 if (n_written) syslog(LOG_DEBUG, "TCP Unblocked"); else syslog(LOG_DEBUG, "Blocking Confusion!"); #endif } } if (n_written < 0) { if (errno != EWOULDBLOCK) { syslog(LOG_DEBUG, "TCP Write Error: %m"); terminate(REPRINT); } else n_written = 0; } if (n_written == 0) return(0); #ifdef DEBUG2 if (n_written > 0) syslog(LOG_DEBUG, "Wrote %d bytes (%d-%d) of %d to TCP", n_written, to_tcp_start, to_tcp_start+n_written-1, tcp_data); #endif tcp_data -= n_written; #ifdef DEBUG2 syslog(LOG_DEBUG, "%d bytes still buffered for TCP", tcp_data); #endif /* Adjust to_tcp_start for wraparound, so next push works properly */ to_tcp_start += n_written; if (to_tcp_start >= OUTBUFSIZE) to_tcp_start -= OUTBUFSIZE; /* If we have flushed all the data in the internal buffer, we will start accumulating data at the beginning again. */ if (!tcp_data) to_tcp_start = to_tcp_end = 0; return(n_written); } /* This subroutine is called by the Output Filter after each Banner Page. It forks a child process which gets the file to be printed from the Input Filter, and sends it out the TCP Connection to the printer. N.B.: Data received from the Input Filter has already been Telnetized. */ int relay() { int from_if; int can_read_from_if; int if_mask; int i, n, n_written; int pid; int accept_retries = 1; unsigned char bytes_from_if[1024]; #ifdef DEBUG1 syslog(LOG_DEBUG, "Forking Child Process"); #endif pid = fork(); /* Parent process returns immediately. */ if (pid != 0) return(pid); /* Child Process continues here, and NEVER returns from this routine. */ closelog(); openlog("Telnet OF Child", LOG_PID); #ifdef DEBUG1 syslog(LOG_DEBUG, "Child running"); #endif addrlen = sizeof(incoming_addr); /* Listen for the connection from the Input Filter. */ while ((from_if = accept(listener, &incoming_addr, &addrlen)) < 0) { syslog(LOG_DEBUG, "Accept Failed: %m"); delay(BASE_RETRY_INTERVAL, MAX_RETRY_INTERVAL, &accept_retries, MAX_RETRY_TIMES_TO_OF); } /* Now read from the input filter, and write out to the printer */ #ifdef DEBUG1 syslog(LOG_DEBUG, "Connection from IF received"); #endif if_mask = 1 << from_if; FOREVER { can_read_from_if = if_mask; select(16, &can_read_from_if, NULL, NULL, NULL); n = read(from_if, bytes_from_if, 1024); #ifdef DEBUG2 syslog(LOG_DEBUG, "Read %d from IF", n); #endif if (n == 0) { #ifdef DEBUG1 syslog(LOG_DEBUG, "OF sees IF terminate"); #endif flush_tcp(); terminate(OKAY); } if (n < 0) { syslog(LOG_DEBUG, "Read from IF failure"); terminate(REPRINT); } for (i = 0; i < n; ++i) { wait_on_tcp(FALSE); n_written = write_tcp(bytes_from_if[i], DONTFLUSH); if (n_written < 0) { syslog(LOG_DEBUG, "Internal Write Error 2"); terminate(REPRINT); } } } } /* This subroutine sets up port number data structures */ set_port(service, sp) char *service; struct servent **sp; { int port; short *port_high = (short *)&port; /* If a service name was given, look it up in /etc/services. If it's not alphabetic, it must be a TCP port number. Look up the port number in /etc/services, but if it's not there, use it anyway. */ if (isalpha(service[0])) { *sp = getservbyname(service, "tcp"); if (!*sp) { syslog(LOG_DEBUG, "Can't Find Service %s: %m", service); exit(REPRINT); } return; } port = 0; sscanf(service, "%d", port_high); *port_high = htons(*port_high); /* Don't quite know what I'm doing here, but it seems to work on big-endian machines and on those horrid little-endian machines too. */ *sp = getservbyport(port, "tcp"); if (!*sp) { *sp = (struct servent *) malloc(sizeof (struct servent)); (*sp) -> s_port = port; (*sp) -> s_proto = "tcp"; } } terminate(exit_status) int exit_status; { #ifdef DEBUG1 syslog(LOG_DEBUG, "Terminating with Status %d", exit_status); #endif if (child) kill(child, SIGINT); close(tcp_connection); exit(exit_status); } /* Since TCP is flow controlled, we can't always write to it whenever we want. This routine is called when we need to block until it is possible to write. Logically, we consider that it is "possible to write" as long as the internal buffer is not full. If the argument is TRUE, however, the routine will return if there is data to read from the network as well. */ wait_on_tcp(need_to_read) int need_to_read; { int can_write_to_tcp; int can_read_from_tcp; if (need_to_read) { can_read_from_tcp = tcp_mask; select(16, &can_read_from_tcp, NULL, NULL, &poll); if (can_read_from_tcp) return(can_read_from_tcp); } if (tcp_data < OUTBUFSIZE) return(NULL); while (tcp_data >= OUTBUFSIZE) { if (need_to_read) can_read_from_tcp = tcp_mask; else can_read_from_tcp = NULL; can_write_to_tcp = tcp_mask; #ifdef DEBUG2 syslog(LOG_DEBUG, "Waiting to write to TCP"); #endif select(16, &can_read_from_tcp, &can_write_to_tcp, NULL, NULL); if (can_write_to_tcp) { #ifdef DEBUG2 syslog(LOG_DEBUG, "TCP now writable"); #endif push_tcp(); } if (can_read_from_tcp) return(can_read_from_tcp); } return(NULL); } /* This subroutine is called to "logically" output a byte to the printer. If its second argument is FALSE, it ordinarily just stores the byte in its own buffer. Once it has accumulated enough bytes to fill a maximum size TCP segment, it automatically delivers those bytes to TCP. If the second argument is TRUE, all bytes in the buffer will be given to TCP. */ write_tcp(byte, flush) unsigned char byte; int flush; { if (tcp_data >= OUTBUFSIZE) { syslog(LOG_DEBUG, "Multiple writes to full buffer!"); errno = EWOULDBLOCK; return(-1); } to_tcp[to_tcp_end++] = byte; ++tcp_data; #ifdef DEBUG2 if (tcp_data == OUTBUFSIZE) syslog(LOG_DEBUG, "Internal buffer full, start = %d", to_tcp_start); #endif if (to_tcp_end == OUTBUFSIZE) { #ifdef DEBUG2 syslog(LOG_DEBUG, "Internal Buffer Wrapping, start = %d", to_tcp_start); #endif to_tcp_end = 0; } if (flush) flush_tcp(); else if (tcp_data >= tcp_maxseg) push_tcp(); return(1); }