summaryrefslogtreecommitdiffstats
path: root/nselib/vuzedht.lua
blob: cacdc21e54b2826c484267d7e4d98eb82c13f110 (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
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
---
-- A Vuze DHT protocol implementation based on the following documentation:
-- o http://wiki.vuze.com/w/Distributed_hash_table
--
-- It currently supports the PING and FIND_NODE requests and parses the
-- responses. The following main classes are used by the library:
--
-- o Request  - the request class containing all of the request classes. It
--              currently contains the Header, PING and FIND_NODE classes.
--
-- o Response - the response class containing all of the response classes. It
--              currently contains the Header, PING, FIND_NODE and ERROR
--              class.
--
-- o Session  - a class containing "session state" such as the transaction- and
--              instance ID's.
--
-- o Helper   - The helper class that serves as the main interface between
--              scripts and the library.
--
-- @author Patrik Karlsson <patrik@cqure.net>
--

local ipOps = require "ipOps"
local math = require "math"
local nmap = require "nmap"
local os = require "os"
local stdnse = require "stdnse"
local string = require "string"
local table = require "table"
local rand = require "rand"
_ENV = stdnse.module("vuzedht", stdnse.seeall)


Request = {

  Actions = {
    ACTION_PING = 1024,
    FIND_NODE = 1028,
  },

  -- The request Header class shared by all Requests classes
  Header = {

    -- Creates a new Header instance
    -- @param action number containing the request action
    -- @param session instance of Session
    -- @return o new instance of Header
    new = function(self, action, session)
      local o = {
        conn_id = string.char(255) .. rand.random_string(7),
        -- we need to handle this one like this, due to a bug in nsedoc
        -- it used to be action = action, but that breaks parsing
        ["action"] = action,
        trans_id = session:getTransactionId(),
        proto_version = 0x32,
        vendor_id = 0,
        network_id = 0,
        local_proto_version = 0x32,
        address = session:getAddress(),
        port = session:getPort(),
        instance_id = session:getInstanceId(),
        time = os.time(),
      }
      setmetatable(o, self)
      self.__index = self
      return o
    end,

    -- Converts the header to a string
    __tostring = function(self)
      local lhost = ipOps.ip_to_str(self.address)
      return self.conn_id .. string.pack( ">I4 I4 BB I4 B s1 I2 I4 I8 ", self.action, self.trans_id,
      self.proto_version, self.vendor_id, self.network_id, self.local_proto_version,
      lhost, self.port, self.instance_id, self.time )
    end,

  },

  -- The PING Request class
  Ping = {

    -- Creates a new Ping instance
    -- @param session instance of Session
    -- @return o new instance of Ping
    new = function(self, session)
      local o = {
        header = Request.Header:new(Request.Actions.ACTION_PING, session)
      }
      setmetatable(o, self)
      self.__index = self
      return o
    end,

    -- Converts a Ping Request to a string
    __tostring = function(self)
      return tostring(self.header)
    end,

  },

  -- The FIND_NODES Request class
  FindNode = {

    -- Creates a new FindNode instance
    -- @param session instance of Session
    -- @return o new instance of FindNode
    new = function(self, session)
      local o = {
        header = Request.Header:new(Request.Actions.FIND_NODE, session),
        node_id = '\xA7' .. rand.random_string(19),
        status = 0xFFFFFFFF,
        dht_size = 0,
      }
      setmetatable(o, self)
      self.__index = self
      return o
    end,

    -- Converts a FindNode Request to a string
    __tostring = function(self)
      local data = tostring(self.header)
      .. string.pack(">s1 I4I4", self.node_id, self.status, self.dht_size)
      return data
    end,
  }

}

Response = {

  -- A table of currently supported Actions (Responses)
  -- It's used in the fromString method to determine which class to create.
  Actions = {
    ACTION_PING = 1025,
    FIND_NODE = 1029,
    ERROR = 1032,
  },

  -- Creates an address record based on received data
  -- @param data containing an address record [C][I|H][S] where
  --        [C] is the length of the address (4 or 16)
  --        [I|H] is the binary address
  --        [S] is the port number as a short
  -- @return o Address instance on success, nil on failure
  Address = {
    new = function(self, data)
      local o = { data = data }
      setmetatable(o, self)
      self.__index = self
      if ( o:parse() ) then
        return o
      end
    end,

    -- Parses the received data
    -- @return true on success, false on failure
    parse = function(self)
      local ip, err
      ip, self.port = string.unpack(">s1 I2", self.data)
      self.ip, err = ipOps.str_to_ip(ip)
      if not self.ip then
        stdnse.debug1("Unknown address type (length: %d)", #ip)
        return false, "Unknown address type"
      end
      return true
    end
  },

  -- The response header, present in all packets
  Header = {

    Vendors = {
      [0] = "Azureus",
      [1] = "ShareNet",
      [255] = "Unknown", -- to be honest, we report all except 0 and 1 as unknown
    },

    Networks = {
      [0] = "Stable",
      [1] = "CVS"
    },

    -- Creates a new Header instance
    -- @param data string containing the received data
    -- @return o instance of Header
    new = function(self, data)
      local o = { data = data }
      setmetatable(o, self)
      self.__index = self
      o:parse()
      return o
    end,

    -- parses the header
    parse = function(self)
      local pos
      self.action, self.trans_id, self.conn_id,
      self.proto_version, self.vendor_id, self.network_id,
      self.instance_id, pos = string.unpack(">I4 I4 c8 BB I4 I4 ", self.data)
    end,

    -- Converts the header to a suitable string representation
    __tostring = function(self)
      local result = {}
      table.insert(result, ("Transaction id: %d"):format(self.trans_id))
      table.insert(result, ("Connection id: 0x%s"):format(stdnse.tohex(self.conn_id)))
      table.insert(result, ("Protocol version: %d"):format(self.proto_version))
      table.insert(result, ("Vendor id: %s (%d)"):format(
        Response.Header.Vendors[self.vendor_id] or "Unknown", self.vendor_id))
      table.insert(result, ("Network id: %s (%d)"):format(
        Response.Header.Networks[self.network_id] or "Unknown", self.network_id))
      table.insert(result, ("Instance id: %d"):format(self.instance_id))
      return stdnse.format_output(true, result)
    end,

  },

  -- The PING response
  PING = {

    -- Creates a new instance of PING
    -- @param data string containing the received data
    -- @return o new PING instance
    new = function(self, data)
      local o = {
        header = Response.Header:new(data)
      }
      setmetatable(o, self)
      self.__index = self
      return o
    end,

    -- Creates a new PING instance based on received data
    -- @param data string containing received data
    -- @return status true on success, false on failure
    -- @return new instance of PING on success, error message on failure
    fromString = function(data)
      local ping = Response.PING:new(data)
      if ( ping ) then
        return true, ping
      end
      return false, "Failed to parse PING response"
    end,

    -- Converts the PING response to a response suitable for script output
    -- @return result formatted script output
    __tostring = function(self)
      return tostring(self.header)
    end,
  },

  -- A class to process the response from a FIND_NODE query
  FIND_NODE = {

    -- Creates a new FIND_NODE instance
    -- @param data string containing the received data
    -- @return o new instance of FIND_NODE
    new = function(self, data)
      local o = {
        header = Response.Header:new(data),
        data = data:sub(27)
      }
      setmetatable(o, self)
      self.__index = self
      o:parse()
      return o
    end,

    -- Parses the FIND_NODE response
    parse = function(self)
      local pos
      self.spoof_id, self.node_type, self.dht_size,
      self.network_coords, pos = string.unpack(">I4 I4 I4 c20", self.data)

      local contact_count
      contact_count, pos = string.unpack("B", self.data, pos)
      self.contacts = {}
      for i=1, contact_count do
        local contact = {}
        local address
        contact.type, contact.proto_version, address, contact.port, pos = string.unpack(
          ">BBs1I2", self.data, pos)

        contact.address = ipOps.str_to_ip(address)
        table.insert(self.contacts, contact)
      end
    end,

    -- Creates a new instance of FIND_NODE based on received data
    -- @param data string containing received data
    -- @return status true on success, false on failure
    -- @return new instance of FIND_NODE on success, error message on failure
    fromString = function(data)
      local find = Response.FIND_NODE:new(data)
      if ( find.header.proto_version < 13 ) then
        stdnse.debug1("ERROR: Unsupported version %d", find.header.proto_version)
        return false
      end

      return true, find
    end,

    -- Convert the FIND_NODE response to formatted string data, suitable
    -- for script output.
    -- @return string with formatted FIND_NODE data
    __tostring = function(self)
      if ( not(self.contacts) ) then
        return ""
      end

      local result = {}
      for _, contact in ipairs(self.contacts) do
        local address = contact.address
        if address:find(":") then
          address = ("[%s]"):format(address)
        end
        table.insert(result, ("%s:%d"):format(address, contact.port))
      end
      return stdnse.format_output(true, result)
    end
  },

  -- The ERROR action
  ERROR = {

    -- Creates a new ERROR instance based on received socket data
    -- @return o new ERROR instance on success, nil on failure
    new = function(self, data)
      local o = {
        header = Response.Header:new(data),
        data = data:sub(27)
      }
      setmetatable(o, self)
      self.__index = self
      if ( o:parse() ) then
        return o
      end
    end,

    -- parses the received data and attempts to create an ERROR response
    -- @return true on success, false on failure
    parse = function(self)
      local err_type, pos = string.unpack(">I4", self.data)
      if ( 1 == err_type ) then
        self.addr = Response.Address:new(self.data:sub(pos))
        return true
      end
      return false
    end,

    -- creates a new ERROR instance based on the received data
    -- @return true on success, false on failure
    fromString = function(data)
      local err = Response.ERROR:new(data)
      if ( err ) then
        return true, err
      end
      return false
    end,

    -- Converts the ERROR action to a formatted response
    -- @return string containing the formatted response
    __tostring = function(self)
      return ("Wrong address, expected: %s"):format(self.addr.ip)
    end,

  },

  -- creates a suitable Response class based on the Action received
  -- @return true on success, false on failure
  -- @return response instance of suitable Response class on success,
  --         err string error message if status is false
  fromString = function(data)
    local action, pos = string.unpack(">I4", data)

    if ( action == Response.Actions.ACTION_PING ) then
      return Response.PING.fromString(data)
    elseif ( action == Response.Actions.FIND_NODE ) then
      return Response.FIND_NODE.fromString(data)
    elseif ( action == Response.Actions.ERROR ) then
      return Response.ERROR.fromString(data)
    end

    stdnse.debug1("ERROR: Unknown response received from server")
    return false, "Failed to parse response"
  end,



}

-- The Session
Session = {

  -- Creates a new Session instance to keep track on some of the protocol
  -- stuff, such as transaction- and instance- identities.
  -- @param address the local address to pass in the requests to the server
  --        this could be either the local address or the IP of the router
  --        depending on if NAT is used or not.
  -- @param port the local port to pass in the requests to the server
  -- @return o new instance of Session
  new = function(self, address, port)
    local o = {
      trans_id = math.random(12345678),
      instance_id = math.random(12345678),
      address = address,
      port = port,
    }
    setmetatable(o, self)
    self.__index = self
    return o
  end,

  -- Gets the next transaction ID
  -- @return trans_id number
  getTransactionId = function(self)
    self.trans_id = self.trans_id + 1
    return self.trans_id
  end,

  -- Gets the next instance ID
  -- @return instance_id number
  getInstanceId = function(self)
    self.instance_id = self.instance_id + 1
    return self.instance_id
  end,

  -- Gets the stored local address used to create the session
  -- @return string containing the IP passed to the session
  getAddress = function(self)
    return self.address
  end,

  -- Get the stored local port used to create the session
  -- @return number containing the local port
  getPort = function(self)
    return self.port
  end

}

-- The Helper class, used as main interface between the scripts and the library
Helper = {

  -- Creates a new instance of the Helper class
  -- @param host table as passed to the action method
  -- @param port table as passed to the action method
  -- @param lhost [optional] used if an alternate local address is to be
  --        passed in the requests to the remote node (ie. NAT is in play).
  -- @param lport [optional] used if an alternate port is to be passed in
  --        the requests to the remote node.
  -- @return o new instance of Helper
  new = function(self, host, port, lhost, lport)
    local o = {
      host = host,
      port = port,
      lhost = lhost,
      lport = lport
    }
    setmetatable(o, self)
    self.__index = self
    return o
  end,

  -- Connects to the remote Vuze Node
  -- @return true on success, false on failure
  -- @return err string error message if status is false
  connect = function(self)
    local lhost = tonumber(self.lhost or stdnse.get_script_args('vuzedht.lhost'))
    local lport = tonumber(self.lport or stdnse.get_script_args('vuzedht.lport'))

    self.socket = nmap.new_socket()

    if ( lport ) then
      self.socket:bind(nil, lport)
    end
    local status, err = self.socket:connect(self.host, self.port)
    if ( not(status) ) then
      return false, "Failed to connect to server"
    end

    if ( not(lhost) or not(lport) ) then
      local status, lh, lp, _, _ = self.socket:get_info()
      if ( not(status) ) then
        return false, "Failed to get socket information"
      end
      lhost = lhost or lh
      lport = lport or lp
    end

    self.session = Session:new(lhost, lport)
    return true
  end,

  -- Sends a Vuze PING request to the server and parses the response
  -- @return status true on success, false on failure
  -- @return response PING response instance on success,
  --         err string containing the error message on failure
  ping = function(self)
    local ping = Request.Ping:new(self.session)
    local status, err = self.socket:send(tostring(ping))
    if ( not(status) ) then
      return false, "Failed to send PING request to server"
    end

    local data
    status, data = self.socket:receive()
    if ( not(status) ) then
      return false, "Failed to receive PING response from server"
    end
    local response
    status, response = Response.fromString(data)
    if ( not(status) ) then
      return false, "Failed to parse PING response from server"
    end
    return true, response
  end,

  -- Requests a list of known nodes by sending the FIND_NODES request
  -- to the remote node and parses the response.
  -- @return status true on success, false on failure
  -- @return response FIND_NODE response instance on success
  --         err string containing the error message on failure
  findNodes = function(self)
    local find = Request.FindNode:new(self.session)
    local status, err = self.socket:send(tostring(find))
    if ( not(status) ) then
      return false, "Failed to send FIND_NODE request to server"
    end

    local data
    status, data = self.socket:receive()
    local response
    status, response = Response.fromString(data)
    if ( not(status) ) then
      return false, "Failed to parse FIND_NODE response from server"
    end
    return true, response
  end,

  -- Closes the socket connect to the remote node
  close = function(self)
    self.socket:close()
  end,
}

return _ENV;