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);
}