summaryrefslogtreecommitdiffstats
path: root/nselib/upnp.lua
blob: 0edbb6bd8cb251a0b345929bd206483acb6870cf (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
--- A UPNP library based on code from upnp-info initially written by
-- Thomas Buchanan. The code was factored out from upnp-info and partly
-- re-written by Patrik Karlsson <patrik@cqure.net> in order to support
-- multicast requests.
--
-- The library supports sending UPnP requests and decoding the responses
--
-- The library contains the following classes
-- * <code>Comm</code>
-- ** A class that handles communication with the UPnP service
-- * <code>Helper</code>
-- ** The helper class wraps the <code>Comm</code> class using functions with a more descriptive name.
-- * <code>Util</code>
-- ** The <code>Util</code> class contains a number of static functions mainly used to convert and sort data.
--
-- The following code snippet queries all UPnP services on the network:
-- <code>
--   local helper = upnp.Helper:new()
--   helper:setMulticast(true)
--   return stdnse.format_output(helper:queryServices())
-- </code>
--
-- This next snippet queries a specific host for the same information:
-- <code>
--   local helper = upnp.Helper:new(host, port)
--   return stdnse.format_output(helper:queryServices())
-- </code>
--
--
-- @author Thomas Buchanan
-- @author Patrik Karlsson <patrik@cqure.net>

--
-- Version 0.1
--

local http = require "http"
local ipOps = require "ipOps"
local nmap = require "nmap"
local stdnse = require "stdnse"
local string = require "string"
local table = require "table"
local target = require "target"
_ENV = stdnse.module("upnp", stdnse.seeall)

Util = {

  --- Compare function used for sorting IP-addresses
  --
  -- @param a table containing first item
  -- @param b table containing second item
  -- @return true if a is less than b
  ipCompare = function(a, b)
    return ipOps.compare_ip(a, "lt", b)
  end,

}

Comm = {

  --- Creates a new Comm instance
  --
  -- @param host string containing the host name or ip
  -- @param port number containing the port to connect to
  -- @return o a new instance of Comm
  new = function( self, host, port )
    local o = {}
    setmetatable(o, self)
    self.__index = self
    o.host = host
    o.port = port
    o.mcast = false
    return o
  end,

  --- Connect to the server
  --
  -- @return status true on success, false on failure
  connect = function( self )
    if ( self.mcast ) then
      self.socket = nmap.new_socket("udp")
      self.socket:set_timeout(5000)
    else
      self.socket = nmap.new_socket()
      self.socket:set_timeout(5000)
      local status, err = self.socket:connect(self.host, self.port, "udp" )
      if ( not(status) ) then return false, err end
    end

    return true
  end,

  --- Send the UPNP discovery request to the server
  --
  -- @return status true on success, false on failure
  sendRequest = function( self )

    -- for details about the UPnP message format, see http://upnp.org/resources/documents.asp
    local payload = 'M-SEARCH * HTTP/1.1\r\n\z
    Host:239.255.255.250:1900\r\n\z
    ST:upnp:rootdevice\r\n\z
    Man:"ssdp:discover"\r\n\z
    MX:3\r\n\r\n'

    local status, err

    if ( self.mcast ) then
      status, err = self.socket:sendto( self.host, self.port, payload )
    else
      status, err = self.socket:send( payload )
    end

    if ( not(status) ) then return false, err end

    return true
  end,

  --- Receives one or multiple UPNP responses depending on whether
  -- <code>setBroadcast</code> was enabled or not.
  --
  -- The function returns the
  -- status and a response containing:
  -- * an array (table) of responses if broadcast is used
  -- * a single response if broadcast is not in use
  -- * an error message if status was false
  --
  -- @return status true on success, false on failure
  -- @return result table or string containing results or error message
  --         on failure.
  receiveResponse = function( self )
    local status, response
    local result = {}
    local host_responses = {}

    repeat
      status, response = self.socket:receive()
      if ( not(status) and #response == 0 ) then
        return false, response
      elseif( not(status) ) then
        break
      end

      local status, _, _, ip, _ = self.socket:get_info()
      if ( not(status) ) then
        return false, "Failed to retrieve socket information"
      end
      if target.ALLOW_NEW_TARGETS then target.add(ip) end

      if ( not(host_responses[ip]) ) then
        local status, output = self:decodeResponse( response )
        if ( not(status) ) then
          return false, "Failed to decode UPNP response"
        end
        output = { output }
        output.name = ip
        table.insert( result, output )
        host_responses[ip] = true
      end
    until ( not( self.mcast ) )

    if ( self.mcast ) then
      table.sort(result, Util.ipCompare)
      return true, result
    end

    if ( status and #result > 0 ) then
      return true, result[1]
    else
      return false, "Received no responses"
    end
  end,

  --- Processes a response from a upnp device
  --
  -- @param response as received over the socket
  -- @return status boolean true on success, false on failure
  -- @return response table or string suitable for output or error message if status is false
  decodeResponse = function( self, response )
    local output = {}

    if response ~= nil then
      -- We should get a response back that has contains one line for the server, and one line for the xml file location
      -- these match any combination of upper and lower case responses
      local server, location
      server = string.match(response, "[Ss][Ee][Rr][Vv][Ee][Rr]:%s*(.-)\r?\n")
      if server ~= nil then table.insert(output, "Server: " .. server ) end
      location = string.match(response, "[Ll][Oo][Cc][Aa][Tt][Ii][Oo][Nn]:%s*(.-)\r?\n")
      if location ~= nil then
        table.insert(output, "Location: " .. location )

        local v = nmap.verbosity()

        -- the following check can output quite a lot of information, so we require at least one -v flag
        if v > 0 then
          local status, result = self:retrieveXML( location )
          if status then
            table.insert(output, result)
          end
        end
      end
      if #output > 0 then
        return true, output
      else
        return false, "Could not decode response"
      end
    end
  end,

  --- Retrieves the XML file that describes the UPNP device
  --
  -- @param location string containing the location of the XML file from the UPNP response
  -- @return status boolean true on success, false on failure
  -- @return response table or string suitable for output or error message if status is false
  retrieveXML = function( self, location )
    local response
    local options = {}
    options['header'] = {}
    options['header']['Accept'] = "text/xml, application/xml, text/html"

    -- if we're in multicast mode, or if the user doesn't want us to override the IP address,
    -- just use the HTTP library to grab the XML file
    if ( self.mcast or ( not self.override ) ) then
      response = http.get_url( location, options )
    else
      -- otherwise, split the location into an IP address, port, and path name for the xml file
      local xhost, xport, xfile
      xhost = string.match(location, "http://(.-)/")
      -- check to see if the host portion of the location specifies a port
      -- if not, use port 80 as a standard web server port
      if xhost ~= nil and string.match(xhost, ":") then
        xport = string.match(xhost, ":(.*)")
        xhost = string.match(xhost, "(.*):")
      end

      -- check to see if the IP address returned matches the IP address we scanned
      if xhost ~= self.host.ip then
        stdnse.debug1("IP addresses did not match! Found %s, using %s instead.", xhost, self.host.ip)
        xhost = self.host.ip
      end

      if xport == nil then
        xport = 80
      end

      -- extract the path name from the location field, but strip off the \r that HTTP servers return
      xfile = string.match(location, "http://.-(/.-)\013")
      if xfile ~= nil then
        response = http.get( xhost, xport, xfile, options )
      end
    end

    if response ~= nil then
      local output = {}

      -- extract information about the webserver that is handling responses for the UPnP system
      local webserver = response['header']['server']
      if webserver ~= nil then table.insert(output, "Webserver: " .. webserver) end

      -- the schema for UPnP includes a number of <device> entries, which can a number of interesting fields
      for device in string.gmatch(response['body'], "<deviceType>(.-)</UDN>") do
        local fn, mnf, mdl, nm, ver

        fn = string.match(device, "<friendlyName>(.-)</friendlyName>")
        mnf = string.match(device, "<manufacturer>(.-)</manufacturer>")
        mdl = string.match(device, "<modelDescription>(.-)</modelDescription>")
        nm = string.match(device, "<modelName>(.-)</modelName>")
        ver = string.match(device, "<modelNumber>(.-)</modelNumber>")

        if fn ~= nil then table.insert(output, "Name: " .. fn) end
        if mnf ~= nil then table.insert(output,"Manufacturer: " .. mnf) end
        if mdl ~= nil then table.insert(output,"Model Descr: " .. mdl) end
        if nm ~= nil then table.insert(output,"Model Name: " .. nm) end
        if ver ~= nil then table.insert(output,"Model Version: " .. ver) end
      end
      return true, output
    else
      return false, "Could not retrieve XML file"
    end
  end,

  --- Enables or disables multicast support
  --
  -- @param mcast boolean true if multicast is to be used, false otherwise
  setMulticast = function( self, mcast )
    assert( type(mcast)=="boolean", "mcast has to be either true or false")
    self.mcast = mcast
    local family = nmap.address_family()
    self.host = (family=="inet6" and "FF02::C" or "239.255.255.250")
    self.port = 1900
  end,

  --- Closes the socket
  close = function( self ) self.socket:close() end

}


Helper = {

  --- Creates a new helper instance
  --
  -- @param host string containing the host name or ip
  -- @param port number containing the port to connect to
  -- @return o a new instance of Helper
  new = function( self, host, port )
    local o = {}
    setmetatable(o, self)
    self.__index = self
    o.comm = Comm:new( host, port )
    return o
  end,

  --- Enables or disables multicast support
  --
  -- @param mcast boolean true if multicast is to be used, false otherwise
  setMulticast = function( self, mcast ) self.comm:setMulticast(mcast) end,

  --- Enables or disables whether the script will override the IP address is the Location URL
  --
  -- @param override boolean true if override is to be enabled, false otherwise
  setOverride = function( self, override )
    assert( type(override)=="boolean", "override has to be either true or false")
    self.comm.override = override
  end,

  --- Sends a UPnP queries and collects a single or multiple responses
  --
  -- @return status true on success, false on failure
  -- @return result table or string containing results or error message
  --         on failure.
  queryServices = function( self )
    local status, err = self.comm:connect()
    local response

    if ( not(status) ) then return false, err end

    status, err = self.comm:sendRequest()
    if ( not(status) ) then return false, err end

    status, response = self.comm:receiveResponse()
    self.comm:close()

    return status, response
  end,

}

return _ENV;