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