summaryrefslogtreecommitdiffstats
path: root/nselib/rsync.lua
blob: f477524d1c2b83682133be0ddf1068fea74ca4ea (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
---
-- A minimalist RSYNC (remote file sync) library
--
-- @author Patrik Karlsson <patrik@cqure.net>

local base64 = require "base64"
local match = require "match"
local nmap = require "nmap"
local stdnse = require "stdnse"
local string = require "string"
local table = require "table"
local openssl = stdnse.silent_require "openssl"
_ENV = stdnse.module("rsync", stdnse.seeall)


-- The Helper class serves as the main interface for script writers
Helper = {

  -- Creates a new instance of the Helper class
  -- @param host table as received by the action function
  -- @param port table as received by the action function
  -- @param options table containing any additional options
  -- @return o instance of Helper
  new = function(self, host, port, options)
    local o = { host = host, port = port, options = options or {} }
    assert(o.options.module, "No rsync module was specified, aborting ...")
    setmetatable(o, self)
    self.__index = self
    return o
  end,

  -- Handles send and receive of control messages
  -- @param data string containing the command to send
  -- @return status true on success, false on failure
  -- @return data containing the response from the server
  --         err string, if status is false
  ctrl_exch = function(self, data)
    local status, err = self.socket:send(data.."\n")
    if ( not(status) ) then
      return false, err
    end
    local status, data = self.socket:receive_buf(match.pattern_limit("\n", 2048), false)
    if( not(status) ) then
      return false, err
    end
    return true, data
  end,

  -- Connects to the rsync server
  -- @return status, true on success, false on failure
  -- @return err string containing an error message if status is false
  connect = function(self, socket)
    self.socket = socket or nmap.new_socket()
    self.socket:set_timeout(self.options.timeout or 5000)
    local status, err = self.socket:connect(self.host, self.port)
    if ( not(status) ) then
      return false, err
    end

    local data
    status, data = self:ctrl_exch("@RSYNCD: 29")
    if ( not(status) ) then
      return false, data
    end
    if ( not(data:match("^@RSYNCD: [%.%d]+$")) ) then
      return false, "Protocol error"
    end
    return true
  end,

  -- Authenticates against the rsync module. If no username is given, assume
  -- no authentication is required.
  -- @param username [optional] string containing the username
  -- @param password [optional] string containing the password
  login = function(self, username, password)
    password = password or ""
    local status, data = self:ctrl_exch(self.options.module)
    if (not(status)) then
      return false, data
    end

    local chall
    if ( data:match("@RSYNCD: OK") ) then
      return true, "No authentication was required"
    else
      chall = data:match("^@RSYNCD: AUTHREQD (.*)$")
      if ( not(chall) and data:match("^@ERROR: Unknown module") ) then
        return false, data:match("^@ERROR: (.*)$")
      elseif ( not(chall) ) then
        return false, "Failed to retrieve challenge"
      end
    end

    if ( chall and not(username) ) then
      return false, "Authentication required"
    end

    local md4 = openssl.md4("\0\0\0\0" .. password .. chall)
    local resp = base64.enc(md4):sub(1,-3)
    status, data = self:ctrl_exch(username .. " " .. resp)
    if (not(status)) then
      return false, data
    end

    if ( data == "@RSYNCD: OK" ) then
      return true, "Authentication successful"
    end
    return false, "Authentication failed"
  end,

  -- Lists accessible modules from the rsync server
  -- @return status true on success, false on failure
  -- @return modules table containing a list of modules
  listModules = function(self)
    local status, data = self.socket:send("\n")
    if (not(status)) then
      return false, data
    end

    local modules = {}
    while(true) do
      status, data = self.socket:receive_buf(match.pattern_limit("\n", 2048), false)
      if (not(status)) then
        return false, data
      end
      if ( data == "@RSYNCD: EXIT" ) then
        break
      else
        table.insert(modules, data)
      end
    end
    return true, modules
  end,

  -- Lists the files available for the directory/module
  -- TODO: Add support for parsing results, seemed straight forward at
  --       first, but wasn't.
  listFiles = function(self)
    -- list recursively and enable MD4 checksums
    local data = ("--server\n--sender\n-rc\n.\n%s\n\n"):format(self.options.module)
    local status, data = self.socket:send(data)
    if ( not(status) ) then
      return false, data
    end
    status, data = self.socket:receive_bytes(4)
    if ( not(status) ) then
      return false, data
    end

    status, data = self.socket:send("\0\0\0\0")
    if ( not(status) ) then
      return false, data
    end

    status, data = self.socket:receive_buf(match.numbytes(4), true)
    if ( not(status) ) then
      return false, data
    end

    local len = string.unpack("<I2", data)
    status, data = self.socket:receive_buf(match.numbytes(len), true)
    if ( not(status) ) then
      return false, data
    end

    -- Parsing goes here
  end,

  -- Disconnects from the rsync server
  -- @return status true on success, false on failure
  disconnect = function(self) return self.socket:close() end,

}

return _ENV;