1-----------------------------------------------------------------------------
2-- LPD support for the Lua language
3-- LuaSocket toolkit.
4-- Author: David Burgess
5-- Modified by Diego Nehab, but David is in charge
6-----------------------------------------------------------------------------
7--[[
8     if you have any questions: RFC 1179
9]]
10-- make sure LuaSocket is loaded
11local io = require("io")
12local base = _G
13local os = require("os")
14local math = require("math")
15local string = require("string")
16local socket = require("socket")
17local ltn12 = require("ltn12")
18module("socket.lp")
19
20-- default port
21PORT = 515
22SERVER = os.getenv("SERVER_NAME") or os.getenv("COMPUTERNAME") or "localhost"
23PRINTER = os.getenv("PRINTER") or "printer"
24
25local function connect(localhost, option)
26    local host = option.host or SERVER
27    local port = option.port or PORT
28    local skt
29    local try = socket.newtry(function() if skt then skt:close() end end)
30    if option.localbind then
31        -- bind to a local port (if we can)
32        local localport = 721
33        local done, err
34        repeat
35            skt = socket.try(socket.tcp())
36            try(skt:settimeout(30))
37            done, err = skt:bind(localhost, localport)
38            if not done then
39                localport = localport + 1
40                skt:close()
41                skt = nil
42            else break end
43        until localport > 731
44        socket.try(skt, err)
45    else skt = socket.try(socket.tcp()) end
46    try(skt:connect(host, port))
47    return { skt = skt, try = try }
48end
49
50--[[
51RFC 1179
525.3 03 - Send queue state (short)
53
54      +----+-------+----+------+----+
55      | 03 | Queue | SP | List | LF |
56      +----+-------+----+------+----+
57      Command code - 3
58      Operand 1 - Printer queue name
59      Other operands - User names or job numbers
60
61   If the user names or job numbers or both are supplied then only those
62   jobs for those users or with those numbers will be sent.
63
64   The response is an ASCII stream which describes the printer queue.
65   The stream continues until the connection closes.  Ends of lines are
66   indicated with ASCII LF control characters.  The lines may also
67   contain ASCII HT control characters.
68
695.4 04 - Send queue state (long)
70
71      +----+-------+----+------+----+
72      | 04 | Queue | SP | List | LF |
73      +----+-------+----+------+----+
74      Command code - 4
75      Operand 1 - Printer queue name
76      Other operands - User names or job numbers
77
78   If the user names or job numbers or both are supplied then only those
79   jobs for those users or with those numbers will be sent.
80
81   The response is an ASCII stream which describes the printer queue.
82   The stream continues until the connection closes.  Ends of lines are
83   indicated with ASCII LF control characters.  The lines may also
84   contain ASCII HT control characters.
85]]
86
87-- gets server acknowledement
88local function recv_ack(con)
89  local ack = con.skt:receive(1)
90  con.try(string.char(0) == ack, "failed to receive server acknowledgement")
91end
92
93-- sends client acknowledement
94local function send_ack(con)
95  local sent = con.skt:send(string.char(0))
96  con.try(sent == 1, "failed to send acknowledgement")
97end
98
99-- sends queue request
100-- 5.2 02 - Receive a printer job
101--
102--       +----+-------+----+
103--       | 02 | Queue | LF |
104--       +----+-------+----+
105--       Command code - 2
106--       Operand - Printer queue name
107--
108--    Receiving a job is controlled by a second level of commands.  The
109--    daemon is given commands by sending them over the same connection.
110--    The commands are described in the next section (6).
111--
112--    After this command is sent, the client must read an acknowledgement
113--    octet from the daemon.  A positive acknowledgement is an octet of
114--    zero bits.  A negative acknowledgement is an octet of any other
115--    pattern.
116local function send_queue(con, queue)
117  queue = queue or PRINTER
118  local str = string.format("\2%s\10", queue)
119  local sent = con.skt:send(str)
120  con.try(sent == string.len(str), "failed to send print request")
121  recv_ack(con)
122end
123
124-- sends control file
125-- 6.2 02 - Receive control file
126--
127--       +----+-------+----+------+----+
128--       | 02 | Count | SP | Name | LF |
129--       +----+-------+----+------+----+
130--       Command code - 2
131--       Operand 1 - Number of bytes in control file
132--       Operand 2 - Name of control file
133--
134--    The control file must be an ASCII stream with the ends of lines
135--    indicated by ASCII LF.  The total number of bytes in the stream is
136--    sent as the first operand.  The name of the control file is sent as
137--    the second.  It should start with ASCII "cfA", followed by a three
138--    digit job number, followed by the host name which has constructed the
139--    control file.  Acknowledgement processing must occur as usual after
140--    the command is sent.
141--
142--    The next "Operand 1" octets over the same TCP connection are the
143--    intended contents of the control file.  Once all of the contents have
144--    been delivered, an octet of zero bits is sent as an indication that
145--    the file being sent is complete.  A second level of acknowledgement
146--    processing must occur at this point.
147
148-- sends data file
149-- 6.3 03 - Receive data file
150--
151--       +----+-------+----+------+----+
152--       | 03 | Count | SP | Name | LF |
153--       +----+-------+----+------+----+
154--       Command code - 3
155--       Operand 1 - Number of bytes in data file
156--       Operand 2 - Name of data file
157--
158--    The data file may contain any 8 bit values at all.  The total number
159--    of bytes in the stream may be sent as the first operand, otherwise
160--    the field should be cleared to 0.  The name of the data file should
161--    start with ASCII "dfA".  This should be followed by a three digit job
162--    number.  The job number should be followed by the host name which has
163--    constructed the data file.  Interpretation of the contents of the
164--    data file is determined by the contents of the corresponding control
165--    file.  If a data file length has been specified, the next "Operand 1"
166--    octets over the same TCP connection are the intended contents of the
167--    data file.  In this case, once all of the contents have been
168--    delivered, an octet of zero bits is sent as an indication that the
169--    file being sent is complete.  A second level of acknowledgement
170--    processing must occur at this point.
171
172
173local function send_hdr(con, control)
174  local sent = con.skt:send(control)
175  con.try(sent and sent >= 1 , "failed to send header file")
176  recv_ack(con)
177end
178
179local function send_control(con, control)
180  local sent = con.skt:send(control)
181  con.try(sent and sent >= 1, "failed to send control file")
182  send_ack(con)
183end
184
185local function send_data(con,fh,size)
186  local buf
187  while size > 0 do
188    buf,message = fh:read(8192)
189    if buf then
190      st = con.try(con.skt:send(buf))
191      size = size - st
192    else
193      con.try(size == 0, "file size mismatch")
194    end
195  end
196  recv_ack(con) -- note the double acknowledgement
197  send_ack(con)
198  recv_ack(con)
199  return size
200end
201
202
203--[[
204local control_dflt = {
205  "H"..string.sub(socket.hostname,1,31).."\10",        -- host
206  "C"..string.sub(socket.hostname,1,31).."\10",        -- class
207  "J"..string.sub(filename,1,99).."\10",               -- jobname
208  "L"..string.sub(user,1,31).."\10",                   -- print banner page
209  "I"..tonumber(indent).."\10",                        -- indent column count ('f' only)
210  "M"..string.sub(mail,1,128).."\10",                  -- mail when printed user@host
211  "N"..string.sub(filename,1,131).."\10",              -- name of source file
212  "P"..string.sub(user,1,31).."\10",                   -- user name
213  "T"..string.sub(title,1,79).."\10",                  -- title for banner ('p' only)
214  "W"..tonumber(width or 132).."\10",                  -- width of print f,l,p only
215
216  "f"..file.."\10",                                    -- formatted print (remove control chars)
217  "l"..file.."\10",                                    -- print
218  "o"..file.."\10",                                    -- postscript
219  "p"..file.."\10",                                    -- pr format - requires T, L
220  "r"..file.."\10",                                    -- fortran format
221  "U"..file.."\10",                                    -- Unlink (data file only)
222}
223]]
224
225-- generate a varying job number
226local seq = 0
227local function newjob(connection)
228    seq = seq + 1
229    return math.floor(socket.gettime() * 1000 + seq)%1000
230end
231
232
233local format_codes = {
234  binary = 'l',
235  text = 'f',
236  ps = 'o',
237  pr = 'p',
238  fortran = 'r',
239  l = 'l',
240  r = 'r',
241  o = 'o',
242  p = 'p',
243  f = 'f'
244}
245
246-- lp.send{option}
247-- requires option.file
248
249send = socket.protect(function(option)
250  socket.try(option and base.type(option) == "table", "invalid options")
251  local file = option.file
252  socket.try(file, "invalid file name")
253  local fh = socket.try(io.open(file,"rb"))
254  local datafile_size = fh:seek("end") -- get total size
255  fh:seek("set")                       -- go back to start of file
256  local localhost = socket.dns.gethostname() or os.getenv("COMPUTERNAME")
257    or "localhost"
258  local con = connect(localhost, option)
259-- format the control file
260  local jobno = newjob()
261  local localip = socket.dns.toip(localhost)
262  localhost = string.sub(localhost,1,31)
263  local user = string.sub(option.user or os.getenv("LPRUSER") or
264    os.getenv("USERNAME") or os.getenv("USER") or "anonymous", 1,31)
265  local lpfile = string.format("dfA%3.3d%-s", jobno, localhost);
266  local fmt = format_codes[option.format] or 'l'
267  local class = string.sub(option.class or localip or localhost,1,31)
268  local _,_,ctlfn = string.find(file,".*[%/%\\](.*)")
269  ctlfn = string.sub(ctlfn  or file,1,131)
270    local cfile =
271      string.format("H%-s\nC%-s\nJ%-s\nP%-s\n%.1s%-s\nU%-s\nN%-s\n",
272      localhost,
273    class,
274      option.job or "LuaSocket",
275    user,
276    fmt, lpfile,
277    lpfile,
278    ctlfn); -- mandatory part of ctl file
279  if (option.banner) then cfile = cfile .. 'L'..user..'\10' end
280  if (option.indent) then cfile = cfile .. 'I'..base.tonumber(option.indent)..'\10' end
281  if (option.mail) then cfile = cfile .. 'M'..string.sub((option.mail),1,128)..'\10' end
282  if (fmt == 'p' and option.title) then cfile = cfile .. 'T'..string.sub((option.title),1,79)..'\10' end
283  if ((fmt == 'p' or fmt == 'l' or fmt == 'f') and option.width) then
284    cfile = cfile .. 'W'..base.tonumber(option,width)..'\10'
285  end
286
287  con.skt:settimeout(option.timeout or 65)
288-- send the queue header
289  send_queue(con, option.queue)
290-- send the control file header
291  local cfilecmd = string.format("\2%d cfA%3.3d%-s\n",string.len(cfile), jobno, localhost);
292  send_hdr(con,cfilecmd)
293
294-- send the control file
295  send_control(con,cfile)
296
297-- send the data file header
298  local dfilecmd = string.format("\3%d dfA%3.3d%-s\n",datafile_size, jobno, localhost);
299  send_hdr(con,dfilecmd)
300
301-- send the data file
302  send_data(con,fh,datafile_size)
303  fh:close()
304  con.skt:close();
305  return jobno, datafile_size
306end)
307
308--
309-- lp.query({host=,queue=printer|'*', format='l'|'s', list=})
310--
311query = socket.protect(function(p)
312  p = p or {}
313  local localhost = socket.dns.gethostname() or os.getenv("COMPUTERNAME")
314    or "localhost"
315  local con = connect(localhost,p)
316  local fmt
317  if string.sub(p.format or 's',1,1) == 's' then fmt = 3 else fmt = 4 end
318  con.try(con.skt:send(string.format("%c%s %s\n", fmt, p.queue or "*",
319    p.list or "")))
320  local data = con.try(con.skt:receive("*a"))
321  con.skt:close()
322  return data
323end)
324