summaryrefslogtreecommitdiffstats
path: root/nselib/rtsp.lua
blob: cd97229a36ce0677917bb5423c61d8c3a453bba6 (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
---
-- This Real Time Streaming Protocol (RTSP) library implements only a minimal
-- subset of the protocol needed by the current scripts.
--
-- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html
-- @author Patrik Karlsson <patrik@cqure.net>
--
-- The library contains the following classes:
--
-- * <code>Request</code>
-- ** This class contains the functions needed to create the RTSP request
--
-- * <code>Response</code>
-- ** This class contains the functions needed to parse the RTSP response
--
-- * <code>Client</code>
-- ** This class contains the RTSP client, a class responsible for sending
--    and receiving requests and responses to/from the server
--
-- * <code>Helper</code>
-- ** This class serves as the main interface for script writers
--
-- The following sample code shows how to use the library:
-- <code>
--   local helper = rtsp.Helper:new(host, port)
--   local status = helper:connect()
--   local response
--   status, response = helper:describe(url)
--   helper:close()
-- </code>

--
-- Version 0.1
-- Created 10/23/2011 - v0.1 - Created by Patrik Karlsson
--

local nmap = require "nmap"
local stdnse = require "stdnse"
local stringaux = require "stringaux"
local table = require "table"
_ENV = stdnse.module("rtsp", stdnse.seeall)

-- The RTSP Request object
Request = {

  --- Creates a new Request instance
  -- @return o instance of Request
  new = function(self, url, headers)
    local o = { url = url, req = {}, headers = headers or {} }
    setmetatable(o, self)
    self.__index = self
    return o
  end,

  --- Sets the RTSP Request method
  -- @param method string containing the RTSP method
  setMethod = function(self, method)
    self.method = method
  end,

  --- Sets the RTSP sequence number
  -- @param cseq number containing the sequence number
  setCSeq = function(self, cseq)
    self.cseq = cseq
  end,

  --- Adds an optional header to the RTSP request
  -- @param header string containing the header name
  -- @param value string containing the header value
  addHeader = function(self, header, value)
    table.insert( self.headers, { header = value } )
  end,

  --- Converts the Request to a string
  --
  -- @return req string containing the request as a string
  __tostring = function(self)
    assert(self.cseq, "Request is missing required header CSeq")
    assert(self.url, "Request is missing URL")

    local req = {
      ("%s %s RTSP/1.0"):format(self.method, self.url),
      ("CSeq: %d"):format(self.cseq),
      table.unpack(self.headers)
    }
    table.insert(req, "")
    table.insert(req, "")
    return table.concat(req, "\r\n")
  end,
}

-- The RTSP response instance
Response = {

  --- Creates a new Response instance
  -- @param data string containing the unparsed data
  new = function(self, data)
    assert(data, "No data was supplied")
    local o = {
      raw = data,
      status = tonumber(data:match("^RTSP%/1%.0 (%d*) "))
    }

    -- Split the response into a temporary array
    local tmp = stringaux.strsplit("\r\n", data)
    if ( not(tmp) ) then return nil end

    -- we should have atleast one entry
    if ( #tmp > 1 ) then
      o.headers = {}
      for i=2, #tmp do
        -- if we have an empty line, this should be the end of headers
        if ( #tmp[i] == 0 ) then break end
        local key, val = tmp[i]:match("^(.-): (.*)$")
        -- create a key per header name
        o.headers[key] = val
      end
    end

    setmetatable(o, self)
    self.__index = self
    return o
  end,

}


-- RTSP Client class
Client = {

  -- Creates a new Client instance
  -- @param host table as received by the action method
  -- @param port table as received by the action method
  -- @return o instance of Client
  new = function(self, host, port)
    local o = {
      host = host,
      port = port,
      cseq = 0,
      headers = { },
      retries = 3,
      timeout = 10 * 1000,
    }
    setmetatable(o, self)
    self.__index = self
    return o
  end,

  --- Sets the number of retries for socket reads
  -- @param retries number containing the number of retries
  setRetries = function(self, retries) self.retries = retries end,

  --- Sets the socket connection timeout in ms
  -- @param timeout number containing the timeout in ms
  setTimeout = function(self, timeout) self.timeout = timeout end,

  --- Adds a RTSP header to the request
  -- @param header string containing the header name
  -- @param value string containing the header value
  addHeader = function(self, header, value)
    table.insert(self.headers, { ("%s: %s"):format(header,value) } )
  end,

  --- Connects to the RTSP server
  -- @return status true on success, false on failure
  -- @return err string containing the error message on failure
  connect = function(self)
    self.socket = nmap.new_socket()
    self.socket:set_timeout(self.timeout)
    local status = self.socket:connect(self.host, self.port)
    if ( not(status) ) then
      stdnse.debug2("Failed to connect to the server: %s", self.host.ip)
      return false, ("Failed to connect to the server: %s"):format(self.host.ip)
    end
    return true
  end,

  --- Sends a DESCRIBE request to the server and receives the response
  -- @param url string containing the RTSP URL
  -- @return status true on success, false on failure
  -- @return response Response instance on success
  --         err string containing the error message on failure
  describe = function(self, url)
    local req = Request:new(url, self.headers)
    req:setMethod("DESCRIBE")
    return self:exch(req)
  end,

  options = function(self, url)
    local req = Request:new(url, self.headers)
    req:setMethod("OPTIONS")
    return self:exch(req)
  end,

  --- Sends a request to the server and receives the response and attempts
  --  to retry if either send or receive fails.
  -- @param request instance of Request
  -- @return status true on success, false on failure
  -- @return response Response instance on success
  --         err string containing the error message on failure
  exch = function(self, req)
    local retries = self.retries
    local status, data
    self.cseq = self.cseq + 1
    req:setCSeq( self.cseq )

    repeat
      local err
      status, err = self.socket:send( tostring(req) )
      -- check if send was successful, in case it wasn't AND
      -- this is our last retry, ABORT
      if ( not(status) and 0 == retries - 1 ) then
        stdnse.debug2("Failed to send request to server (%s)", err)
        return false, ("Failed to send request to server (%s)"):format(err)
      -- if send was successful, attempt to receive the response
      elseif ( status ) then
        status, data = self.socket:receive()
        -- if we got the response all right, break out of retry loop
        if ( status ) then break end
      end
      -- if either send or receive fails, re-connect the socket
      if ( not(status) ) then
        self:close()
        local status, err = self:connect()
        -- if re-connect fails, BAIL out of here
        if ( not(status) ) then
          stdnse.debug2("Failed to reconnect socket to server (%s)", err)
          return false, ("Failed to reconnect socket to server (%s)"):format(err)
        end
      end
      retries = retries - 1
    until( status or retries == 0 )

    if( not(status) ) then
      stdnse.debug2("Failed to receive response from server (%s)", data)
      return false, ("Failed to receive response from server (%s)"):format(data)
    end

    return true, Response:new(data)
  end,

  --- Closes the RTSP socket with the server
  close = function(self)
    return self.socket:close()
  end,

}

-- The Helper class is the main script interface
Helper = {

  -- Creates a new Helper instance
  -- @param host table as received by the action method
  -- @param port table as received by the action method
  -- @return o instance of Client
  new = function(self, host, port)
    local o = { host = host, port = port, client = Client:new(host, port) }
    setmetatable(o, self)
    self.__index = self
    return o
  end,

  -- Connects to the RTSP server
  -- @return status true on success, false on failure
  -- @return err string containing the error message on failure
  connect = function(self)
    return self.client:connect()
  end,

  -- Closes the RTSP socket with the server
  close = function(self)
    return self.client:close()
  end,

  -- Sends a DESCRIBE request to the server and receives the response
  --
  -- @param url string containing the RTSP URL
  -- @return status true on success, false on failure
  -- @return response string containing the unparsed RTSP response on success
  --         err string containing the error message on failure
  describe = function(self, url)
    return self.client:describe(url)
  end,

  options = function(self, url)
    return self.client:options(url)
  end,

}

return _ENV;