├── LICENSE ├── README.md ├── Tupfile ├── const.lua ├── lua-tarantool-scm-1.rockspec ├── tarantool.lua └── tarantool.moon /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2017, Dinar Sabitov, Conrad Steenberg 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | lua-tarantool-client 2 | =================== 3 | 4 | Driver for tarantool 1.7 on nginx cosockets and plain lua sockets 5 | 6 | Introduction 7 | ------------ 8 | 9 | A pure Lua driver for the NoSQL database [Tarantool](http://tarantool.org/) using fast nginx cosockets when available, or [luasocket](https://github.com/diegonehab/luasocket) as a fallback. 10 | 11 | Requires [lua-MessagePack](https://github.com/fperrad/lua-MessagePack). 12 | 13 | luasock 14 | ------- 15 | 16 | For `luasock` sockets, [lua-resty-socket](https://github.com/thibaultcha/lua-resty-socket) and [sha1.lua](https://github.com/kikito/sha1.lua) are required. 17 | 18 | These can be installed using `luarocks install lua-resty-socket` and `luarocks install sha1` 19 | 20 | 21 | Synopsis 22 | ------------ 23 | 24 | ```lua 25 | 26 | tarantool = require("tarantool") 27 | 28 | -- initialize connection 29 | local tar = tarantool({ 30 | host = '127.0.0.1', 31 | port = 3301, 32 | user = 'gg_tester', 33 | password = 'pass', 34 | socket_timeout = 2000, 35 | connect_now = true, 36 | }) 37 | 38 | -- requests 39 | local data, err = tar:ping() 40 | local data, err = tar:insert('profiles', { 1, "nick 1" }) 41 | local data, err = tar:insert('profiles', { 2, "nick 2" }) 42 | local data, err = tar:select(2, 0, 3) 43 | local data, err = tar:select('profiles', 'uid', 3) 44 | local data, err = tar:replace('profiles', {3, "nick 33"}) 45 | local data, err = tar:delete('profiles', 3) 46 | local data, err = tar:update('profiles', 'uid', 3, {{ '=', 1, 'nick new' }}) 47 | local data, err = tar:update('profiles', 'uid', 3, {{ '#', 1, 1 }}) 48 | 49 | -- disconnect or set_keepalive at the end 50 | local ok, err = tar:disconnect() 51 | local ok, err = tar:set_keepalive() 52 | 53 | ``` 54 | 55 | Hacking 56 | ------- 57 | 58 | Module contains implementations written in Lua and 59 | [Moonscript][moonscript-url]. First one could be generated using second one 60 | using the Moonscript compiler: 61 | 62 | ```sh 63 | $ luarocks --local install moonscript 64 | $ export PATH=$PATH:$(luarocks path --lr-bin) 65 | $ moonc -o tarantool.lua tarantool.moon 66 | ``` 67 | 68 | [moonscript-url]: https://moonscript.org/ 69 | -------------------------------------------------------------------------------- /Tupfile: -------------------------------------------------------------------------------- 1 | : foreach *.moon |> moonc -o %B.lua %f |> %B.lua 2 | -------------------------------------------------------------------------------- /const.lua: -------------------------------------------------------------------------------- 1 | -- Constants 2 | return { 3 | -- common 4 | GREETING_SIZE = 128, 5 | GREETING_SALT_OFFSET = 64, 6 | GREETING_SALT_SIZE = 44, 7 | HEAD_BODY_LEN_SIZE = 5, 8 | REQUEST_PER_CONNECTION = 100000, 9 | MAX_LIMIT = 0xFFFFFFFF, 10 | 11 | -- default options 12 | HOST = '127.0.0.1', 13 | PORT = 3301, 14 | USER = false, 15 | PASSWORD = '', 16 | SOCKET_TIMEOUT = 5000, 17 | CONNECT_NOW = true, 18 | 19 | -- packet codes 20 | OK = 0, 21 | SELECT = 1, 22 | INSERT = 2, 23 | REPLACE = 3, 24 | UPDATE = 4, 25 | DELETE = 5, 26 | CALL = 6, 27 | AUTH = 7, 28 | EVAL = 8, 29 | UPSERT = 9, 30 | PING = 64, 31 | ERROR_TYPE = 65536, 32 | 33 | -- packet keys 34 | TYPE = 0x00, 35 | SYNC = 0x01, 36 | SPACE_ID = 0x10, 37 | INDEX_ID = 0x11, 38 | LIMIT = 0x12, 39 | OFFSET = 0x13, 40 | ITERATOR = 0x14, 41 | KEY = 0x20, 42 | TUPLE = 0x21, 43 | FUNCTION_NAME = 0x22, 44 | USER_NAME = 0x23, 45 | OPS = 0x28, 46 | DATA = 0x30, 47 | ERROR = 0x31, 48 | 49 | -- default spaces 50 | SPACE_SCHEMA = 272, 51 | SPACE_SPACE = 280, 52 | SPACE_INDEX = 288, 53 | SPACE_FUNC = 296, 54 | SPACE_USER = 304, 55 | SPACE_PRIV = 312, 56 | SPACE_CLUSTER = 320, 57 | 58 | -- default views 59 | VIEW_SPACE = 281, 60 | VIEW_INDEX = 289, 61 | 62 | -- index info 63 | INDEX_SPACE_PRIMARY = 0, 64 | INDEX_SPACE_NAME = 2, 65 | INDEX_INDEX_PRIMARY = 0, 66 | INDEX_INDEX_NAME = 2, 67 | } 68 | -------------------------------------------------------------------------------- /lua-tarantool-scm-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "lua-tarantool" 2 | version = "scm-1" 3 | 4 | source = { 5 | url = "git://github.com/tarantool/tarantool-lua.git", 6 | branch = "master" 7 | } 8 | 9 | description = { 10 | summary = "Pure Lua library for querying the tarantool NoSQL database", 11 | homepage = "https://github.com/tarantool/tarantool-lua", 12 | maintainer = "Conrad Steenberg ", 13 | license = "BSD 2-Clause" 14 | } 15 | 16 | dependencies = { 17 | "lua ~> 5.1", 18 | "lua-messagepack", 19 | "lua-resty-socket", 20 | "sha1" 21 | } 22 | 23 | build = { 24 | type = "builtin", 25 | modules = { 26 | ["tarantool"] = "tarantool.lua", 27 | ["const"] = "const.lua", 28 | }, 29 | install = { 30 | lua = { 31 | ["tarantool"] = "tarantool.lua", 32 | ["const"] = "const.lua", 33 | } 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /tarantool.lua: -------------------------------------------------------------------------------- 1 | local mp = require("MessagePack") 2 | local C = require("const") 3 | local string = string 4 | local table = table 5 | local ngx = ngx 6 | local type = type 7 | local ipairs = ipairs 8 | local error = error 9 | string = string 10 | local socket = nil 11 | local decode_base64 = nil 12 | local sha1_bin = nil 13 | if not ngx then 14 | socket = require("socket") 15 | socket.unix = require("socket.unix") 16 | local mime = require("mime") 17 | decode_base64 = mime.unb64 18 | sha1_bin = require("sha1").binary 19 | else 20 | socket = ngx.socket 21 | decode_base64 = ngx.decode_base64 22 | sha1_bin = ngx.sha1_bin 23 | end 24 | mp.set_integer('unsigned') 25 | local _prepare_request 26 | _prepare_request = function(h, b) 27 | local header = mp.pack(h) 28 | local body = mp.pack(b) 29 | local len = mp.pack(string.len(header) + string.len(body)) 30 | return len .. header .. body 31 | end 32 | local _xor 33 | _xor = function(str_a, str_b) 34 | local _bxor 35 | _bxor = function(a, b) 36 | local r = 0 37 | for i = 0, 31 do 38 | local x = a / 2 + b / 2 39 | if x ~= math.floor(x) then 40 | r = r + 2 ^ i 41 | end 42 | a = math.floor(a / 2) 43 | b = math.floor(b / 2) 44 | end 45 | return r 46 | end 47 | local result = '' 48 | if string.len(str_a) ~= string.len(str_b) then 49 | return 50 | end 51 | for i = 1, string.len(str_a) do 52 | result = result .. string.char(_bxor(string.byte(str_a, i), string.byte(str_b, i))) 53 | end 54 | return result 55 | end 56 | local _prepare_key 57 | _prepare_key = function(value) 58 | if type(value) == 'table' then 59 | return value 60 | elseif value == nil then 61 | return { } 62 | else 63 | return { 64 | value 65 | } 66 | end 67 | end 68 | local Tarantool 69 | do 70 | local _class_0 71 | local _base_0 = { 72 | enable_lookups = function(self) 73 | self._lookup_spaces = true 74 | self._lookup_indexes = true 75 | end, 76 | disable_lookups = function(self) 77 | self._lookup_spaces = false 78 | self._lookup_indexes = false 79 | self._spaces = { } 80 | self._indexes = { } 81 | end, 82 | _wraperr = function(self, err) 83 | if err then 84 | return err .. ', server: ' .. self.host .. ':' .. self.port 85 | else 86 | return "Internal error" 87 | end 88 | end, 89 | connect = function(self, host, port) 90 | if not self.sock then 91 | return nil, "No socket created" 92 | end 93 | self.host = host or self.host 94 | self.port = tonumber(port or self.port) 95 | local ok = nil 96 | local err = nil 97 | if string.find(self.host, 'unix:/') then 98 | if ngx then 99 | ok, err = self.sock:connect(self.host) 100 | else 101 | ok, err = self.unix:connect(string.match(self.host, 'unix:(.+)')) 102 | if ok then 103 | self.sock = self.unix 104 | end 105 | end 106 | else 107 | ok, err = self.sock:connect(self.host, self.port) 108 | end 109 | if not ok then 110 | return ok, self:_wraperr(err) 111 | end 112 | return self:_handshake() 113 | end, 114 | disconnect = function(self) 115 | if not self.sock then 116 | return nil, "no socket created" 117 | end 118 | return self.sock:close() 119 | end, 120 | set_keepalive = function(self) 121 | if not self.sock then 122 | return nil, "no socket created" 123 | end 124 | local ok, err = self.sock:setkeepalive() 125 | if not ok then 126 | self:disconnect() 127 | return nil, err 128 | end 129 | return ok 130 | end, 131 | select = function(self, space, index, key, opts) 132 | if opts == nil then 133 | opts = { } 134 | end 135 | local spaceno, err = self:_resolve_space(space) 136 | if not spaceno then 137 | return nil, err 138 | end 139 | local indexno 140 | indexno, err = self:_resolve_index(spaceno, index or "primary") 141 | if not indexno then 142 | return nil, err 143 | end 144 | local body = { 145 | [C.SPACE_ID] = spaceno, 146 | [C.INDEX_ID] = indexno, 147 | [C.KEY] = _prepare_key(key) 148 | } 149 | if opts.limit ~= nil then 150 | body[C.LIMIT] = tonumber(opts.limit) 151 | else 152 | body[C.LIMIT] = C.MAX_LIMIT 153 | end 154 | if opts.offset ~= nil then 155 | body[C.OFFSET] = tonumber(opts.offset) 156 | else 157 | body[C.OFFSET] = 0 158 | end 159 | if type(opts.iterator) == 'number' then 160 | body[C.ITERATOR] = opts.iterator 161 | end 162 | local response 163 | response, err = self:_request({ 164 | [C.TYPE] = C.SELECT 165 | }, body) 166 | if err then 167 | return nil, err 168 | elseif response and response.code ~= C.OK then 169 | return nil, self:_wraperr(response.error) 170 | else 171 | return response.data 172 | end 173 | end, 174 | insert = function(self, space, tuple) 175 | local spaceno, err = self:_resolve_space(space) 176 | if not spaceno then 177 | return nil, err 178 | end 179 | local response 180 | response, err = self:_request({ 181 | [C.TYPE] = C.INSERT 182 | }, { 183 | [C.SPACE_ID] = spaceno, 184 | [C.TUPLE] = tuple 185 | }) 186 | if err then 187 | return nil, err 188 | elseif response and response.code ~= C.OK then 189 | return nil, self:_wraperr(response.error) 190 | else 191 | return response.data 192 | end 193 | end, 194 | replace = function(self, space, tuple) 195 | local spaceno, err = self:_resolve_space(space) 196 | if not spaceno then 197 | return nil, err 198 | end 199 | local response 200 | response, err = self:_request({ 201 | [C.TYPE] = C.REPLACE 202 | }, { 203 | [C.SPACE_ID] = spaceno, 204 | [C.TUPLE] = tuple 205 | }) 206 | if err then 207 | return nil, err 208 | elseif response and response.code ~= C.OK then 209 | return nil, self:_wraperr(response.error) 210 | else 211 | return response.data 212 | end 213 | end, 214 | delete = function(self, space, key) 215 | local spaceno, err = self:_resolve_space(space) 216 | if not spaceno then 217 | return nil, err 218 | end 219 | local response 220 | response, err = self:_request({ 221 | [C.TYPE] = C.DELETE 222 | }, { 223 | [C.SPACE_ID] = spaceno, 224 | [C.KEY] = _prepare_key(key) 225 | }) 226 | if err then 227 | return nil, err 228 | elseif response and response.code ~= C.OK then 229 | return nil, self:_wraperr(response.error) 230 | else 231 | return response.data 232 | end 233 | end, 234 | update = function(self, space, index, key, oplist) 235 | local spaceno, err = self:_resolve_space(space) 236 | if not spaceno then 237 | return nil, err 238 | end 239 | local indexno 240 | indexno, err = self:_resolve_index(spaceno, index) 241 | if not indexno then 242 | return nil, err 243 | end 244 | local response 245 | response, err = self:_request({ 246 | [C.TYPE] = C.UPDATE 247 | }, { 248 | [C.SPACE_ID] = spaceno, 249 | [C.INDEX_ID] = indexno, 250 | [C.KEY] = _prepare_key(key), 251 | [C.TUPLE] = oplist 252 | }) 253 | if err then 254 | return nil, err 255 | elseif response and response.code ~= C.OK then 256 | return nil, self:_wraperr(response.error) 257 | else 258 | return response.data 259 | end 260 | end, 261 | upsert = function(self, space, tuple, oplist) 262 | local spaceno, err = self:_resolve_space(space) 263 | if not spaceno then 264 | return nil, err 265 | end 266 | local response 267 | response, err = self:_request({ 268 | [C.TYPE] = C.UPSERT 269 | }, { 270 | [C.SPACE_ID] = spaceno, 271 | [C.TUPLE] = tuple, 272 | [C.OPS] = oplist 273 | }) 274 | if err then 275 | return nil, err 276 | elseif response and response.code ~= C.OK then 277 | return nil, self:_wraperr(response.error) 278 | else 279 | return response.data 280 | end 281 | end, 282 | ping = function(self) 283 | local response, err = self:_request({ 284 | [C.TYPE] = C.PING 285 | }, { }) 286 | if err then 287 | return nil, err 288 | elseif response and response.code ~= C.OK then 289 | return nil, self:_wraperr(response.error) 290 | else 291 | return "PONG" 292 | end 293 | end, 294 | call = function(self, proc, args) 295 | local response, err = self:_request({ 296 | [C.TYPE] = C.CALL 297 | }, { 298 | [C.FUNCTION_NAME] = proc, 299 | [C.TUPLE] = args 300 | }) 301 | if err then 302 | return nil, err 303 | elseif response and response.code ~= C.OK then 304 | return nil, self:_wraperr(response.error) 305 | else 306 | return unpack(response.data) 307 | end 308 | end, 309 | _resolve_space = function(self, space) 310 | if type(space) == 'number' then 311 | return space 312 | elseif type(space) == 'string' then 313 | if self._lookup_spaces and self._spaces[space] then 314 | return self._spaces[space] 315 | end 316 | else 317 | return nil, 'Invalid space identificator: ' .. space 318 | end 319 | local data, err = self:select(C.VIEW_SPACE, C.INDEX_SPACE_NAME, space) 320 | if not data or not data[1] or not data[1][1] or err then 321 | return nil, (err or 'Can\'t find space with identifier: ' .. space) 322 | end 323 | local newspace = data[1][1] 324 | if self._lookup_spaces then 325 | self._spaces[space] = newspace 326 | end 327 | return newspace 328 | end, 329 | _resolve_index = function(self, space, index) 330 | if type(index) == 'number' then 331 | return index 332 | elseif type(index) == 'string' then 333 | if self.lookup_indexes and self._indexes[index] then 334 | return self._indexes[index] 335 | end 336 | else 337 | return nil, 'Invalid index identifier: ' .. index 338 | end 339 | local spaceno, err = self:_resolve_space(space) 340 | if not spaceno then 341 | return nil, err 342 | end 343 | local data 344 | data, err = self:select(C.VIEW_INDEX, C.INDEX_INDEX_NAME, { 345 | spaceno, 346 | index 347 | }) 348 | if not data or not data[1] or not data[1][2] or err then 349 | return nil, (err or 'Can\'t find index with identifier: ' .. index) 350 | end 351 | local newindex = data[1][2] 352 | if self._lookup_indexes then 353 | self._indexes[index] = newindex 354 | end 355 | return newindex 356 | end, 357 | _handshake = function(self) 358 | local greeting = nil 359 | local greeting_err = nil 360 | if not self._salt then 361 | greeting, greeting_err = self.sock:receive(C.GREETING_SIZE) 362 | if not greeting or greeting_err then 363 | self.sock:close() 364 | return nil, self:_wraperr(greeting_err) 365 | end 366 | self._salt = string.sub(greeting, C.GREETING_SALT_OFFSET + 1) 367 | self._salt = string.sub(decode_base64(self._salt), 1, 20) 368 | local err 369 | self.authenticated, err = self:_authenticate() 370 | return self.authenticated, err 371 | end 372 | return true 373 | end, 374 | _authenticate = function(self) 375 | if not self.user then 376 | return true 377 | end 378 | local rbody = { 379 | [C.USER_NAME] = self.user, 380 | [C.TUPLE] = { } 381 | } 382 | local password = self.password or '' 383 | if password ~= '' then 384 | local step_1 = sha1_bin(self.password) 385 | local step_2 = sha1_bin(step_1) 386 | local step_3 = sha1_bin(self._salt .. step_2) 387 | local scramble = _xor(step_1, step_3) 388 | rbody[C.TUPLE] = { 389 | "chap-sha1", 390 | scramble 391 | } 392 | end 393 | local response, err = self:_request({ 394 | [C.TYPE] = C.AUTH 395 | }, rbody) 396 | if err then 397 | return nil, err 398 | elseif response and response.code ~= C.OK then 399 | return nil, self:_wraperr(response.error) 400 | else 401 | return true 402 | end 403 | end, 404 | _request = function(self, header, body) 405 | local sock = self.sock 406 | if type(header) ~= 'table' then 407 | return nil, 'invlid request header' 408 | end 409 | self.sync_num = ((self.sync_num or 0) + 1) % C.REQUEST_PER_CONNECTION 410 | if not header[C.SYNC] then 411 | header[C.SYNC] = self.sync_num 412 | else 413 | self.sync_num = header[C.SYNC] 414 | end 415 | local request = _prepare_request(header, body) 416 | local bytes, err = sock:send(request) 417 | if bytes == nil then 418 | sock:close() 419 | return nil, self:_wraperr("Failed to send request: " .. err) 420 | end 421 | local size 422 | size, err = sock:receive(C.HEAD_BODY_LEN_SIZE) 423 | if not size then 424 | sock:close() 425 | return nil, self:_wraperr("Failed to get response size: " .. err) 426 | end 427 | size = mp.unpack(size) 428 | if not size then 429 | sock:close() 430 | return nil, self:_wraperr("Client get response invalid size") 431 | end 432 | local header_and_body 433 | header_and_body, err = sock:receive(size) 434 | if not header_and_body then 435 | sock:close() 436 | return nil, self:_wraperr("Failed to get response header and body: " .. err) 437 | end 438 | local iterator = mp.unpacker(header_and_body) 439 | local value, res_header = iterator() 440 | if type(res_header) ~= 'table' then 441 | return nil, self:_wraperr("Invalid header: " .. type(res_header) .. " (table expected)") 442 | end 443 | if res_header[C.SYNC] ~= self.sync_num then 444 | return nil, self:_wraperr("Invalid header SYNC: request: " .. self.sync_num .. " response: " .. res_header[C.SYNC]) 445 | end 446 | local res_body 447 | value, res_body = iterator() 448 | if type(res_body) ~= 'table' then 449 | res_body = { } 450 | end 451 | return { 452 | code = res_header[C.TYPE], 453 | data = res_body[C.DATA], 454 | error = res_body[C.ERROR] 455 | } 456 | end 457 | } 458 | _base_0.__index = _base_0 459 | _class_0 = setmetatable({ 460 | __init = function(self, params) 461 | self.meta = { 462 | host = C.HOST, 463 | port = C.PORT, 464 | user = C.USER, 465 | password = C.PASSWORD, 466 | socket_timeout = C.SOCKET_TIMEOUT, 467 | connect_now = C.CONNECT_NOW, 468 | _lookup_spaces = true, 469 | _lookup_indexes = true, 470 | _spaces = { }, 471 | _indexes = { } 472 | } 473 | if params and type(params) == 'table' then 474 | for key, value in pairs(self.meta) do 475 | if params[key] ~= nil then 476 | self.meta[key] = params[key] 477 | end 478 | self[key] = self.meta[key] 479 | end 480 | end 481 | local sock, err = socket.tcp() 482 | if not sock then 483 | self.err = err 484 | return 485 | end 486 | if self.socket_timeout then 487 | sock:settimeout(self.socket_timeout) 488 | end 489 | self.sock = sock 490 | if not ngx then 491 | self.unix = socket.unix() 492 | end 493 | if self.connect_now then 494 | local ok 495 | ok, err = self:connect() 496 | if not ok then 497 | print(err) 498 | self.err = err 499 | end 500 | end 501 | end, 502 | __base = _base_0, 503 | __name = "Tarantool" 504 | }, { 505 | __index = _base_0, 506 | __call = function(cls, ...) 507 | local _self_0 = setmetatable({}, _base_0) 508 | cls.__init(_self_0, ...) 509 | return _self_0 510 | end 511 | }) 512 | _base_0.__class = _class_0 513 | Tarantool = _class_0 514 | return _class_0 515 | end 516 | -------------------------------------------------------------------------------- /tarantool.moon: -------------------------------------------------------------------------------- 1 | mp = require "MessagePack" 2 | C = require "const" 3 | string = string 4 | table = table 5 | ngx = ngx 6 | type = type 7 | ipairs = ipairs 8 | error = error 9 | string = string 10 | socket = nil 11 | decode_base64 = nil 12 | sha1_bin = nil 13 | 14 | -- Use non NGINX modules 15 | -- requires: luasock (implicit), lua-resty-socket, sha1 16 | 17 | if not ngx then 18 | socket = require("socket") 19 | socket.unix = require("socket.unix") 20 | mime = require("mime") 21 | decode_base64 = mime.unb64 22 | sha1_bin = require("sha1").binary 23 | else 24 | socket = ngx.socket 25 | decode_base64 = ngx.decode_base64 26 | sha1_bin = ngx.sha1_bin 27 | 28 | mp.set_integer('unsigned') 29 | 30 | _prepare_request = (h, b) -> 31 | header = mp.pack(h) 32 | body = mp.pack(b) 33 | len = mp.pack(string.len(header) + string.len(body)) 34 | len .. header .. body 35 | 36 | _xor = (str_a, str_b) -> 37 | _bxor = (a, b) -> 38 | r = 0 39 | for i = 0, 31 do 40 | x = a / 2 + b / 2 41 | if x ~= math.floor(x) 42 | r = r + 2^i 43 | a = math.floor(a / 2) 44 | b = math.floor(b / 2) 45 | return r 46 | result = '' 47 | if string.len(str_a) != string.len(str_b) then 48 | return 49 | for i = 1, string.len(str_a) do 50 | result = result .. string.char(_bxor(string.byte(str_a, i), string.byte(str_b, i))) 51 | result 52 | 53 | _prepare_key = (value) -> 54 | if type(value) == 'table' 55 | return value 56 | elseif value == nil 57 | return { } 58 | else 59 | return { value } 60 | 61 | class Tarantool 62 | new: (params) => 63 | @meta = { 64 | host: C.HOST, 65 | port: C.PORT, 66 | user: C.USER, 67 | password: C.PASSWORD, 68 | socket_timeout: C.SOCKET_TIMEOUT, 69 | connect_now: C.CONNECT_NOW 70 | _lookup_spaces: true 71 | _lookup_indexes: true 72 | _spaces: {} 73 | _indexes: {} 74 | } 75 | 76 | if params and type(params) == 'table' 77 | for key, value in pairs(@meta) do 78 | if params[key] != nil then 79 | @meta[key] = params[key] 80 | self[key] = @meta[key] 81 | 82 | sock, err = socket.tcp() 83 | if not sock 84 | @err = err 85 | return 86 | 87 | if @socket_timeout 88 | sock\settimeout(@socket_timeout) 89 | @sock = sock 90 | 91 | if not ngx 92 | @unix = socket.unix() 93 | 94 | if @connect_now 95 | ok, err = @connect() 96 | if not ok 97 | print(err) 98 | @err = err 99 | 100 | enable_lookups: () => 101 | @_lookup_spaces = true 102 | @_lookup_indexes = true 103 | 104 | disable_lookups: () => 105 | @_lookup_spaces = false 106 | @_lookup_indexes = false 107 | @_spaces = {} 108 | @_indexes = {} 109 | 110 | _wraperr: (err) => 111 | if err then 112 | err .. ', server: ' .. @host .. ':' .. @port 113 | else 114 | "Internal error" 115 | 116 | connect: (host, port) => 117 | if not @sock 118 | return nil, "No socket created" 119 | 120 | @host = host or @host 121 | @port = tonumber(port or @port) 122 | 123 | ok = nil 124 | err = nil 125 | if string.find(@host, 'unix:/') 126 | if ngx 127 | ok, err = @sock\connect(@host) 128 | else 129 | ok, err = @unix\connect(string.match(@host, 'unix:(.+)')) 130 | if ok 131 | @sock = @unix 132 | else 133 | ok, err = @sock\connect(@host, @port) 134 | 135 | if not ok then 136 | return ok, @_wraperr(err) 137 | return @_handshake() 138 | 139 | disconnect: () => 140 | if not @sock 141 | return nil, "no socket created" 142 | return @sock\close() 143 | 144 | set_keepalive: () => 145 | if not @sock 146 | return nil, "no socket created" 147 | ok, err = @sock\setkeepalive() 148 | if not ok then 149 | @disconnect() 150 | return nil, err 151 | return ok 152 | 153 | select: (space, index, key, opts) => 154 | if opts == nil 155 | opts = {} 156 | 157 | spaceno, err = @_resolve_space(space) 158 | if not spaceno 159 | return nil, err 160 | 161 | indexno, err = @_resolve_index(spaceno, index or "primary") 162 | if not indexno 163 | return nil, err 164 | 165 | body = { 166 | [C.SPACE_ID]: spaceno, 167 | [C.INDEX_ID]: indexno, 168 | [C.KEY]: _prepare_key(key) 169 | } 170 | 171 | if opts.limit != nil 172 | body[C.LIMIT] = tonumber(opts.limit) 173 | else 174 | body[C.LIMIT] = C.MAX_LIMIT 175 | if opts.offset != nil then 176 | body[C.OFFSET] = tonumber(opts.offset) 177 | else 178 | body[C.OFFSET] = 0 179 | 180 | if type(opts.iterator) == 'number' then 181 | body[C.ITERATOR] = opts.iterator 182 | 183 | response, err = @_request({ [ C.TYPE ]: C.SELECT }, body ) 184 | if err 185 | return nil, err 186 | elseif response and response.code != C.OK 187 | return nil, @_wraperr(response.error) 188 | else 189 | return response.data 190 | 191 | insert: (space, tuple) => 192 | spaceno, err = @_resolve_space(space) 193 | if not spaceno 194 | return nil, err 195 | 196 | response, err = @_request({ [C.TYPE]: C.INSERT }, { [C.SPACE_ID]: spaceno, [C.TUPLE]: tuple }) 197 | if err 198 | return nil, err 199 | elseif response and response.code != C.OK 200 | return nil, @_wraperr(response.error) 201 | else 202 | return response.data 203 | 204 | replace: (space, tuple) => 205 | spaceno, err = @_resolve_space(space) 206 | if not spaceno 207 | return nil, err 208 | 209 | response, err = @_request({ [C.TYPE]: C.REPLACE }, { [C.SPACE_ID]: spaceno, [C.TUPLE]: tuple }) 210 | if err 211 | return nil, err 212 | elseif response and response.code != C.OK then 213 | return nil, @_wraperr(response.error) 214 | else 215 | return response.data 216 | 217 | delete: (space, key) => 218 | spaceno, err = @_resolve_space(space) 219 | if not spaceno 220 | return nil, err 221 | 222 | response, err = @_request({ [C.TYPE]: C.DELETE }, { [C.SPACE_ID]: spaceno, [C.KEY]: _prepare_key(key) }) 223 | if err 224 | return nil, err 225 | elseif response and response.code != C.OK 226 | return nil, @_wraperr(response.error) 227 | else 228 | return response.data 229 | 230 | update: (space, index, key, oplist) => 231 | spaceno, err = @_resolve_space(space) 232 | if not spaceno 233 | return nil, err 234 | 235 | indexno, err = @_resolve_index(spaceno, index) 236 | if not indexno 237 | return nil, err 238 | 239 | response, err = @_request({ [C.TYPE]: C.UPDATE }, { 240 | [C.SPACE_ID]: spaceno, 241 | [C.INDEX_ID]: indexno, 242 | [C.KEY]: _prepare_key(key), 243 | [C.TUPLE]: oplist, 244 | }) 245 | if err 246 | return nil, err 247 | elseif response and response.code != C.OK 248 | return nil, @_wraperr(response.error) 249 | else 250 | return response.data 251 | 252 | upsert: (space, tuple, oplist) => 253 | spaceno, err = @_resolve_space(space) 254 | if not spaceno 255 | return nil, err 256 | 257 | response, err = @_request({ [C.TYPE]: C.UPSERT }, { 258 | [C.SPACE_ID]: spaceno, 259 | [C.TUPLE]: tuple, 260 | [C.OPS]: oplist, 261 | }) 262 | 263 | if err 264 | return nil, err 265 | elseif response and response.code != C.OK 266 | return nil, @_wraperr(response.error) 267 | else 268 | return response.data 269 | 270 | ping: () => 271 | response, err = @_request({ [ C.TYPE ]: C.PING }, {} ) 272 | if err 273 | return nil, err 274 | elseif response and response.code != C.OK 275 | return nil, @_wraperr(response.error) 276 | else 277 | return "PONG" 278 | 279 | call: (proc, args) => 280 | response, err = @_request({ [ C.TYPE ]: C.CALL }, { [C.FUNCTION_NAME]: proc, [C.TUPLE]: args } ) 281 | if err 282 | return nil, err 283 | elseif response and response.code != C.OK 284 | return nil, @_wraperr(response.error) 285 | else 286 | return unpack(response.data) 287 | 288 | _resolve_space: (space) => 289 | if type(space) == 'number' then 290 | return space 291 | elseif type(space) == 'string' then 292 | if @_lookup_spaces and @_spaces[space] then 293 | return @_spaces[space] 294 | else 295 | return nil, 'Invalid space identificator: ' .. space 296 | 297 | data, err = @select(C.VIEW_SPACE, C.INDEX_SPACE_NAME, space) 298 | if not data or not data[1] or not data[1][1] or err then 299 | return nil, (err or 'Can\'t find space with identifier: ' .. space) 300 | 301 | newspace = data[1][1] 302 | if @_lookup_spaces 303 | @_spaces[space] = newspace 304 | return newspace 305 | 306 | _resolve_index: (space, index) => 307 | if type(index) == 'number' then 308 | return index 309 | elseif type(index) == 'string' 310 | if @lookup_indexes and @_indexes[index] 311 | return @_indexes[index] 312 | else 313 | return nil, 'Invalid index identifier: ' .. index 314 | 315 | spaceno, err = @_resolve_space(space) 316 | if not spaceno 317 | return nil, err 318 | 319 | data, err = @select(C.VIEW_INDEX, C.INDEX_INDEX_NAME, { spaceno, index }) 320 | if not data or not data[1] or not data[1][2] or err 321 | return nil, (err or 'Can\'t find index with identifier: ' .. index) 322 | 323 | newindex = data[1][2] 324 | if @_lookup_indexes 325 | @_indexes[index] = newindex 326 | return newindex 327 | 328 | _handshake: () => 329 | greeting = nil 330 | greeting_err = nil 331 | if not @_salt 332 | greeting, greeting_err = @sock\receive(C.GREETING_SIZE) 333 | if not greeting or greeting_err 334 | @sock\close() 335 | return nil, @_wraperr(greeting_err) 336 | 337 | @_salt = string.sub(greeting, C.GREETING_SALT_OFFSET + 1) 338 | @_salt = string.sub(decode_base64(@_salt), 1, 20) 339 | @authenticated, err = @_authenticate() 340 | return @authenticated, err 341 | return true 342 | 343 | _authenticate: () => 344 | if not @user then 345 | return true 346 | 347 | rbody = { [C.USER_NAME]: @user, [C.TUPLE]: { } } 348 | 349 | password = @password or '' 350 | if password != '' 351 | step_1 = sha1_bin(@password) 352 | step_2 = sha1_bin(step_1) 353 | step_3 = sha1_bin(@_salt .. step_2) 354 | scramble = _xor(step_1, step_3) 355 | rbody[C.TUPLE] = { "chap-sha1", scramble } 356 | 357 | response, err = @_request({ [C.TYPE]: C.AUTH }, rbody) 358 | if err 359 | return nil, err 360 | elseif response and response.code != C.OK 361 | return nil, @_wraperr(response.error) 362 | else 363 | return true 364 | 365 | _request: (header, body) => 366 | sock = @sock 367 | 368 | if type(header) != 'table' 369 | return nil, 'invlid request header' 370 | 371 | @sync_num = ((@sync_num or 0) + 1) % C.REQUEST_PER_CONNECTION 372 | if not header[C.SYNC] 373 | header[C.SYNC] = @sync_num 374 | else 375 | @sync_num = header[C.SYNC] 376 | 377 | request = _prepare_request(header, body) 378 | bytes, err = sock\send(request) 379 | 380 | if bytes == nil then 381 | sock\close() 382 | return nil, @_wraperr("Failed to send request: " .. err) 383 | 384 | size, err = sock\receive(C.HEAD_BODY_LEN_SIZE) 385 | if not size 386 | sock\close() 387 | return nil, @_wraperr("Failed to get response size: " .. err) 388 | 389 | size = mp.unpack(size) 390 | if not size 391 | sock\close() 392 | return nil, @_wraperr("Client get response invalid size") 393 | 394 | header_and_body, err = sock\receive(size) 395 | if not header_and_body 396 | sock\close() 397 | return nil, @_wraperr("Failed to get response header and body: " .. err) 398 | 399 | iterator = mp.unpacker(header_and_body) 400 | value, res_header = iterator() 401 | if type(res_header) != 'table' then 402 | return nil, @_wraperr("Invalid header: " .. type(res_header) .. " (table expected)") 403 | 404 | if res_header[C.SYNC] != @sync_num then 405 | return nil, @_wraperr("Invalid header SYNC: request: " .. @sync_num .. " response: " .. res_header[C.SYNC]) 406 | 407 | value, res_body = iterator() 408 | if type(res_body) != 'table' 409 | res_body = {} 410 | 411 | return { code: res_header[C.TYPE], data: res_body[C.DATA], error: res_body[C.ERROR] } 412 | 413 | --------------------------------------------------------------------------------