summaryrefslogtreecommitdiffstats
path: root/nselib/membase.lua
blob: 9b721a6356813ea3cc8124378f5c032cff57a6a9 (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
---
-- A smallish implementation of the Couchbase Membase TAP protocol
-- Based on the scarce documentation from the Couchbase Wiki:
-- * http://www.couchbase.org/wiki/display/membase/SASL+Authentication+Example
--
-- @args membase.authmech SASL authentication mechanism to use. Default and
--                        currently supported: PLAIN
--
-- @author Patrik Karlsson <patrik@cqure.net>
--


local match = require "match"
local nmap = require "nmap"
local sasl = require "sasl"
local stdnse = require "stdnse"
local string = require "string"
local table = require "table"
_ENV = stdnse.module("membase", stdnse.seeall)

-- A minimalistic implementation of the Couchbase Membase TAP protocol
TAP = {

  -- Operations
  Op = {
    LIST_SASL_MECHS = 0x20,
    AUTHENTICATE = 0x21,
  },

  -- Requests
  Request = {

    -- Header breakdown
    -- Field        (offset) (value)
    -- Magic            (0): 0x80 (PROTOCOL_BINARY_REQ)
    -- Opcode           (1): 0x00
    -- Key length     (2-3): 0x0000 (0)
    -- Extra length     (4): 0x00
    -- Data type        (5): 0x00
    -- vbucket        (6-7): 0x0000 (0)
    -- Total body    (8-11): 0x00000000 (0)
    -- Opaque       (12-15): 0x00000000 (0)
    -- CAS          (16-23): 0x0000000000000000 (0)
    Header = {

      -- Creates a new instance of Header
      -- @param opcode number containing the operation
      -- @return o new instance of Header
      new = function(self, opcode)
        local o = {
          magic = 0x80,
          opcode = tonumber(opcode),
          keylen = 0x0000,
          extlen = 0x00,
          data_type = 0x00,
          vbucket = 0x0000,
          total_body = 0x00000000,
          opaque = 0x00000000,
          CAS = 0x0000000000000000,
        }
        setmetatable(o, self)
        self.__index = self
        return o
      end,

      -- Converts the header to string
      -- @return string containing the Header as string
      __tostring = function(self)
        return string.pack(">BB I2 BB I2 I4 I4 I8", self.magic, self.opcode, self.keylen,
        self.extlen, self.data_type, self.vbucket, self.total_body,
        self.opaque, self.CAS)
      end,
    },

    -- List SASL authentication mechanism
    SASLList = {

      -- Creates a new instance of the request
      -- @return o instance of request
      new = function(self)
        local o = {
          -- 0x20 SASL List Mechs
          header = TAP.Request.Header:new(TAP.Op.LIST_SASL_MECHS)
        }
        setmetatable(o, self)
        self.__index = self
        return o
      end,

      -- Converts the request to string
      -- @return string containing the request as string
      __tostring = function(self)
        return tostring(self.header)
      end,
    },

    -- Authenticates using SASL
    Authenticate = {

      -- Creates a new instance of the request
      -- @param username string containing the username
      -- @param password string containing the password
      -- @param mech string containing the SASL mechanism, currently supported:
      --        PLAIN - plain-text authentication
      -- @return o instance of request
      new = function(self, username, password, mech)
        local o = {
          -- 0x20 SASL List Mechs
          header = TAP.Request.Header:new(TAP.Op.AUTHENTICATE),
          username = username,
          password = password,
          mech = mech,
        }
        setmetatable(o, self)
        self.__index = self
        return o
      end,

      -- Converts the request to string
      -- @return string containing the request as string
      __tostring = function(self)
        if ( self.mech == "PLAIN" ) then
          local mech_params = { self.username, self.password }
          local auth_data = sasl.Helper:new(self.mech):encode(table.unpack(mech_params))

          self.header.keylen = #self.mech
          self.header.total_body = #auth_data + #self.mech
          return tostring(self.header) .. self.mech .. auth_data
        end
      end,

    }

  },

  -- Responses
  Response = {

    -- The response header
    -- Header breakdown
    -- Field        (offset) (value)
    -- Magic            (0): 0x81 (PROTOCOL_BINARY_RES)
    -- Opcode           (1): 0x00
    -- Key length     (2-3): 0x0000 (0)
    -- Extra length     (4): 0x00
    -- Data type        (5): 0x00
    -- Status         (6-7): 0x0000 (SUCCESS)
    -- Total body    (8-11): 0x00000005 (5)
    -- Opaque       (12-15): 0x00000000 (0)
    -- CAS          (16-23): 0x0000000000000000 (0)
    Header = {

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

      -- Parse the raw header and populates the class members
      -- @return status true on success, false on failure
      parse = function(self)
        if ( 24 > #self.data ) then
          stdnse.debug1("membase: Header packet too short (%d bytes)", #self.data)
          return false, "Packet to short"
        end
        local pos
        self.magic, self.opcode, self.keylen, self.extlen,
          self.data_type, self.status, self.total_body, self.opaque,
          self.BAI2 , pos = string.unpack(">BB I2 BB I2 I4 I4 I8", self.data)
        return true
      end

    },

    -- Decoders
    Decoder = {

      -- TAP.Op.LIST_SASL_MECHS
      [0x20] = {
        -- Creates a new instance of the decoder
        -- @param data string containing the raw response
        -- @return o instance if successfully parsed, nil on failure
        --         the member variable <code>mechs</code> contains the
        --         supported authentication mechanisms.
        new = function(self, data)
          local o = { data = data }
          setmetatable(o, self)
          self.__index = self
          if ( o:parse() ) then
            return o
          end
        end,

        -- Parses the raw response
        -- @return true on success
        parse = function(self)
          self.mechs = self.data
          return true
        end
      },

      -- Login response
      [0x21] = {
        -- Creates a new instance of the decoder
        -- @param data string containing the raw response
        -- @return o instance if successfully parsed, nil on failure
        --         the member variable <code>status</code> contains the
        --         servers authentication response.
        new = function(self, data)
          local o = { data = data }
          setmetatable(o, self)
          self.__index = self
          if ( o:parse() ) then
            return o
          end
        end,

        -- Parses the raw response
        -- @return true on success
        parse = function(self)
          self.status = self.data
          return true
        end
      }

    }

  },

}

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

  -- Creates a new instance of the helper
  -- @param host table as received by the action method
  -- @param port table as received by the action method
  -- @param options table including options to the helper, currently:
  --        <code>timeout</code> - socket timeout in milliseconds
  new = function(self, host, port, options)
    local o = {
      host = host,
      port = port,
      mech = stdnse.get_script_args("membase.authmech"),
      options = options or {}
    }
    setmetatable(o, self)
    self.__index = self
    return o
  end,

  -- Connects the socket to the server
  -- @return true on success, false on failure
  connect = function(self, socket)
    self.socket = socket or nmap.new_socket()
    self.socket:set_timeout(self.options.timeout or 10000)
    return self.socket:connect(self.host, self.port)
  end,

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

  -- Sends a request to the server, receives and parses the response
  -- @param req a Request instance
  -- @return status true on success, false on failure
  -- @return response instance of Response
  exch = function(self, req)
    local status, err = self.socket:send(tostring(req))
    if ( not(status) ) then
      return false, "Failed to send data"
    end

    local data
    status, data = self.socket:receive_buf(match.numbytes(24), true)
    if ( not(status) ) then
      return false, "Failed to receive data"
    end

    local header = TAP.Response.Header:new(data)

    if ( header.opcode ~= req.header.opcode ) then
      stdnse.debug1("WARNING: Received invalid op code, request contained (%d), response contained (%d)", req.header.opcode, header.opcode)
    end

    if ( not(TAP.Response.Decoder[tonumber(header.opcode)]) ) then
      return false, ("No response handler for opcode: %d"):format(header.opcode)
    end

    local status, data = self.socket:receive_buf(match.numbytes(header.total_body), true)
    if ( not(status) ) then
      return false, "Failed to receive data"
    end

    local response = TAP.Response.Decoder[tonumber(header.opcode)]:new(data)
    if ( not(response) ) then
      return false, "Failed to parse response from server"
    end
    return true, response
  end,

  -- Gets list of supported SASL authentication mechanisms
  getSASLMechList = function(self)
    return self:exch(TAP.Request.SASLList:new())
  end,

  -- Logins to the server
  -- @param username string containing the username
  -- @param password string containing the password
  -- @param mech string containing the SASL mechanism to use
  -- @return status true on success, false on failure
  -- @return response string containing "Auth failure" on failure
  login = function(self, username, password, mech)
    mech = mech or self.mech or "PLAIN"
    local status, response = self:exch(TAP.Request.Authenticate:new(username, password, mech))
    if ( not(status) ) then
      return false, "Auth failure"
    end
    if ( response.status == "Auth failure" ) then
      return false, response.status
    end
    return true, response.status
  end,
}



return _ENV;