1---
2-- An implementation of the Canon BJNP protocol used to discover and query
3-- Canon network printers and scanner devices.
4--
5-- The implementation is pretty much based on Wireshark decoded messages
6-- the cups-bjnp implementation and the usual guesswork.
7--
8-- @author Patrik Karlsson <patrik [at] cqure.net>
9--
10
11local nmap = require("nmap")
12local os = require("os")
13local stdnse = require("stdnse")
14local table = require("table")
15local string = require "string"
16local stringaux = require "stringaux"
17
18_ENV = stdnse.module("bjnp", stdnse.seeall)
19
20BJNP = {
21
22  -- The common BJNP header
23  Header = {
24
25    new = function(self, o)
26      o = o or {}
27      o = {
28        id = o.id or "BJNP",
29        type = o.type or 1,
30        code = o.code,
31        seq = o.seq or 1,
32        session = o.session or 0,
33        length = o.length or 0,
34      }
35      assert(o.code, "code argument required")
36      setmetatable(o, self)
37      self.__index = self
38      return o
39    end,
40
41    parse = function(data)
42      local hdr = BJNP.Header:new({ code = -1 })
43
44      hdr.id, hdr.type, hdr.code,
45        hdr.seq, hdr.session, hdr.length = string.unpack(">c4BBI4I2I4", data)
46      return hdr
47    end,
48
49    __tostring = function(self)
50      return string.pack(">c4BBI4I2I4",
51      self.id,
52      self.type,
53      self.code,
54      self.seq,
55      self.session,
56      self.length
57      )
58    end
59  },
60
61  -- Scanner related code
62  Scanner = {
63
64    Code = {
65      DISCOVER = 1,
66      IDENTITY = 48,
67    },
68
69    Request = {
70
71      Discover = {
72
73        new = function(self)
74          local o = { header = BJNP.Header:new( { type = 2, code = BJNP.Scanner.Code.DISCOVER }) }
75          setmetatable(o, self)
76          self.__index = self
77          return o
78        end,
79
80        __tostring = function(self)
81          return tostring(self.header)
82        end,
83      },
84
85
86      Identity = {
87
88        new = function(self)
89          local o = { header = BJNP.Header:new( { type = 2, code = BJNP.Scanner.Code.IDENTITY, length = 4 }), data = 0 }
90          setmetatable(o, self)
91          self.__index = self
92          return o
93        end,
94
95        __tostring = function(self)
96          return tostring(self.header) .. string.pack(">I4", self.data)
97        end,
98      }
99
100    },
101
102    Response = {
103
104      Identity = {
105
106        new = function(self)
107          local o = {}
108          setmetatable(o, self)
109          self.__index = self
110          return o
111        end,
112
113        parse = function(data)
114          local identity = BJNP.Scanner.Response.Identity:new()
115          identity.header = BJNP.Header.parse(data)
116
117          local pos = #tostring(identity.header) + 1
118          if pos - 1 > #data - 2 then
119            return nil
120          end
121          local len, pos = string.unpack(">I2", data, pos)
122          identity.data = string.unpack("c" .. len - 2, data, pos)
123          return identity
124        end,
125
126
127      }
128
129    }
130
131  },
132
133  -- Printer related code
134  Printer = {
135
136    Code = {
137      DISCOVER = 1,
138      IDENTITY = 48,
139    },
140
141    Request = {
142
143      Discover = {
144        new = function(self)
145          local o = { header = BJNP.Header:new( { code = BJNP.Printer.Code.DISCOVER }) }
146          setmetatable(o, self)
147          self.__index = self
148          return o
149        end,
150
151        __tostring = function(self)
152          return tostring(self.header)
153        end,
154      },
155
156      Identity = {
157
158        new = function(self)
159          local o = { header = BJNP.Header:new( { code = BJNP.Printer.Code.IDENTITY }) }
160          setmetatable(o, self)
161          self.__index = self
162          return o
163        end,
164
165        __tostring = function(self)
166          return tostring(self.header)
167        end,
168      }
169
170    },
171
172    Response = {
173
174      Identity = {
175
176        new = function(self)
177          local o = {}
178          setmetatable(o, self)
179          self.__index = self
180          return o
181        end,
182
183        parse = function(data)
184          local identity = BJNP.Printer.Response.Identity:new()
185          identity.header = BJNP.Header.parse(data)
186
187          local pos = #tostring(identity.header) + 1
188          if pos - 1 > #data - 2 then
189            return nil
190          end
191          local len, pos = string.unpack(">I2", data, pos)
192          identity.data = string.unpack("c" .. len - 2, data, pos)
193          return identity
194        end,
195
196
197      }
198
199    },
200
201  }
202
203}
204
205-- Helper class, the main script writer interface
206Helper = {
207
208  -- Creates a new Helper instance
209  -- @param host table
210  -- @param port table
211  -- @param options table containing one or more of the following fields;
212  -- <code>timeout</code> - the timeout in milliseconds for socket communication
213  -- <code>bcast</code> - instructs the library that the host is a broadcast
214  --                      address
215  -- @return o new instance of Helper
216  new = function(self, host, port, options)
217    local o = {
218      host = host, port = port, options = options or {}
219    }
220    o.options.timeout = o.options.timeout or 5000
221    setmetatable(o, self)
222    self.__index = self
223    return o
224  end,
225
226  -- Connects the socket to the device
227  -- This should always be called, regardless if the broadcast option is set
228  -- or not.
229  --
230  -- @return status, true on success, false on failure
231  -- @return err string containing the error message if status is false
232  connect = function(self)
233    self.socket = nmap.new_socket(( self.options.bcast and "udp" ))
234    self.socket:set_timeout(self.options.timeout)
235    if ( not(self.options.bcast) ) then
236      return self.socket:connect(self.host, self.port)
237    end
238    return true
239  end,
240
241  -- Discover network devices using either broadcast or unicast
242  -- @param packet discovery packet (printer or scanner)
243  -- @return status, true on success, false on failure
244  -- @return devices table containing discovered devices when status is true
245  --         errmsg string containing the error message when status is false
246  discoverDevice = function(self, packet)
247    if ( not(self.options.bcast) ) then
248      if ( not(self.socket:send(tostring(packet))) ) then
249        return false, "Failed to send request to server"
250      end
251    else
252      if ( not(self.socket:sendto(self.host, self.port, tostring(packet))) ) then
253        return false, "Failed to send request to server"
254      end
255    end
256    -- discover run in loop
257    local devices, tmp = {}, {}
258    local start = os.time()
259    while( true ) do
260      local status, data = self.socket:receive()
261      if ( not(status) or ( os.time() - start > ( self.options.timeout/1000 - 1 ) )) then
262        break
263      end
264      local status, _, _, rhost = self.socket:get_info()
265      tmp[rhost] = true
266    end
267    for host in pairs(tmp) do table.insert(devices, host) end
268    return true, ( self.options.bcast and devices or ( #devices > 0 and devices[1] ))
269  end,
270
271  -- Discover BJNP supporting scanners
272  discoverScanner = function(self)
273    return self:discoverDevice(BJNP.Scanner.Request.Discover:new())
274  end,
275
276  -- Discover BJNP supporting printers
277  discoverPrinter = function(self)
278    return self:discoverDevice(BJNP.Printer.Request.Discover:new())
279  end,
280
281  -- Gets a printer identity (additional information)
282  -- @param devtype string containing either the string printer or scanner
283  -- @return status, true on success, false on failure
284  -- @return attribs table containing device attributes when status is true
285  --         errmsg string containing the error message when status is false
286  getDeviceIdentity = function(self, devtype)
287    -- Were currently only decoding this as I don't know what the other cruft is
288    local attrib_names = {
289      ["scanner"] = {
290        { ['MFG'] = "Manufacturer" },
291        { ['MDL'] = "Model" },
292        { ['DES'] = "Description" },
293        { ['CMD'] = "Command" },
294      },
295      ["printer"] = {
296        { ['MFG'] = "Manufacturer" },
297        { ['MDL'] = "Model" },
298        { ['DES'] = "Description" },
299        { ['VER'] = "Firmware version" },
300        { ['CMD'] = "Command" },
301      }
302    }
303    local identity
304    if ( "printer" == devtype ) then
305      identity = BJNP.Printer.Request.Identity:new()
306    elseif ( "scanner" == devtype ) then
307      identity = BJNP.Scanner.Request.Identity:new()
308    end
309    assert(not(self.options.bcast), "getIdentity is not supported for broadcast")
310    if ( not(self.socket:send(tostring(identity))) ) then
311      return false, "Failed to send request to server"
312    end
313    local status, data = self.socket:receive()
314    if ( not(status) ) then
315      return false, "Failed to receive response from server"
316    end
317
318    local identity
319    if ( "printer" == devtype ) then
320      identity = BJNP.Printer.Response.Identity.parse(data)
321    elseif ( "scanner" == devtype ) then
322      identity = BJNP.Scanner.Response.Identity.parse(data)
323    end
324    if ( not(identity) ) then
325      return false, "Failed to parse identity"
326    end
327    local attrs, kvps = {}, {}
328
329    for k, v in ipairs(stringaux.strsplit(";", identity.data)) do
330      local nm, val = v:match("^([^:]*):(.*)$")
331      if ( nm ) then kvps[nm] = val end
332    end
333
334    for _, attrib in ipairs(attrib_names[devtype]) do
335      local short, long = next(attrib)
336      if ( kvps[short] ) then
337        table.insert(attrs, ("%s: %s"):format(long, kvps[short]))
338      end
339    end
340
341    return true, attrs
342  end,
343
344  -- Retrieves information related to the printer
345  getPrinterIdentity = function(self)
346    return self:getDeviceIdentity("printer")
347  end,
348
349  -- Retrieves information related to the scanner
350  getScannerIdentity = function(self)
351    return self:getDeviceIdentity("scanner")
352  end,
353
354  -- Closes the connection
355  -- @return status, true on success, false on failure
356  -- @return errmsg string containing the error message when status is false
357  close = function(self)
358    return self.socket:close()
359  end
360
361}
362
363return _ENV;
364