diff --git a/README.md b/README.md index 081a390..d1d6e4f 100644 --- a/README.md +++ b/README.md @@ -2,21 +2,20 @@ ![test](https://github.com/leafo/pgmoon/workflows/test/badge.svg) -> **Note!** Are you using the latest version of OpenResty? You must update to -> pgmoon 1.12 or above, due to a change in Lua pattern compatibility, any query -> that returns affected number of rows will return the expected value. +> **Note:** Have you updated from an older version of OpenResty? You must update to +> pgmoon 1.12 or above, due to a change in Lua pattern compatibility to avoid incorrect +> results from queries that return affected rows. -pgmoon is a PostgreSQL client library written in pure Lua (MoonScript). +**pgmoon** is a PostgreSQL client library written in pure Lua (MoonScript). -pgmoon was originally designed for use in [OpenResty][5] to take advantage of -the [cosocket api][4] to provide asynchronous queries but it also works in the -regular Lua environment as well using [LuaSocket][1] (and optionally -[luaossl](https://luarocks.org/modules/daurnimator/luaossl) or [LuaCrypto][2] for MD5 authentication and [LuaSec][6] for SSL connections). -pgmoon can also use [cqueues][]' socket when passed `"cqueues"` as the socket -type when instantiating. +**pgmoon** was originally designed for use in [OpenResty][] to take advantage +of the [cosocket +api](https://github.com/openresty/lua-nginx-module#ngxsockettcp) to provide +asynchronous queries but it also works in the regular any Lua environment where +[LuaSocket][] or [cqueues][] is available. It's a perfect candidate for running your queries both inside OpenResty's -environment and on the command line (eg. tests) in web frameworks like [Lapis][3]. +environment and on the command line (eg. tests) in web frameworks like [Lapis][]. ## Install @@ -24,19 +23,32 @@ environment and on the command line (eg. tests) in web frameworks like [Lapis][3 $ luarocks install pgmoon ``` +
+Using OpenResty's OPM + +```bash +$ opm get leafo/pgmoon +``` + +
+ + ### Dependencies pgmoon supports a wide range of environments and libraries, so it may be necessary to install additional dependencies depending on how you intend to communicate with the database: -> Tip: If you're using OpenResty then no additional dependencies are needed +> **Tip:** If you're using OpenResty then no additional dependencies are needed +> (generally, a crypto library may be necessary for some authentication +> methods) -Some socket library **is required**, depending on the environment you can chose one: +A socket implementation **is required** to use pgmoon, depending on the +environment you can chose one: -* [OpenResty](https://openresty.org/en/) — The built in socket is used, so additional dependencies necessary -* [LuaSocket](http://w3.impa.br/~diego/software/luasocket/) — Suitable for command line database access, has the highest platform compatibility `luarocks install luasocket` -* [cqueues](https://github.com/wahern/cqueues) — `luarocks install cqueues` +* [OpenResty][] — The built in socket is used, no additional dependencies necessary +* [LuaSocket][] — `luarocks install luasocket` +* [cqueues][] — `luarocks install cqueues` If you're on PUC Lua 5.1 or 5.2 then you will need a bit libray (not needed for LuaJIT): @@ -50,19 +62,21 @@ If you want to use JSON types you will need lua-cjson $ luarocks install lua-cjson ``` -If you want to use SSL connections with LuaSocket then you will need LuaSec: -(OpenResty and cqueues come with their own SSL implementations) +SSL connections may require an additional dependency: + +* OpenResty — No additional dependencies required +* LuaSocket — `luarocks install luasec` +* cqueues — `luarocks install luaossl` +Password authentication may require a crypto library, [luaossl][]. ```bash -$ luarocks install luasec +$ luarocks install luaossl ``` -If you want to use password authentication then you will need a crypto library: +> **Note:** [LuaCrypto][] can be used as a fallback, but the library is abandoned and not recommended for use -* [OpenResty](https://openresty.org/en/) — Built in function will be used, no additional dependencies necessary -* [luaossl](https://github.com/wahern/luaossl) — Recommended `luarocks install luaossl` -* [luacrypto](https://github.com/starius/luacrypto) — Deprecated library, not recommended +> **Note:** Use within [OpenResty][] will prioritize built in functions if possible ## Example @@ -81,8 +95,8 @@ local res = assert(pg:query("select * from users where username = " .. pg:escape_literal("leafo"))) ``` -If you are using OpenResty you should relinquish the socket after you are done -with it so it can be reused in future requests: +If you are using OpenResty you can relinquish the socket to the connection pool +after you are done with it so it can be reused in future requests: ```lua pg:keepalive() @@ -94,19 +108,27 @@ Functions in table returned by `require("pgmoon")`: ### `new(options={})` -Creates a new `Postgres` object. Does not connect automatically. Takes a table -of options. The table can have the following keys: +Creates a new `Postgres` object from a configuration object. All fields are +optional unless otherwise stated. The newly created object will not +automatically connect, you must call `conect` after creating the object. +Available options: + +* `"database"`: the database name to connect to **required** * `"host"`: the host to connect to (default: `"127.0.0.1"`) * `"port"`: the port to connect to (default: `"5432"`) * `"user"`: the database username to authenticate (default: `"postgres"`) -* `"database"`: the database name to connect to **required** -* `"password"`: password for authentication, optional depending on server configuration +* `"password"`: password for authentication, may be required depending on server configuration * `"ssl"`: enable ssl (default: `false`) * `"ssl_verify"`: verify server certificate (default: `nil`) * `"ssl_required"`: abort the connection if the server does not support SSL connections (default: `nil`) -* `"pool"`: optional name of pool to use when using OpenResty cosocket (defaults to `"#{host}:#{port}:#{database}"`) -* `"socket_type"`: optional, the type of socket to use, one of: `"nginx"`, `"luasocket"`, `cqueues` (default: `"nginx"` if in nginx, `"luasocket"` otherwise) +* `"socket_type"`: the type of socket to use, one of: `"nginx"`, `"luasocket"`, `cqueues` (default: `"nginx"` if in nginx, `"luasocket"` otherwise) +* `"application_name"`: set the name of the connection as displayed in `pg_stat_activity`. (default: `"pgmoon"`) +* `"pool"`: (OpenResty only) name of pool to use when using OpenResty cosocket (default: `"#{host}:#{port}:#{database}"`) +* `"pool_size"`: (OpenResty only) Passed directly to OpenResty cosocket connect function, [see docs](https://github.com/openresty/lua-nginx-module#tcpsockconnect) +* `"backlog"`: (OpenResty only) Passed directly to OpenResty cosocket connect function, [see docs](https://github.com/openresty/lua-nginx-module#tcpsockconnect) +* `"cqueues_openssl_context"`: Manually created `opensssl.ssl.context` to use when created cqueues SSL connections +* `"luasec_opts"`: Manually created options object to use when using LuaSec SSL connections Methods on the `Postgres` object returned by `new`: @@ -119,9 +141,8 @@ message. ### postgres:settimeout(time) -Sets the timeout value (in milliseconds) for all socket operations (connect, -write, receive). This function does not have any return values. - +Sets the timeout value (in milliseconds) for all subsequent socket operations +(connect, write, receive). This function does not have any return values. ### success, err = postgres:disconnect() @@ -252,9 +273,9 @@ Returns string representation of current state of `Postgres` object. ## SSL connections pgmoon can establish an SSL connection to a Postgres server. It can also refuse -to connect to it if the server does not support SSL. -Just as pgmoon depends on LuaSocket for usage outside of OpenResty, it depends -on LuaSec for SSL connections in such contexts. +to connect to it if the server does not support SSL. Just as pgmoon depends on +LuaSocket for usage outside of OpenResty, it depends on luaossl/LuaSec for SSL +connections in such contexts. ```lua local pgmoon = require("pgmoon") @@ -272,13 +293,13 @@ local pg = pgmoon.new({ assert(pg:connect()) ``` -> Note: In Postgres 12 and above, the minium SSL version accepted by client -> connections is 1.2. When using LuaSec to connect to an SSL server, if you -> don't specify an `ssl_version` then `tlsv1_2` is used. +> **Note:** In Postgres 12 and above, the minium SSL version accepted by client +> connections is 1.2. When using LuaSocket + LuaSec to connect to an SSL +> server, if you don't specify an `ssl_version` then `tlsv1_2` is used. -In OpenResty, make sure to configure the [lua_ssl_trusted_certificate][7] -directive if you wish to verify the server certificate, as the LuaSec-only -options become irrelevant in that case. +In OpenResty, make sure to configure the +[lua_ssl_trusted_certificate](https://github.com/openresty/lua-nginx-module#lua_ssl_trusted_certificate) +directive if you wish to verify the server certificate. ## Authentication types @@ -319,6 +340,22 @@ local my_array = {1,2,3,4,5} pg:query("insert into some_table (some_arr_col) values(" .. encode_array(my_array) .. ")") ``` +### Empty Arrays + +When trying to encode an empty array an error will be thrown. Postgres requires +a type when using an array. When there are values in the array Postgres can +infer the type, but with no values in the array no type can be inferred. This +is illustrated in the erorr provided by Postgres: + + +``` +postgres=# select ARRAY[]; +ERROR: cannot determine type of empty array +LINE 1: select ARRAY[]; + ^ +HINT: Explicitly cast to the desired type, for example ARRAY[]::integer[]. +``` + ## Handling JSON `json` and `jsonb` types are automatically decoded when they are returned from @@ -397,6 +434,7 @@ Homepage: # Changelog +* 1.13.0 — 2021-10-13 - Add support for scram_sha_256_auth (@murillopaula), 'backlog' and 'pool_size' options while using ngx.socket (@xiaocang), update LuaSec ssl_protocol default options (@jeremymv2), `application_name` option (@mecampbellsoup) * 1.12.0 — 2021-01-06 - Lua pattern compatibility fix, Support for Lua 5.1 through 5.4 (@jprjr). Fix bug where SSL vesrion was not being passed. Default to TLS v1.2 when using LuaSec. Luabitop is no longer automatically installed as a dependency. New test suite. * 1.11.0 — 2020-03-26 - Allow for TLS v1.2 when using LuaSec (Miles Elam) * 1.10.0 — 2019-04-15 - Support luaossl for crypto functions, added better error when missing crypto library @@ -434,12 +472,10 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - [1]: http://w3.impa.br/~diego/software/luasocket/ - [2]: http://mkottman.github.io/luacrypto/ - [3]: http://leafo.net/lapis - [4]: http://wiki.nginx.org/HttpLuaModule#ngx.socket.tcp - [5]: http://openresty.org/ - [6]: https://github.com/brunoos/luasec - [7]: https://github.com/openresty/lua-nginx-module#lua_ssl_trusted_certificate + [luaossl]: https://github.com/wahern/luaossl + [LuaCrypto]: https://luarocks.org/modules/starius/luacrypto + [LuaSec]: https://github.com/brunoos/luasec + [Lapis]: http://leafo.net/lapis + [OpenResty]: https://openresty.org/ + [LuaSocket]: http://w3.impa.br/~diego/software/luasocket/ [cqueues]: http://25thandclement.com/~william/projects/cqueues.html diff --git a/Tupfile b/Tupfile deleted file mode 100644 index f0fe651..0000000 --- a/Tupfile +++ /dev/null @@ -1 +0,0 @@ -include_rules diff --git a/Tuprules.tup b/Tuprules.tup deleted file mode 100644 index 5fa92eb..0000000 --- a/Tuprules.tup +++ /dev/null @@ -1 +0,0 @@ -: foreach *.moon |> moonc %f |> %B.lua diff --git a/pgmoon-dev-1.rockspec b/pgmoon-dev-1.rockspec index f672764..f6a1aea 100644 --- a/pgmoon-dev-1.rockspec +++ b/pgmoon-dev-1.rockspec @@ -2,7 +2,8 @@ package = "pgmoon" version = "dev-1" source = { - url = "git://github.com/leafo/pgmoon.git" + url = "git://github.com/kriscode1/pgmoon.git", + tag = "luasocket-to-haproxy" } description = { diff --git a/pgmoon/arrays.lua b/pgmoon/arrays.lua index 15df2e9..882c843 100644 --- a/pgmoon/arrays.lua +++ b/pgmoon/arrays.lua @@ -22,6 +22,11 @@ getmetatable(PostgresArray).__call = function(self, t) return setmetatable(t, self.__base) end local default_escape_literal = nil +local insert, concat +do + local _obj_0 = table + insert, concat = _obj_0.insert, _obj_0.concat +end local encode_array do local append_buffer @@ -29,13 +34,13 @@ do for _index_0 = 1, #values do local item = values[_index_0] if type(item) == "table" and not getmetatable(item) then - table.insert(buffer, "[") + insert(buffer, "[") append_buffer(escape_literal, buffer, item) buffer[#buffer] = "]" - table.insert(buffer, ",") + insert(buffer, ",") else - table.insert(buffer, escape_literal(item)) - table.insert(buffer, ",") + insert(buffer, escape_literal(item)) + insert(buffer, ",") end end return buffer @@ -53,8 +58,12 @@ do local buffer = append_buffer(escape_literal, { "ARRAY[" }, tbl) - buffer[#buffer] = "]" - return table.concat(buffer) + if buffer[#buffer] == "," then + buffer[#buffer] = "]" + else + insert(buffer, "]") + end + return concat(buffer) end end local convert_values diff --git a/pgmoon/arrays.moon b/pgmoon/arrays.moon index 7bb2d5a..100dfe0 100644 --- a/pgmoon/arrays.moon +++ b/pgmoon/arrays.moon @@ -6,18 +6,20 @@ getmetatable(PostgresArray).__call = (t) => default_escape_literal = nil +import insert, concat from table + encode_array = do append_buffer = (escape_literal, buffer, values) -> for item in *values -- plain array if type(item) == "table" and not getmetatable(item) - table.insert buffer, "[" + insert buffer, "[" append_buffer escape_literal, buffer, item buffer[#buffer] = "]" -- strips trailing comma - table.insert buffer, "," + insert buffer, "," else - table.insert buffer, escape_literal item - table.insert buffer, "," + insert buffer, escape_literal item + insert buffer, "," buffer @@ -33,8 +35,12 @@ encode_array = do buffer = append_buffer escape_literal, {"ARRAY["}, tbl - buffer[#buffer] = "]" -- strips trailing comma - table.concat buffer + + if buffer[#buffer] == "," + buffer[#buffer] = "]" + else + insert buffer, "]" + concat buffer convert_values = (array, fn) -> for idx, v in ipairs array diff --git a/pgmoon/bit.lua b/pgmoon/bit.lua index 102f944..1b99ceb 100644 --- a/pgmoon/bit.lua +++ b/pgmoon/bit.lua @@ -1,16 +1,17 @@ -local rshift, lshift, band, ok, _ -local string_loader -string_loader = function(str) +local rshift, lshift, band, bxor +local load_code +load_code = function(str) local sent = false - return function() + return pcall(load(function() if sent then return nil end sent = true return str - end + end)) end -ok, band = pcall(load(string_loader([[ return function(a,b) +local ok +ok, band = load_code([[ return function(a,b) a = a & b if a > 0x7FFFFFFF then -- extend the sign bit @@ -18,9 +19,19 @@ ok, band = pcall(load(string_loader([[ return function(a,b) end return a end -]]))) +]]) if ok then - _, lshift = pcall(load(string_loader([[ return function(x,y) + local _ + _, bxor = load_code([[ return function(a,b) + a = a ~ b + if a > 0x7FFFFFFF then + -- extend the sign bit + a = ~0xFFFFFFFF | a + end + return a + end + ]]) + _, lshift = load_code([[ return function(x,y) -- limit to 32-bit shifts y = y % 32 x = x << y @@ -30,8 +41,8 @@ if ok then end return x end - ]]))) - _, rshift = pcall(load(string_loader([[ return function(x,y) + ]]) + _, rshift = load_code([[ return function(x,y) y = y % 32 -- truncate to 32-bit before applying shift x = x & 0xFFFFFFFF @@ -41,15 +52,16 @@ if ok then end return x end - ]]))) + ]]) else do local _obj_0 = require("bit") - rshift, lshift, band = _obj_0.rshift, _obj_0.lshift, _obj_0.band + rshift, lshift, band, bxor = _obj_0.rshift, _obj_0.lshift, _obj_0.band, _obj_0.bxor end end return { rshift = rshift, lshift = lshift, - band = band + band = band, + bxor = bxor } diff --git a/pgmoon/bit.moon b/pgmoon/bit.moon index f2a2cc6..df253fd 100644 --- a/pgmoon/bit.moon +++ b/pgmoon/bit.moon @@ -1,20 +1,16 @@ -local rshift, lshift, band, ok, _ -local string_loader - +local rshift, lshift, band, bxor -- lua5.1 has separate 'loadstring' and 'load' -- functions ('load' doesn't accept strings). -- This provides a function that 'load' can use, -- and will work on all versions of lua -string_loader = (str) -> +load_code = (str) -> sent = false - return -> - if sent then - return nil + pcall load -> + return nil if sent sent = true - return str - + str -- use load to treat as a string to prevent -- parse errors under lua < 5.3 @@ -23,7 +19,7 @@ string_loader = (str) -> -- uses 32-bit or 64-bit integers, so these wrappers will -- truncate results and/or extend the sign, as appropriate -- to match luajit's behavior. -ok, band = pcall(load(string_loader([[ +ok, band = load_code [[ return function(a,b) a = a & b if a > 0x7FFFFFFF then @@ -32,10 +28,21 @@ ok, band = pcall(load(string_loader([[ end return a end -]]))) +]] if ok then - _, lshift = pcall(load(string_loader([[ + _, bxor = load_code [[ + return function(a,b) + a = a ~ b + if a > 0x7FFFFFFF then + -- extend the sign bit + a = ~0xFFFFFFFF | a + end + return a + end + ]] + + _, lshift = load_code [[ return function(x,y) -- limit to 32-bit shifts y = y % 32 @@ -46,8 +53,9 @@ if ok then end return x end - ]]))) - _, rshift = pcall(load(string_loader([[ + ]] + + _, rshift = load_code [[ return function(x,y) y = y % 32 -- truncate to 32-bit before applying shift @@ -58,13 +66,14 @@ if ok then end return x end - ]]))) + ]] else - import rshift, lshift, band from require "bit" + import rshift, lshift, band, bxor from require "bit" -return { - rshift: rshift - lshift: lshift - band: band +{ + :rshift + :lshift + :band + :bxor } diff --git a/pgmoon/cqueues.lua b/pgmoon/cqueues.lua index 9e5b238..7f26b5a 100644 --- a/pgmoon/cqueues.lua +++ b/pgmoon/cqueues.lua @@ -21,8 +21,12 @@ do end return true end, - sslhandshake = function(self) - return self.sock:starttls() + starttls = function(self, ...) + return self.sock:starttls(...) + end, + getpeercertificate = function(self) + local ssl = assert(self.sock:checktls()) + return assert(ssl:getPeerCertificate(), "no peer certificate available") end, send = function(self, ...) return self.sock:write(flatten(...)) diff --git a/pgmoon/cqueues.moon b/pgmoon/cqueues.moon index 47237dc..977461a 100644 --- a/pgmoon/cqueues.moon +++ b/pgmoon/cqueues.moon @@ -21,8 +21,14 @@ class CqueuesSocket true - sslhandshake: => - @sock\starttls! + -- args: [context][, timeout] + starttls: (...) => + @sock\starttls ... + + -- returns openssl.x509 object + getpeercertificate: => + ssl = assert @sock\checktls! + assert ssl\getPeerCertificate!, "no peer certificate available" send: (...) => @sock\write flatten ... diff --git a/pgmoon/crypto.lua b/pgmoon/crypto.lua index e8f1572..cd1a938 100644 --- a/pgmoon/crypto.lua +++ b/pgmoon/crypto.lua @@ -1,11 +1,10 @@ -if ngx then - return { - md5 = ngx.md5 - } -end local md5 -pcall(function() - local digest = require("openssl.digest") +if ngx then + md5 = ngx.md5 +elseif pcall(function() + return require("openssl.digest") +end) then + local openssl_digest = require("openssl.digest") local hex_char hex_char = function(c) return string.format("%02x", string.byte(c)) @@ -15,20 +14,55 @@ pcall(function() return (str:gsub(".", hex_char)) end md5 = function(str) - return hex(digest.new("md5"):final(str)) + return hex(openssl_digest.new("md5"):final(str)) + end +elseif pcall(function() + return require("crypto") +end) then + local crypto = require("crypto") + md5 = function(str) + return crypto.digest("md5", str) end -end) -if not (md5) then - pcall(function() - local crypto = require("crypto") - md5 = function(str) - return crypto.digest("md5", str) - end - end) +else + md5 = function() + return error("Either luaossl (recommended) or LuaCrypto is required to calculate md5") + end +end +local hmac_sha256 +hmac_sha256 = function(key, str) + local openssl_hmac = require("openssl.hmac") + local hmac = assert(openssl_hmac.new(key, "sha256")) + hmac:update(str) + return assert(hmac:final()) end -if not (md5) then - error("Either luaossl (recommended) or LuaCrypto is required to calculate md5") +local digest_sha256 +digest_sha256 = function(str) + local digest = assert(require("openssl.digest").new("sha256")) + digest:update(str) + return assert(digest:final()) +end +local kdf_derive_sha256 +kdf_derive_sha256 = function(str, salt, i) + local openssl_kdf = require("openssl.kdf") + local decode_base64 + decode_base64 = require("pgmoon.util").decode_base64 + salt = decode_base64(salt) + local key, err = openssl_kdf.derive({ + type = "PBKDF2", + md = "sha256", + salt = salt, + iter = i, + pass = str, + outlen = 32 + }) + if not (key) then + return nil, "failed to derive pbkdf2 key: " .. tostring(err) + end + return key end return { - md5 = md5 + md5 = md5, + hmac_sha256 = hmac_sha256, + digest_sha256 = digest_sha256, + kdf_derive_sha256 = kdf_derive_sha256 } diff --git a/pgmoon/crypto.moon b/pgmoon/crypto.moon index a810996..89c6836 100644 --- a/pgmoon/crypto.moon +++ b/pgmoon/crypto.moon @@ -1,26 +1,49 @@ -if ngx - return { md5: ngx.md5 } +md5 = if ngx + ngx.md5 +elseif pcall -> require "openssl.digest" + openssl_digest = require "openssl.digest" + hex_char = (c) -> string.format "%02x", string.byte c + hex = (str) -> (str\gsub ".", hex_char) -local md5 + (str) -> hex openssl_digest.new("md5")\final str +elseif pcall -> require "crypto" + crypto = require "crypto" + (str) -> crypto.digest "md5", str +else + -> error "Either luaossl (recommended) or LuaCrypto is required to calculate md5" -pcall -> - digest = require "openssl.digest" +hmac_sha256 = (key, str) -> + openssl_hmac = require("openssl.hmac") + hmac = assert openssl_hmac.new(key, "sha256") - hex_char = (c) -> string.format "%02x", string.byte c - hex = (str) -> (str\gsub ".", hex_char) + hmac\update str + assert hmac\final! + +digest_sha256 = (str) -> + digest = assert require("openssl.digest").new("sha256") + digest\update str + assert digest\final! + + +kdf_derive_sha256 = (str, salt, i) -> + openssl_kdf = require "openssl.kdf" + import decode_base64 from require "pgmoon.util" - md5 = (str) -> - hex digest.new("md5")\final str + salt = decode_base64 salt -unless md5 - pcall -> - crypto = require "crypto" + key, err = openssl_kdf.derive { + type: "PBKDF2" + md: "sha256" + salt: salt + iter: i + pass: str + outlen: 32 -- our H() produces a 32 byte hash value (SHA-256) + } - md5 = (str) -> - crypto.digest "md5", str + unless key + return nil, "failed to derive pbkdf2 key: #{err}" -unless md5 - error "Either luaossl (recommended) or LuaCrypto is required to calculate md5" + key -{ :md5 } +{ :md5, :hmac_sha256, :digest_sha256, :kdf_derive_sha256 } diff --git a/pgmoon/init.lua b/pgmoon/init.lua index c95df35..04da2c5 100644 --- a/pgmoon/init.lua +++ b/pgmoon/init.lua @@ -1,13 +1,13 @@ local socket = require("pgmoon.socket") local insert insert = table.insert -local rshift, lshift, band +local rshift, lshift, band, bxor do local _obj_0 = require("pgmoon.bit") - rshift, lshift, band = _obj_0.rshift, _obj_0.lshift, _obj_0.band + rshift, lshift, band, bxor = _obj_0.rshift, _obj_0.lshift, _obj_0.band, _obj_0.bxor end local unpack = table.unpack or unpack -local VERSION = "1.12.0" +local VERSION = "1.13.0" local _len _len = function(thing, t) if t == nil then @@ -126,10 +126,13 @@ do "NULL" }, PG_TYPES = PG_TYPES, - user = "postgres", - host = "127.0.0.1", - port = "5432", - ssl = false, + default_config = { + application_name = "pgmoon", + user = "postgres", + host = "127.0.0.1", + port = "5432", + ssl = false + }, type_deserializers = { json = function(self, val, name) local decode_json @@ -185,18 +188,24 @@ do return self:set_type_oid(tonumber(res.oid), "hstore") end, connect = function(self) - local opts - if self.sock_type == "nginx" then - opts = { - pool = self.pool_name or tostring(self.host) .. ":" .. tostring(self.port) .. ":" .. tostring(self.database) .. ":" .. tostring(self.user) + if not (self.sock) then + self.sock = socket.new(self.sock_type) + end + local connect_opts + local _exp_0 = self.sock_type + if "nginx" == _exp_0 then + connect_opts = { + pool = self.config.pool_name or tostring(self.config.host) .. ":" .. tostring(self.config.port) .. ":" .. tostring(self.config.database) .. ":" .. tostring(self.config.user), + pool_size = self.config.pool_size, + backlog = self.config.backlog } end - local ok, err = self.sock:connect(self.host, self.port, opts) + local ok, err = self.sock:connect(self.config.host, self.config.port, connect_opts) if not (ok) then return nil, err end if self.sock:getreusedtimes() == 0 then - if self.ssl then + if self.config.ssl then local success success, err = self:send_ssl_message() if not (success) then @@ -232,6 +241,35 @@ do self.sock = nil return sock:setkeepalive(...) end, + create_cqueues_openssl_context = function(self) + if not (self.config.ssl_verify ~= nil or self.config.cert or self.config.key or self.config.ssl_version) then + return + end + local ssl_context = require("openssl.ssl.context") + local out = ssl_context.new(self.config.ssl_version) + if self.config.ssl_verify == true then + out:setVerify(ssl_context.VERIFY_PEER) + end + if self.config.ssl_verify == false then + out:setVerify(ssl_context.VERIFY_NONE) + end + if self.config.cert then + out:setCertificate(self.config.cert) + end + if self.config.key then + out:setPrivateKey(self.config.key) + end + return out + end, + create_luasec_opts = function(self) + return { + key = self.config.key, + certificate = self.config.cert, + cafile = self.config.cafile, + protocol = self.config.ssl_version, + verify = self.config.ssl_verify and "peer" or "none" + } + end, auth = function(self) local t, msg = self:receive_message() if not (t) then @@ -252,26 +290,194 @@ do return self:cleartext_auth(msg) elseif 5 == _exp_0 then return self:md5_auth(msg) + elseif 10 == _exp_0 then + return self:scram_sha_256_auth(msg) else return error("don't know how to auth: " .. tostring(auth_type)) end end, cleartext_auth = function(self, msg) - assert(self.password, "missing password, required for connect") + assert(self.config.password, "missing password, required for connect") self:send_message(MSG_TYPE.password, { - self.password, + self.config.password, NULL }) return self:check_auth() end, + scram_sha_256_auth = function(self, msg) + assert(self.config.password, "missing password, required for connect") + local openssl_rand = require("openssl.rand") + local rand_bytes = assert(openssl_rand.bytes(18)) + local encode_base64 + encode_base64 = require("pgmoon.util").encode_base64 + local c_nonce = encode_base64(rand_bytes) + local nonce = "r=" .. c_nonce + local saslname = "" + local username = "n=" .. saslname + local client_first_message_bare = username .. "," .. nonce + local plus = false + local bare = false + if msg:match("SCRAM%-SHA%-256%-PLUS") then + plus = true + elseif msg:match("SCRAM%-SHA%-256") then + bare = true + else + error("unsupported SCRAM mechanism name: " .. tostring(msg)) + end + local gs2_cbind_flag + local gs2_header + local cbind_input + local mechanism_name + if bare then + gs2_cbind_flag = "n" + gs2_header = gs2_cbind_flag .. ",," + cbind_input = gs2_header + mechanism_name = "SCRAM-SHA-256" .. NULL + elseif plus then + local cb_name = "tls-server-end-point" + gs2_cbind_flag = "p=" .. cb_name + gs2_header = gs2_cbind_flag .. ",," + mechanism_name = "SCRAM-SHA-256-PLUS" .. NULL + local cbind_data + do + if self.sock_type == "cqueues" then + local openssl_x509 = self.sock:getpeercertificate() + cbind_data = openssl_x509:digest("sha256", "s") + else + local pem, signature + if self.sock_type == "nginx" then + local ssl = require("resty.openssl.ssl").from_socket(self.sock) + local server_cert = ssl:get_peer_certificate() + pem, signature = server_cert:to_PEM(), server_cert:get_signature_name() + else + local server_cert = self.sock:getpeercertificate() + pem, signature = server_cert:pem(), server_cert:getsignaturename() + end + signature = signature:lower() + if signature:match("^md5") or signature:match("^sha1") then + signature = "sha256" + end + local openssl_x509 = require("openssl.x509").new(pem, "PEM") + cbind_data = assert(openssl_x509:digest(signature, "s")) + end + end + cbind_input = gs2_header .. cbind_data + end + local client_first_message = gs2_header .. client_first_message_bare + self:send_message(MSG_TYPE.password, { + mechanism_name, + self:encode_int(#client_first_message), + client_first_message + }) + local t + t, msg = self:receive_message() + if not (t) then + return nil, msg + end + local server_first_message = msg:sub(5) + local int32 = self:decode_int(msg, 4) + if int32 == nil or int32 ~= 11 then + return nil, "server_first_message error: " .. msg + end + local channel_binding = "c=" .. encode_base64(cbind_input) + nonce = server_first_message:match("([^,]+)") + if not (nonce) then + return nil, "malformed server message (nonce)" + end + local client_final_message_without_proof = channel_binding .. "," .. nonce + local xor + xor = function(a, b) + local result + do + local _accum_0 = { } + local _len_0 = 1 + for i = 1, #a do + local x = a:byte(i) + local y = b:byte(i) + if not (x and y) then + return nil + end + local _value_0 = string.char(bxor(x, y)) + _accum_0[_len_0] = _value_0 + _len_0 = _len_0 + 1 + end + result = _accum_0 + end + return table.concat(result) + end + local salt = server_first_message:match(",s=([^,]+)") + if not (salt) then + return nil, "malformed server message (salt)" + end + local i = server_first_message:match(",i=(.+)") + if not (i) then + return nil, "malformed server message (iteraton count)" + end + if tonumber(i) < 4096 then + return nil, "the iteration-count sent by the server is less than 4096" + end + local kdf_derive_sha256, hmac_sha256, digest_sha256 + do + local _obj_0 = require("pgmoon.crypto") + kdf_derive_sha256, hmac_sha256, digest_sha256 = _obj_0.kdf_derive_sha256, _obj_0.hmac_sha256, _obj_0.digest_sha256 + end + local salted_password, err = kdf_derive_sha256(self.config.password, salt, tonumber(i)) + if not (salted_password) then + return nil, err + end + local client_key + client_key, err = hmac_sha256(salted_password, "Client Key") + if not (client_key) then + return nil, err + end + local stored_key + stored_key, err = digest_sha256(client_key) + if not (stored_key) then + return nil, err + end + local auth_message = tostring(client_first_message_bare) .. "," .. tostring(server_first_message) .. "," .. tostring(client_final_message_without_proof) + local client_signature + client_signature, err = hmac_sha256(stored_key, auth_message) + if not (client_signature) then + return nil, err + end + local proof = xor(client_key, client_signature) + if not (proof) then + return nil, "failed to generate the client proof" + end + local client_final_message = tostring(client_final_message_without_proof) .. ",p=" .. tostring(encode_base64(proof)) + self:send_message(MSG_TYPE.password, { + client_final_message + }) + t, msg = self:receive_message() + if not (t) then + return nil, msg + end + local server_key + server_key, err = hmac_sha256(salted_password, "Server Key") + if not (server_key) then + return nil, err + end + local server_signature + server_signature, err = hmac_sha256(server_key, auth_message) + if not (server_signature) then + return nil, err + end + server_signature = encode_base64(server_signature) + local sent_server_signature = msg:match("v=([^,]+)") + if server_signature ~= sent_server_signature then + return nil, "authentication exchange unsuccessful" + end + return self:check_auth() + end, md5_auth = function(self, msg) local md5 md5 = require("pgmoon.crypto").md5 local salt = msg:sub(5, 8) - assert(self.password, "missing password, required for connect") + assert(self.config.password, "missing password, required for connect") self:send_message(MSG_TYPE.password, { "md5", - md5(md5(self.password .. self.user) .. salt), + md5(md5(self.config.password .. self.config.user) .. salt), NULL }) return self:check_auth() @@ -546,21 +752,21 @@ do return t, msg end, send_startup_message = function(self) - assert(self.user, "missing user for connect") - assert(self.database, "missing database for connect") + assert(self.config.user, "missing user for connect") + assert(self.config.database, "missing database for connect") local data = { self:encode_int(196608), "user", NULL, - self.user, + self.config.user, NULL, "database", NULL, - self.database, + self.config.database, NULL, "application_name", NULL, - "pgmoon", + self.config.application_name, NULL, NULL } @@ -583,12 +789,17 @@ do return nil, err end if t == MSG_TYPE.status then - if self.sock_type == "nginx" then - return self.sock:sslhandshake(false, nil, self.ssl_verify) + local _exp_0 = self.sock_type + if "nginx" == _exp_0 then + return self.sock:sslhandshake(false, nil, self.config.ssl_verify) + elseif "luasocket" == _exp_0 then + return self.sock:sslhandshake(self.config.luasec_opts or self:create_luasec_opts()) + elseif "cqueues" == _exp_0 then + return self.sock:starttls(self.config.cqueues_openssl_context or self:create_cqueues_openssl_context()) else - return self.sock:sslhandshake(self.ssl_verify, self.luasec_opts) + return error("don't know how to do ssl handshake for socket type: " .. tostring(self.sock_type)) end - elseif t == MSG_TYPE.error or self.ssl_required then + elseif t == MSG_TYPE.error or self.config.ssl_required then self:disconnect() return nil, "the server does not support SSL connections" else @@ -672,31 +883,22 @@ do } _base_0.__index = _base_0 _class_0 = setmetatable({ - __init = function(self, opts) - self.sock, self.sock_type = socket.new(opts and opts.socket_type) - if opts then - self.user = opts.user - self.host = opts.host - self.database = opts.database - self.port = opts.port - self.password = opts.password - self.ssl = opts.ssl - self.ssl_verify = opts.ssl_verify - self.ssl_required = opts.ssl_required - self.pool_name = opts.pool - self.luasec_opts = { - key = opts.key, - cert = opts.cert, - cafile = opts.cafile, - ssl_version = opts.ssl_version or "any", - options = { - "all", - "no_sslv2", - "no_sslv3", - "no_tlsv1" - } - } - end + __init = function(self, _config) + if _config == nil then + _config = { } + end + self._config = _config + self.config = setmetatable({ }, { + __index = function(t, key) + local value = self._config[key] + if value == nil then + return self.default_config[key] + else + return value + end + end + }) + self.sock, self.sock_type = socket.new(self.config.socket_type) end, __base = _base_0, __name = "Postgres" diff --git a/pgmoon/init.moon b/pgmoon/init.moon index 4f9fa61..cea6660 100644 --- a/pgmoon/init.moon +++ b/pgmoon/init.moon @@ -1,11 +1,14 @@ socket = require "pgmoon.socket" import insert from table -import rshift, lshift, band from require "pgmoon.bit" +import rshift, lshift, band, bxor from require "pgmoon.bit" unpack = table.unpack or unpack -VERSION = "1.12.0" +-- Protocol documentation: +-- https://www.postgresql.org/docs/current/protocol-message-formats.html + +VERSION = "1.13.0" _len = (thing, t=type(thing)) -> switch t @@ -106,10 +109,13 @@ class Postgres NULL: {"NULL"} :PG_TYPES - user: "postgres" - host: "127.0.0.1" - port: "5432" - ssl: false + default_config: { + application_name: "pgmoon" + user: "postgres" + host: "127.0.0.1" + port: "5432" + ssl: false + } -- custom types supplementing PG_TYPES type_deserializers: { @@ -153,38 +159,47 @@ class Postgres assert res, "hstore oid not found" @set_type_oid tonumber(res.oid), "hstore" - new: (opts) => - @sock, @sock_type = socket.new opts and opts.socket_type - - if opts - @user = opts.user - @host = opts.host - @database = opts.database - @port = opts.port - @password = opts.password - @ssl = opts.ssl - @ssl_verify = opts.ssl_verify - @ssl_required = opts.ssl_required - @pool_name = opts.pool - @luasec_opts = { - key: opts.key - cert: opts.cert - cafile: opts.cafile - ssl_version: opts.ssl_version or "any" - options: { "all", "no_sslv2", "no_sslv3", "no_tlsv1" } - } + -- config={} + -- host: server hostname + -- port: server port + -- user: the username to authenticate with + -- password: the username to authenticate with + -- database: database to connect to + -- application_name: name assigned to connection to server + -- socket_type: type of socket to use (nginx, luasocket, cqueues) + -- ssl: enable ssl connections + -- ssl_verify: verify the certificate + -- cqueues_openssl_context: manually created openssl.ssl.context for cqueues sockets + -- luasec_opts: manually created options for LuaSocket ssl connections + new: (@_config={}) => + @config = setmetatable {}, { + __index: (t, key) -> + value = @_config[key] + if value == nil + @default_config[key] + else + value + } + + @sock, @sock_type = socket.new @config.socket_type connect: => - opts = if @sock_type == "nginx" - { - pool: @pool_name or "#{@host}:#{@port}:#{@database}:#{@user}" - } - - ok, err = @sock\connect @host, @port, opts + unless @sock + @sock = socket.new @sock_type + + connect_opts = switch @sock_type + when "nginx" + { + pool: @config.pool_name or "#{@config.host}:#{@config.port}:#{@config.database}:#{@config.user}" + pool_size: @config.pool_size + backlog: @config.backlog + } + + ok, err = @sock\connect @config.host, @config.port, connect_opts return nil, err unless ok if @sock\getreusedtimes! == 0 - if @ssl + if @config.ssl success, err = @send_ssl_message! return nil, err unless success @@ -212,6 +227,38 @@ class Postgres @sock = nil sock\setkeepalive ... + -- see: http://25thandclement.com/~william/projects/luaossl.pdf + create_cqueues_openssl_context: => + return unless @config.ssl_verify != nil or @config.cert or @config.key or @config.ssl_version + + ssl_context = require("openssl.ssl.context") + + out = ssl_context.new @config.ssl_version + + if @config.ssl_verify == true + out\setVerify ssl_context.VERIFY_PEER + + if @config.ssl_verify == false + out\setVerify ssl_context.VERIFY_NONE + + if @config.cert + out\setCertificate @config.cert + + if @config.key + out\setPrivateKey @config.key + + out + + create_luasec_opts: => + { + key: @config.key + certificate: @config.cert + cafile: @config.cafile + protocol: @config.ssl_version + verify: @config.ssl_verify and "peer" or "none" + } + + auth: => t, msg = @receive_message! return nil, msg unless t @@ -232,27 +279,206 @@ class Postgres @cleartext_auth msg when 5 -- md5 password @md5_auth msg + when 10 -- AuthenticationSASL + @scram_sha_256_auth msg else error "don't know how to auth: #{auth_type}" cleartext_auth: (msg) => - assert @password, "missing password, required for connect" + assert @config.password, "missing password, required for connect" @send_message MSG_TYPE.password, { - @password + @config.password NULL } @check_auth! + -- https://www.postgresql.org/docs/current/sasl-authentication.html#SASL-SCRAM-SHA-256 + scram_sha_256_auth: (msg) => + assert @config.password, "missing password, required for connect" + + openssl_rand = require "openssl.rand" + + -- '18' is the number set by postgres on the server side + rand_bytes = assert openssl_rand.bytes 18 + + import encode_base64 from require "pgmoon.util" + + c_nonce = encode_base64 rand_bytes + nonce = "r=" .. c_nonce + saslname = "" + username = "n=" .. saslname + client_first_message_bare = username .. "," .. nonce + + plus = false + bare = false + + if msg\match "SCRAM%-SHA%-256%-PLUS" + plus = true + elseif msg\match "SCRAM%-SHA%-256" + bare = true + else + error "unsupported SCRAM mechanism name: " .. tostring(msg) + + local gs2_cbind_flag + local gs2_header + local cbind_input + local mechanism_name + + if bare + gs2_cbind_flag = "n" + gs2_header = gs2_cbind_flag .. ",," + cbind_input = gs2_header + mechanism_name = "SCRAM-SHA-256" .. NULL + elseif plus + cb_name = "tls-server-end-point" + gs2_cbind_flag = "p=" .. cb_name + gs2_header = gs2_cbind_flag .. ",," + mechanism_name = "SCRAM-SHA-256-PLUS" .. NULL + + cbind_data = do + if @sock_type == "cqueues" + openssl_x509 = @sock\getpeercertificate! + openssl_x509\digest "sha256", "s" + else + pem, signature = if @sock_type == "nginx" + ssl = require("resty.openssl.ssl").from_socket(@sock) + server_cert = ssl\get_peer_certificate() + server_cert\to_PEM!, server_cert\get_signature_name! + else + server_cert = @sock\getpeercertificate() + server_cert\pem!, server_cert\getsignaturename! + + signature = signature\lower! + + -- upgrade the signature if necessary + if signature\match("^md5") or signature\match("^sha1") + signature = "sha256" + + openssl_x509 = require("openssl.x509").new(pem, "PEM") + assert openssl_x509\digest(signature, "s") + + cbind_input = gs2_header .. cbind_data + + client_first_message = gs2_header .. client_first_message_bare + + @send_message MSG_TYPE.password, { + mechanism_name + @encode_int #client_first_message + client_first_message + } + + t, msg = @receive_message() + + unless t + return nil, msg + + server_first_message = msg\sub 5 + int32 = @decode_int msg, 4 + + if int32 == nil or int32 != 11 + return nil, "server_first_message error: " .. msg + + channel_binding = "c=" .. encode_base64 cbind_input + nonce = server_first_message\match "([^,]+)" + + unless nonce + return nil, "malformed server message (nonce)" + + client_final_message_without_proof = channel_binding .. "," .. nonce + + xor = (a, b) -> + result = for i=1,#a + x = a\byte i + y = b\byte i + + unless x and y + return nil + + string.char bxor x, y + + table.concat result + + salt = server_first_message\match ",s=([^,]+)" + + unless salt + return nil, "malformed server message (salt)" + + i = server_first_message\match ",i=(.+)" + + unless i + return nil, "malformed server message (iteraton count)" + + if tonumber(i) < 4096 + return nil, "the iteration-count sent by the server is less than 4096" + + import kdf_derive_sha256, hmac_sha256, digest_sha256 from require "pgmoon.crypto" + salted_password, err = kdf_derive_sha256 @config.password, salt, tonumber i + + unless salted_password + return nil, err + + client_key, err = hmac_sha256 salted_password, "Client Key" + + unless client_key + return nil, err + + stored_key, err = digest_sha256 client_key + + unless stored_key + return nil, err + + auth_message = "#{client_first_message_bare },#{server_first_message },#{client_final_message_without_proof}" + + client_signature, err = hmac_sha256 stored_key, auth_message + + unless client_signature + return nil, err + + proof = xor client_key, client_signature + + unless proof + return nil, "failed to generate the client proof" + + client_final_message = "#{client_final_message_without_proof },p=#{encode_base64 proof}" + + @send_message MSG_TYPE.password, { + client_final_message + } + + t, msg = @receive_message() + + unless t + return nil, msg + + server_key, err = hmac_sha256 salted_password, "Server Key" + + unless server_key + return nil, err + + server_signature, err = hmac_sha256 server_key, auth_message + + unless server_signature + return nil, err + + + server_signature = encode_base64 server_signature + sent_server_signature = msg\match "v=([^,]+)" + + if server_signature != sent_server_signature then + return nil, "authentication exchange unsuccessful" + + @check_auth! + md5_auth: (msg) => import md5 from require "pgmoon.crypto" salt = msg\sub 5, 8 - assert @password, "missing password, required for connect" + assert @config.password, "missing password, required for connect" @send_message MSG_TYPE.password, { "md5" - md5 md5(@password .. @user) .. salt + md5 md5(@config.password .. @config.user) .. salt NULL } @@ -500,17 +726,17 @@ class Postgres t, msg send_startup_message: => - assert @user, "missing user for connect" - assert @database, "missing database for connect" + assert @config.user, "missing user for connect" + assert @config.database, "missing database for connect" data = { @encode_int 196608 "user", NULL - @user, NULL + @config.user, NULL "database", NULL - @database, NULL + @config.database, NULL "application_name", NULL - "pgmoon", NULL + @config.application_name, NULL NULL } @@ -530,11 +756,16 @@ class Postgres return nil, err unless t if t == MSG_TYPE.status - if @sock_type == "nginx" - @sock\sslhandshake false, nil, @ssl_verify - else - @sock\sslhandshake @ssl_verify, @luasec_opts - elseif t == MSG_TYPE.error or @ssl_required + switch @sock_type + when "nginx" + @sock\sslhandshake false, nil, @config.ssl_verify + when "luasocket" + @sock\sslhandshake @config.luasec_opts or @create_luasec_opts! + when "cqueues" + @sock\starttls @config.cqueues_openssl_context or @create_cqueues_openssl_context! + else + error "don't know how to do ssl handshake for socket type: #{@sock_type}" + elseif t == MSG_TYPE.error or @config.ssl_required @disconnect! nil, "the server does not support SSL connections" else diff --git a/pgmoon/socket.lua b/pgmoon/socket.lua index d823baa..43db8f5 100644 --- a/pgmoon/socket.lua +++ b/pgmoon/socket.lua @@ -26,8 +26,7 @@ do } luasocket = { tcp = function(...) - local socket = require("socket") - local sock = socket.tcp(...) + local sock = core.tcp(...) local proxy = setmetatable({ sock = sock, send = function(self, ...) @@ -42,18 +41,15 @@ do end return self.sock:settimeout(t) end, - sslhandshake = function(self, verify, opts) + sslhandshake = function(self, opts) if opts == nil then opts = { } end local ssl = require("ssl") local params = { mode = "client", - protocol = opts.ssl_version or "any", - key = opts.key, - certificate = opts.cert, - cafile = opts.cafile, - verify = verify and "peer" or "none", + protocol = "any", + verify = "none", options = { "all", "no_sslv2", @@ -61,6 +57,9 @@ do "no_tlsv1" } } + for k, v in pairs(opts) do + params[k] = v + end local sec_sock, err = ssl.wrap(self.sock, params) if not (sec_sock) then return false, err @@ -89,14 +88,14 @@ return { if ngx and ngx.get_phase() ~= "init" then socket_type = "nginx" else - socket_type = "luasocket" + socket_type = "haproxy" end end local socket local _exp_0 = socket_type if "nginx" == _exp_0 then socket = ngx.socket.tcp() - elseif "luasocket" == _exp_0 then + elseif "haproxy" == _exp_0 then socket = luasocket.tcp() elseif "cqueues" == _exp_0 then socket = require("pgmoon.cqueues").CqueuesSocket() diff --git a/pgmoon/socket.moon b/pgmoon/socket.moon index e206f61..2b9ca44 100644 --- a/pgmoon/socket.moon +++ b/pgmoon/socket.moon @@ -34,18 +34,19 @@ luasocket = do if t t = t/1000 @sock\settimeout t - sslhandshake: (verify, opts={}) => + + sslhandshake: (opts={}) => ssl = require "ssl" params = { mode: "client" - protocol: opts.ssl_version or "any" - key: opts.key - certificate: opts.cert - cafile: opts.cafile - verify: verify and "peer" or "none" + protocol: "any" + verify: "none" options: { "all", "no_sslv2", "no_sslv3", "no_tlsv1" } } + for k,v in pairs opts + params[k] = v + sec_sock, err = ssl.wrap @sock, params return false, err unless sec_sock diff --git a/pgmoon/util.lua b/pgmoon/util.lua index 0d40e35..9752404 100644 --- a/pgmoon/util.lua +++ b/pgmoon/util.lua @@ -20,6 +20,27 @@ do return table.concat(buffer) end end +local encode_base64, decode_base64 +if ngx then + do + local _obj_0 = ngx + encode_base64, decode_base64 = _obj_0.encode_base64, _obj_0.decode_base64 + end +else + local b64, unb64 + do + local _obj_0 = require("mime") + b64, unb64 = _obj_0.b64, _obj_0.unb64 + end + encode_base64 = function(...) + return (b64(...)) + end + decode_base64 = function(...) + return (unb64(...)) + end +end return { - flatten = flatten + flatten = flatten, + encode_base64 = encode_base64, + decode_base64 = decode_base64 } diff --git a/pgmoon/util.moon b/pgmoon/util.moon index 6ac64f5..9a4d937 100644 --- a/pgmoon/util.moon +++ b/pgmoon/util.moon @@ -16,4 +16,13 @@ flatten = do __flatten t, buffer table.concat buffer -{:flatten} +local encode_base64, decode_base64 + +if ngx + {:encode_base64, :decode_base64} = ngx +else + { :b64, :unb64 } = require "mime" -- provided by luasocket + encode_base64 = (...) -> (b64 ...) + decode_base64 = (...) -> (unb64 ...) + +{:flatten, :encode_base64, :decode_base64} diff --git a/spec/pgmoon_spec.moon b/spec/pgmoon_spec.moon index 8eeb748..4ce57d3 100644 --- a/spec/pgmoon_spec.moon +++ b/spec/pgmoon_spec.moon @@ -580,6 +580,7 @@ describe "pgmoon with server", -> assert PostgresArray.__base == getmetatable array it "encodes array value", -> + assert.same "ARRAY[]", encode_array {} assert.same "ARRAY[1,2,3]", encode_array {1,2,3} assert.same "ARRAY['hello','world']", encode_array {"hello", "world"} assert.same "ARRAY[[4,5],[6,7]]", encode_array {{4,5}, {6,7}} diff --git a/spec/pgmoon_ssl_spec.moon b/spec/pgmoon_ssl_spec.moon index c29c463..d3ce227 100644 --- a/spec/pgmoon_ssl_spec.moon +++ b/spec/pgmoon_ssl_spec.moon @@ -79,7 +79,6 @@ describe "pgmoon with server", -> assert.same 'TLSv1.3', res[1].version pg\disconnect! - it "connects with ssl using cqueues", -> pg = Postgres { database: DB @@ -93,5 +92,27 @@ describe "pgmoon with server", -> assert pg\connect! assert pg\query "select * from information_schema.tables" + + pg\disconnect! + + + it "connects with ssl using cqueues with context options", -> + pg = Postgres { + database: DB + port: PORT + user: USER + password: PASSWORD + host: HOST + ssl: true + socket_type: "cqueues" + ssl_version: "TLSv1_2" + } + + assert pg\connect! + assert pg\query "select * from information_schema.tables" + + res = assert pg\query [[SELECT version FROM pg_stat_ssl WHERE pid=pg_backend_pid()]] + assert.same 'TLSv1.2', res[1].version + pg\disconnect!