From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from smtpng1.m.smailru.net (smtpng1.m.smailru.net [94.100.181.251]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by dev.tarantool.org (Postfix) with ESMTPS id 39F6D4696C6 for ; Thu, 9 Jan 2020 11:04:37 +0300 (MSK) From: olegrok@tarantool.org Date: Thu, 9 Jan 2020 11:04:14 +0300 Message-Id: <86c6e75e1a4e4c612306832fa1846266546074a7.1577881257.git.babinoleg@mail.ru> In-Reply-To: References: MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: 8bit Subject: [Tarantool-patches] [PATCH 2/2] httpc: introduce format_query and parse_query functions List-Id: Tarantool development patches List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , To: tarantool-patches@dev.tarantool.org, alexander.turenko@tarantool.org Cc: Oleg Babin From: Oleg Babin Previous patch introduced url_escape and url_unescape. However it's quite poorly to give our users too low-level functions. The main use case is to form and parse http query parameters This patch introduces format_query and parse_query functions Closes #3682 @TarantoolBot document Title: New http.client functions Four new functions are available in http.client module: - url_escape - simple wrapper over curl_easy_escape (https://curl.haxx.se/libcurl/c/curl_easy_escape.html) - url_unescape - simple wrapper over curl_easy_unescape (https://curl.haxx.se/libcurl/c/curl_easy_unescape.html) Examples: ```lua -- According RFC3986 tarantool> httpc.url_escape('hello') --- - hello ... tarantool> httpc.url_escape('Привет') --- - '%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82' ... tarantool> httpc.url_escape('$&+,:;=?@-._~') --- - '%24%26%2B%2C%3A%3B%3D%3F%40-._~' ... ``` - format_query - format query arguments from key-value string pairs for HTTP request - parse_query - parse query string into table Example: ```lua -- The query string is composed of a series of field-value pairs -- Within each pair, the field name and value are -- separated by an equals sign, "=" -- The series of pairs is separated by the ampersand, "&" tarantool> httpc.format_query({['hello'] = 'world', ['привет'] = 'мир'}) --- - '%D0%BF%D1%80%D0%B8%D0%B2%D0%B5%D1%82=%D0%BC%D0%B8%D1%80&hello=world' ... tarantool> httpc.parse_query('%D0%BF%D1%80%D0%B8%D0%B2%D0%B5%D1%82=' .. '%D0%BC%D0%B8%D1%80&hello=world') --- - привет: мир hello: world ... ``` --- Issue: https://github.com/tarantool/tarantool/issues/3682 src/lua/httpc.lua | 60 ++++++++++++++++++++++++++++++- test/app-tap/http_client.test.lua | 39 +++++++++++++++++++- 2 files changed, 97 insertions(+), 2 deletions(-) diff --git a/src/lua/httpc.lua b/src/lua/httpc.lua index 33cb78f3c..7d40b3bcc 100644 --- a/src/lua/httpc.lua +++ b/src/lua/httpc.lua @@ -449,10 +449,68 @@ local function url_unescape(str) return driver.unescape(http_default.curl, str) end +-- +-- This function format query params from table with arguments +-- +-- Parameters: +-- +-- args - table with key-value pairs that will be +-- encoded as string "key1=value1&key2=value2" +-- each element of pair is encoded with url_escape +-- +local function format_query(args) + if type(args) ~= "table" then + error("httpc.format_query expected table with args") + end + local encoded = {} + for k, v in pairs(args) do + k = tostring(k) + v = tostring(v) + k = url_escape(k) + v = url_escape(v) + table.insert(encoded, k .. '=' .. v) + end + + return table.concat(encoded, '&') +end + +-- +-- This function parse query params from string to table +-- +-- Parameters: +-- +-- str - string in format "key1=value1&key2=value2" +-- that will be parsed into {key1 = value1, key2 = value2} +-- each element of pair is decoded with url_unescape +-- +local function parse_query(str) + if type(str) ~= "string" then + error("httpc.parse_query expected string with args") + end + local args = string.split(str, '&') + + local decoded = {} + for _, arg in ipairs(args) do + local k_v = string.split(arg, '=', 1) + if #k_v ~= 2 then + error("httpc.parse_query expected arguments in format 'key=value'" + .. " separated by '&', got: " .. tostring(arg)) + end + local key = url_unescape(k_v[1]) + local value = url_unescape(k_v[2]) + + decoded[key] = value + end + + return decoded +end + local this_module = { new = http_new, url_escape = url_escape, - url_unescape = url_unescape + url_unescape = url_unescape, + format_query = format_query, + parse_query = parse_query, } local function http_default_wrap(fname) diff --git a/test/app-tap/http_client.test.lua b/test/app-tap/http_client.test.lua index 3676f56b4..4b5e6bbc5 100755 --- a/test/app-tap/http_client.test.lua +++ b/test/app-tap/http_client.test.lua @@ -611,7 +611,7 @@ function run_tests(test, sock_family, sock_addr) stop_server(test, server) end -test:plan(3) +test:plan(5) test:test("http over AF_INET", function(test) local s = socketlib('AF_INET', 'SOCK_STREAM', 0) @@ -653,4 +653,41 @@ test:test("url_escape/url_unescape", function(test) test:is('-._~', client.url_escape('-._~'), '-._~ are not escaped according RFC 3986') end) +test:test("format_query", function(test) + test:plan(6) + test:is(client.format_query({['hello'] = 'world'}), 'hello=world', 'correct formation for one eng arg') + test:is(client.format_query({['привет'] = 'мир'}), + '%D0%BF%D1%80%D0%B8%D0%B2%D0%B5%D1%82=%D0%BC%D0%B8%D1%80', 'correct formation for one rus arg') + test:is(client.format_query({['key=key'] = 'value;value'}), 'key%3Dkey=value%3Bvalue', + 'correct formation for args with special characters') + test:is(client.format_query({['key1'] = 'value1', ['ключ2'] = 'значение'}), + 'key1=value1&%D0%BA%D0%BB%D1%8E%D1%872=%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5', + 'correct formation for several args') + + local ok, err = pcall(client.format_query, 'str') + test:is(ok, false, 'only table could be passed as argument') + test:ok(err:endswith('httpc.format_query expected table with args'), 'correct error message') +end) + +test:test("parse_query", function(test) + test:plan(8) + test:is_deeply(client.parse_query('hello=world'), {['hello'] = 'world'}, 'correct parsing for one eng arg') + test:is_deeply(client.parse_query('%D0%BF%D1%80%D0%B8%D0%B2%D0%B5%D1%82=%D0%BC%D0%B8%D1%80'), + {['привет'] = 'мир'}, 'correct parsing for one rus arg') + test:is_deeply(client.parse_query('key%3Dkey=value%3Bvalue'), {['key=key'] = 'value;value'}, + 'correct formation for args with special characters') + test:is_deeply(client.parse_query( + 'key1=value1&%D0%BA%D0%BB%D1%8E%D1%872=%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5'), + {['key1'] = 'value1', ['ключ2'] = 'значение'}, 'correct parsing for several args') + + local ok, err = pcall(client.parse_query, {}) + test:is(ok, false, 'only string could be passed as argument') + test:ok(err:endswith('httpc.parse_query expected string with args'), 'correct error message') + + local ok, err = pcall(client.parse_query, '1=2&3&4') + test:is(ok, false, 'unexpected input format error') + test:ok(err:endswith('httpc.parse_query expected arguments in format \'key=value\'' .. + ' separated by \'&\', got: 3'), 'correct error message') +end) + os.exit(test:check() == true and 0 or -1) -- 2.23.0