uv_write_t *req;
};
-static void http_stream_status_free(struct http_stream_status *stat)
-{
- if (!stat)
- return;
-
- free(stat->err_msg);
- stat->err_msg = NULL;
- free(stat);
-}
-
-static int status_free(trie_val_t *stat, void *null)
-{
- assert(stat);
- http_stream_status_free(*stat);
- return 0;
-}
-
-
/*
* Write HTTP/2 protocol data to underlying transport layer.
*/
return 0;
}
-/*
- * Provide data from buffer to HTTP/2 library.
- *
- * To avoid copying the packet wire buffer, we use NGHTTP2_DATA_FLAG_NO_COPY
- * and take care of sending entire DATA frames ourselves with nghttp2_send_data_callback.
- *
- * See https://www.nghttp2.org/documentation/types.html#c.nghttp2_data_source_read_callback
- */
-static ssize_t read_callback(nghttp2_session *h2, int32_t stream_id, uint8_t *buf,
- size_t length, uint32_t *data_flags,
- nghttp2_data_source *source, void *user_data)
-{
- struct http_data *data;
- size_t avail;
- size_t send;
-
- if (!source->ptr) {
- *data_flags |= NGHTTP2_DATA_FLAG_EOF;
- return 0;
- }
-
- data = (struct http_data*)source->ptr;
- avail = data->len - data->pos;
- send = MIN(avail, length);
-
- if (avail == send)
- *data_flags |= NGHTTP2_DATA_FLAG_EOF;
-
- *data_flags |= NGHTTP2_DATA_FLAG_NO_COPY;
- return send;
-}
-
-/*
- * Send http error status code.
- */
-static int send_err_status(struct http_ctx *ctx, int32_t stream_id)
-{
- int ret;
- int status_len;
- nghttp2_data_provider prov;
- trie_val_t *stat_p = trie_get_try(ctx->stream_status, (char *)&stream_id, sizeof(stream_id));
-
- if (!stat_p || !*stat_p)
- return kr_error(EINVAL);
- struct http_stream_status *stat = *stat_p;
-
- prov.source.ptr = NULL;
- prov.read_callback = read_callback;
-
- char status_str[MAX_DECIMAL_LENGTH(stat->err_status)] = { 0 };
- status_len = snprintf(status_str, MAX_DECIMAL_LENGTH(stat->err_status), "%u", stat->err_status);
- nghttp2_nv hdrs_err[] = {
- MAKE_NV(":status", 7, status_str, status_len),
- };
-
- if (stat->err_msg) {
- struct http_data *data = malloc(sizeof(struct http_data));
- if (!data)
- return kr_error(ENOMEM);
-
- data->buf = (uint8_t *)stat->err_msg;
- data->len = strlen(stat->err_msg);
- data->pos = 0;
- data->on_write = NULL;
- data->req = NULL;
- data->ttl = 0;
- prov.source.ptr = data;
-
- /* data will be freed in callback. */
- ret = nghttp2_session_set_stream_user_data(ctx->h2, stream_id, (void*)data);
- if (ret != 0)
- return kr_error(EIO);
- }
-
- ret = nghttp2_submit_response(ctx->h2, stream_id, hdrs_err, sizeof(hdrs_err)/sizeof(*hdrs_err), &prov);
- if (ret != 0)
- return kr_error(EIO);
-
- if (queue_len(ctx->streams) != 0)
- queue_pop(ctx->streams);
-
- return 0;
-}
-
-/*
- * Set error status for particular stream_id and return stream status or NULL on fail.
- *
- * status_msg is optional and defines error message.
- */
-static struct http_stream_status * set_error_status(struct http_ctx *ctx, int32_t stream_id, int status, const char *const status_msg)
-{
-
- trie_val_t *stat_p = trie_get_ins(ctx->stream_status, (char *)&stream_id, sizeof(stream_id));
- struct http_stream_status *stat = *stat_p;
- if (stat && stat->err_status != 200)
- return stat;
-
- // add new item to array
- if (!stat) {
- stat = malloc(sizeof(*stat));
- if (!stat)
- return NULL;
-
- *stat_p = stat;
- stat->err_msg = NULL;
- }
- stat->stream_id = stream_id;
- stat->err_status = status;
-
- if (!status_msg) {
- if (stat->err_msg) { // remove previous message
- free(stat->err_msg);
- stat->err_msg = NULL;
- }
-
- return stat;
- }
-
- stat->err_msg = realloc(stat->err_msg, sizeof(*stat->err_msg) * (strlen(status_msg) + 2));
- if (!stat->err_msg) {
- return stat;
- }
-
- memcpy(stat->err_msg, status_msg, strlen(status_msg));
- stat->err_msg[strlen(status_msg)] = '\n';
- stat->err_msg[strlen(status_msg)+1] = '\0';
-
- return stat;
-}
-
-/*
- * Reinit temporaly data of current stream
- */
-static void http_status_reinit(struct http_ctx *ctx, int stream_id)
-{
- ctx->current_method = HTTP_METHOD_NONE;
- ctx->current_stream = NULL;
- ctx->buf_pos = 0;
- if (ctx->uri_path) {
- free(ctx->uri_path);
- ctx->uri_path = NULL;
- }
- if (ctx->content_type) {
- free(ctx->content_type);
- ctx->content_type = NULL;
- }
-}
-
-static void http_status_reinit_error(struct http_ctx *ctx, int stream_id)
-{
-
- if (ctx->current_method == HTTP_METHOD_POST)
- queue_pop(ctx->streams);
-
- http_status_reinit(ctx, stream_id);
-}
-
/*
* Check endpoint and uri path
*/
-static int check_uri(struct http_ctx *ctx, int32_t stream_id, const char* uri_path)
+static int check_uri(const char* uri_path)
{
static const char key[] = "dns=";
static const char *delim = "&";
char *end_prev;
ssize_t endpoint_len;
ssize_t ret;
- struct http_stream_status *stat;
if (!uri_path)
return kr_error(EINVAL);
break;
}
- if (ret) { /* no endpoint found */
- stat = set_error_status(ctx, stream_id, 400, "missing endpoint");
- return stat ? kr_error(EINVAL) : kr_error(ENOMEM);
- }
+ if (ret) /* no endpoint found */
+ return -1;
if (endpoint_len == strlen(path) - 1) /* done for POST method */
return 0;
if (!strncmp(beg, key, 4)) { /* dns variable in path found */
break;
}
- end_prev = beg + strlen(beg) - 1;
+ end_prev = beg + strlen(beg);
beg = strtok(NULL, delim);
- if (beg && beg-1 != end_prev+1) { /* detect && */
- stat = set_error_status(ctx, stream_id, 400, "invalid uri path");
- return stat ? kr_error(EINVAL) : kr_error(ENOMEM);
+ if (beg-1 != end_prev) { /* detect && */
+ return -1;
}
}
if (!beg) { /* no dns variable in path */
- stat = set_error_status(ctx, stream_id, 400, "'dns' key in path not found");
- return stat ? kr_error(EINVAL) : kr_error(ENOMEM);
- }
- } else {
- if (!beg) { /* no dns variable in path */
- stat = set_error_status(ctx, stream_id, 400, "'dns' key in path not found");
- return stat ? kr_error(EINVAL) : kr_error(ENOMEM);
+ return -1;
}
}
return 0;
}
-
/*
* Process a query from URI path if there's base64url encoded dns variable.
*/
-static int process_uri_path(struct http_ctx *ctx, int32_t stream_id)
+static int process_uri_path(struct http_ctx *ctx, const char* path, int32_t stream_id)
{
+ if (!ctx || !path)
+ return kr_error(EINVAL);
+
static const char key[] = "dns=";
- char *beg, *end;
+ char *beg = strstr(path, key);
+ char *end;
size_t remaining;
ssize_t ret;
uint8_t *dest;
- struct http_stream_status *stat;
- if (!ctx || !ctx->uri_path) {
- stat = set_error_status(ctx, stream_id, 400, "invalid uri path");
- return stat ? 0 : kr_error(ENOMEM);
- }
-
- beg = strstr(ctx->uri_path, key);
- if (!beg) { /* No dns variable in ctx->uri_path. */
- stat = set_error_status(ctx, stream_id, 400, "'dns' key in path not found");
- return stat ? 0 : kr_error(ENOMEM);
- }
+ if (!beg) /* No dns variable in path. */
+ return 0;
beg += sizeof(key) - 1;
end = strchr(beg, '&');
ret = kr_base64url_decode((uint8_t*)beg, end - beg, dest, remaining);
if (ret < 0) {
ctx->buf_pos = 0;
- kr_log_verbose("[http] base64url decode failed %s\n", strerror(ret));
- if (ret == KNOT_ERANGE) {
- stat = set_error_status(ctx, stream_id, 414, NULL);// ? ;
- } else {
- stat = set_error_status(ctx, stream_id, 400, NULL);
- }
- return stat ? 0 : kr_error(ENOMEM);
+ kr_log_verbose("[http] base64url decode failed %s\n",
+ strerror(ret));
+ return ret;
}
ctx->buf_pos += ret;
return 0;
}
+static void refuse_stream(nghttp2_session *h2, int32_t stream_id)
+{
+ nghttp2_submit_rst_stream(
+ h2, NGHTTP2_FLAG_NONE, stream_id, NGHTTP2_REFUSED_STREAM);
+}
+
/*
* Save stream id from first header's frame.
*
return 0;
}
- if (ctx->current_stream != NULL) {
+ if (ctx->incomplete_stream != -1) {
kr_log_verbose(
- "[http] stream %d incomplete\n", ctx->current_stream->stream_id);
- if (!set_error_status(ctx, stream_id, 501, "incomplete stream"))
- return NGHTTP2_ERR_CALLBACK_FAILURE;
+ "[http] stream %d incomplete, refusing\n", ctx->incomplete_stream);
+ refuse_stream(h2, stream_id);
} else {
- ctx->current_stream = set_error_status(ctx, stream_id, 200, NULL);
+ ctx->incomplete_stream = stream_id;
}
return 0;
}
if (frame->hd.type != NGHTTP2_HEADERS)
return 0;
- if (ctx->current_stream->stream_id != stream_id) {
+ if (ctx->incomplete_stream != stream_id) {
kr_log_verbose(
- "[http] stream %d incomplete\n", ctx->current_stream->stream_id);
- if (!set_error_status(ctx, stream_id, 501, "incomplete stream"))
- return NGHTTP2_ERR_CALLBACK_FAILURE;
+ "[http] stream %d incomplete, refusing\n", ctx->incomplete_stream);
+ refuse_stream(h2, stream_id);
return 0;
}
if (!strcasecmp(":path", (const char *)name)) {
- int rc = check_uri(ctx, stream_id, (const char *)value);
- if (rc < 0) {
- if (rc == kr_error(ENOMEM))
- return NGHTTP2_ERR_CALLBACK_FAILURE;
- } else {
- ctx->uri_path = malloc(sizeof(*ctx->uri_path) * (valuelen + 1));
- if (!ctx->uri_path)
- return NGHTTP2_ERR_CALLBACK_FAILURE;
- memcpy(ctx->uri_path, value, valuelen);
- ctx->uri_path[valuelen] = '\0';
+ if (check_uri((const char *)value) < 0) {
+ refuse_stream(h2, stream_id);
+ return 0;
}
+
+ ctx->uri_path = malloc(sizeof(*ctx->uri_path) * (valuelen + 1));
+ if (!ctx->uri_path)
+ return kr_error(ENOMEM);
+ memcpy(ctx->uri_path, value, valuelen);
+ ctx->uri_path[valuelen] = '\0';
}
if (!strcasecmp(":method", (const char *)name)) {
}
}
- if (!strcasecmp("content-type", (const char *)name)) {
- ctx->content_type = malloc(sizeof(*ctx->content_type) * valuelen+1);
- if (!ctx->content_type)
- return NGHTTP2_ERR_CALLBACK_FAILURE;
- memcpy(ctx->content_type, value, valuelen);
- ctx->content_type[valuelen] = '\0';
- }
-
return 0;
}
struct http_ctx *ctx = (struct http_ctx *)user_data;
ssize_t remaining;
ssize_t required;
- assert(ctx->current_stream);
- bool is_first = queue_len(ctx->streams) == 0 || queue_tail(ctx->streams) != ctx->current_stream->stream_id;
+ bool is_first = queue_len(ctx->streams) == 0 || queue_tail(ctx->streams) != ctx->incomplete_stream;
- if (ctx->current_stream->stream_id != stream_id) {
+ if (ctx->incomplete_stream != stream_id) {
kr_log_verbose(
- "[http] stream %d incomplete\n",
- ctx->current_stream->stream_id);
- if (!set_error_status(ctx, stream_id, 501, "incomplete stream"))
- return NGHTTP2_ERR_CALLBACK_FAILURE;
+ "[http] stream %d incomplete, refusing\n",
+ ctx->incomplete_stream);
+ refuse_stream(h2, stream_id);
+ ctx->incomplete_stream = -1;
return 0;
}
if (required > remaining) {
kr_log_error("[http] insufficient space in buffer\n");
- if (!set_error_status(ctx, stream_id, 413, NULL)) {
- http_status_reinit_error(ctx, stream_id);
- return NGHTTP2_ERR_CALLBACK_FAILURE;
- }
- return 0;
+ ctx->incomplete_stream = -1;
+ return NGHTTP2_ERR_CALLBACK_FAILURE;
}
if (is_first) {
int32_t stream_id = frame->hd.stream_id;
assert(stream_id != -1);
- if (stream_id == 0 || ctx == NULL)
- return 0;
-
- if (ctx->current_method == HTTP_METHOD_NONE) {
- kr_log_verbose("[http] unsupported HTTP method\n");
- if (!set_error_status(ctx, stream_id, 405, "only HTTP POST and GET are supported\n")) {
- http_status_reinit_error(ctx, stream_id);
- return NGHTTP2_ERR_CALLBACK_FAILURE;
+ if ((frame->hd.flags & NGHTTP2_FLAG_END_STREAM) && ctx->incomplete_stream == stream_id) {
+ if (ctx->current_method == HTTP_METHOD_GET) {
+ if (process_uri_path(ctx, ctx->uri_path, stream_id) < 0) {
+ refuse_stream(h2, stream_id);
+ }
}
- }
+ ctx->incomplete_stream = -1;
+ ctx->current_method = HTTP_METHOD_NONE;
+ free(ctx->uri_path);
+ ctx->uri_path = NULL;
- if (ctx->content_type && strcasecmp("application/dns-message", (const char *)ctx->content_type)) {
- kr_log_verbose("[http] unsupported content-type %s\n", ctx->content_type);
- if (!set_error_status(ctx, stream_id, 415, "only Content-Type: application/dns-message is supported\n")) {
- http_status_reinit_error(ctx, stream_id);
+ len = ctx->buf_pos - sizeof(uint16_t);
+ if (len <= 0 || len > KNOT_WIRE_MAX_PKTSIZE) {
+ kr_log_verbose("[http] invalid dnsmsg size: %zd B\n", len);
return NGHTTP2_ERR_CALLBACK_FAILURE;
}
- }
- if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) {
- struct http_stream_status *stat = ctx->current_stream;
- assert(stat);
- if (stat->stream_id == stream_id) {
- if (stat->err_status == 200) {
- if (ctx->current_method == HTTP_METHOD_GET) {
- if (process_uri_path(ctx, stream_id) < 0) {
- http_status_reinit_error(ctx, stream_id);
- return NGHTTP2_ERR_CALLBACK_FAILURE;
- }
- }
-
- if (ctx->buf_pos) {
- len = ctx->buf_pos - sizeof(uint16_t);
- if (len <= 0 || len > KNOT_WIRE_MAX_PKTSIZE) {
- kr_log_verbose("[http] invalid dnsmsg size: %zd B\n", len);
- http_status_reinit_error(ctx, stream_id);
- return NGHTTP2_ERR_CALLBACK_FAILURE;
- }
-
- if (len < 12) {
- if (!set_error_status(ctx, stream_id, 400, "input too short\n")) {
- http_status_reinit_error(ctx, stream_id);
- return NGHTTP2_ERR_CALLBACK_FAILURE;
- }
- }
-
- if (stat->err_status == 200) {
- knot_wire_write_u16(ctx->buf, len);
- ctx->submitted += ctx->buf_pos;
- ctx->buf += ctx->buf_pos;
- }
- }
- }
-
- if (stat->err_status != 200) {
- if (send_err_status(ctx, stream_id) < 0) {
- http_status_reinit_error(ctx, stream_id);
- return NGHTTP2_ERR_CALLBACK_FAILURE;
- }
- }
-
- http_status_reinit(ctx, stream_id);
- ctx->buf_pos = 0;
- } else {
- /* send error for non-processed stream */
- if (send_err_status(ctx, stream_id) < 0) {
- http_status_reinit_error(ctx, stream_id);
- return NGHTTP2_ERR_CALLBACK_FAILURE;
- }
- }
+ knot_wire_write_u16(ctx->buf, len);
+ ctx->submitted += ctx->buf_pos;
+ ctx->buf += ctx->buf_pos;
+ ctx->buf_pos = 0;
}
return 0;
*/
static void on_pkt_write(struct http_data *data, int status)
{
- if (!data)
+ if (!data || !data->req || !data->on_write)
return;
- if (data->req && data->on_write)
- data->on_write(data->req, status);
+ data->on_write(data->req, status);
free(data);
}
static int on_stream_close_callback(nghttp2_session *h2, int32_t stream_id,
uint32_t error_code, void *user_data)
{
- struct http_ctx *ctx = (struct http_ctx *)user_data;
struct http_data *data;
- struct http_stream_status *stat;
- int ret;
data = nghttp2_session_get_stream_user_data(h2, stream_id);
if (data)
on_pkt_write(data, error_code == 0 ? 0 : kr_error(EIO));
- ret = trie_del(ctx->stream_status, (char *)&stream_id, sizeof(stream_id), (trie_val_t *)&stat);
- if (ret == 0)
- http_stream_status_free(stat);
-
return 0;
}
ctx->send_cb = send_cb;
ctx->session = session;
queue_init(ctx->streams);
- ctx->current_stream = NULL;
+ ctx->incomplete_stream = -1;
ctx->submitted = 0;
ctx->current_method = HTTP_METHOD_NONE;
ctx->uri_path = NULL;
- ctx->content_type = NULL;
- ctx->stream_status = trie_create(NULL);
-
nghttp2_session_server_new(&ctx->h2, callbacks, ctx);
nghttp2_submit_settings(ctx->h2, NGHTTP2_FLAG_NONE,
return ctx->submitted;
}
+/*
+ * Provide data from buffer to HTTP/2 library.
+ *
+ * To avoid copying the packet wire buffer, we use NGHTTP2_DATA_FLAG_NO_COPY
+ * and take care of sending entire DATA frames ourselves with nghttp2_send_data_callback.
+ *
+ * See https://www.nghttp2.org/documentation/types.html#c.nghttp2_data_source_read_callback
+ */
+static ssize_t read_callback(nghttp2_session *h2, int32_t stream_id, uint8_t *buf,
+ size_t length, uint32_t *data_flags,
+ nghttp2_data_source *source, void *user_data)
+{
+ struct http_data *data;
+ size_t avail;
+ size_t send;
+
+ data = (struct http_data*)source->ptr;
+ avail = data->len - data->pos;
+ send = MIN(avail, length);
+
+ if (avail == send)
+ *data_flags |= NGHTTP2_DATA_FLAG_EOF;
+
+ *data_flags |= NGHTTP2_DATA_FLAG_NO_COPY;
+ return send;
+}
+
/*
* Send dns response provided by the HTTTP/2 data provider.
*
if (!ctx)
return;
- trie_apply(ctx->stream_status, status_free, NULL);
- trie_free(ctx->stream_status);
-
queue_deinit(ctx->streams);
nghttp2_session_del(ctx->h2);
- free(ctx->content_type);
free(ctx);
}
-- SPDX-License-Identifier: GPL-3.0-or-later
local basexx = require('basexx')
local ffi = require('ffi')
-local monotime = require('cqueues').monotime
-
--- check prerequisites
-local timeout = 8 -- randomly chosen timeout by tkrizek
-local bound, port
-local host = '127.0.0.1'
-for _ = 1,10 do
- port = math.random(30000, 39999)
- bound = pcall(net.listen, host, port, { kind = 'doh2'})
- if bound then
- break
- end
-end
local function gen_huge_answer(_, req)
local answer = req:ensure_answer()
ffi.C.kr_pkt_make_auth_header(answer)
answer:rcode(kres.rcode.NOERROR)
+
-- 64k answer
answer:begin(kres.section.ANSWER)
answer:put('\4test\0', 300, answer:qclass(), kres.type.URI,
return pkt
end
-local function non_final_status(status)
- return status:sub(1, 1) == "1" and status ~= "101"
-end
-
-local function request_set_body(req, body)
- req['body'] = body
-end
-
-local function connection_connect(req)
- local client = require('http.client')
- local err, errno
- req['deadline'] = req['timeout'] and (monotime()+req['timeout'])
-
- connection, err, errno = client.connect({
- host = host;
- port = port;
- tls = true;
- ctx = req['ctx'];
- version = 2;
- h2_settings = { ENABLE_PUSH = false; };
- }, req['deadline'] and req['deadline']-monotime())
- if connection == nil then
- print('Connection error ' .. err .. ': ' .. errno)
- return false
- end
- -- Close the connection (and free resources) when done
- connection:onidle(connection.close)
- req['connection'] = connection
-
- return req
-end
-
-local function connection_init(time)
- local http_util = require('http.util')
- local ssl_ctx = require('openssl.ssl.context')
- local headers = require('http.headers').new()
- local request = {}
-
- headers:append(':method', 'GET')
- headers:upsert(':authority', http_util.to_authority(host, port, 'https'))
- headers:upsert(':path', '/dns-query')
- headers:upsert(':scheme', 'https')
- headers:upsert('user-agent', 'doh2.test.lua')
- headers:upsert('content-type', 'application/dns-message')
- request['headers'] = headers;
-
- local ctx = ssl_ctx.new()
- ctx:setVerify(ssl_ctx.VERIFY_NONE)
- request['ctx'] = ctx;
- request['timeout'] = time
-
- request = connection_connect(request)
-
- request['stream1'], err, errno = request['connection']:new_stream()
- if request['stream1'] == nil then
- return nil, err, errno
- end
- request['stream2'], err, errno = request['connection']:new_stream()
- if request['stream2'] == nil then
- return nil, err, errno
- end
-
- return request
-end
-
-local function set_headers_from_body(headers, body)
- local length
-
- if type(body) == "string" then
- length = #body
- end
- if length then
- headers:upsert("content-length", string.format("%d", #body))
- end
- if not length or length > 1024 then
- headers:append("expect", "100-continue")
- end
-
- return headers
-end
-
-local function send_data(req, stream_name, method, body)
- local pass, err, errno
- local new_headers = set_headers_from_body(req['headers'], body)
- local stream = req[stream_name]
-
- new_headers:upsert(':method', method)
- do -- Write outgoing headers
- pass, err, errno = stream:write_headers(new_headers, body == nil, req['deadline'] and req['deadline']-monotime())
- if not pass then
- stream:shutdown()
- return nil, err, errno
- end
- end
-
- if body then
- pass, err, errno = stream:write_body_from_string(body, req['deadline'] and req['deadline']-monotime())
- if not pass then
- stream:shutdown()
- return nil, err, errno
- end
- end
-
- return pass, err, errno
-end
-
-local function read_data(req, stream)
- local headers
- repeat
- local err, errno
- headers, err, errno = stream:get_headers(req['deadline'] and (req['deadline']-monotime()))
- if headers == nil then
- stream:shutdown()
- if err == nil then
- return nil, ce.strerror(ce.EPIPE), ce.EPIPE
- end
- return nil, err, errno
- end
- until not non_final_status(headers:get(":status"))
-
- return headers, stream
-end
-
-local function send_and_check_ok(req, method, desc)
- local pass, headers, stream, stream_check
-
- -- main request
- pass = send_data(req, 'stream1', method, req['body'])
- if not pass then
- return nil, nil
- end
-
- headers, stream, errno = read_data(req, req['stream1'])
+local function check_ok(req, desc)
+ local headers, stream, errno = req:go(8) -- randomly chosen timeout by tkrizek
if errno then
local errmsg = stream
nok(errmsg, desc .. ': ' .. errmsg)
- return nil, nil
+ return
end
same(tonumber(headers:get(':status')), 200, desc .. ': status 200')
same(headers:get('content-type'), 'application/dns-message', desc .. ': content-type')
- local answ_headers = headers
-
- -- test request - noerror.test. A
- req['headers']:upsert('content-type', 'application/dns-message')
- if method == 'GET' then
- req.headers:upsert(':path', '/dns-query?dns=vMEBAAABAAAAAAAAB25vZXJyb3IEdGVzdAAAAQAB')
- end
- pass = send_data(req, 'stream2', method, method == 'POST' and basexx.from_base64(
- 'vMEBAAABAAAAAAAAB25vZXJyb3IEdGVzdAAAAQAB') or nil) -- noerror.test. A
- if not pass then
- return nil, nil
- end
-
- headers, stream_check, errno = read_data(req, req['stream2'])
- if errno then
- local errmsg = stream_check
- nok(errmsg, desc .. ': ' .. errmsg)
- return nil, nil
- end
- same(tonumber(headers:get(':status')), 200, desc .. ' (test second stream): status 200')
- same(headers:get('content-type'), 'application/dns-message', desc .. ' (test second stream): content-type')
-
local body = assert(stream:get_body_as_string())
local pkt = parse_pkt(body, desc)
- req['stream1']:shutdown()
- req['stream2']:shutdown()
-
- return answ_headers, pkt
+ return headers, pkt
end
-local function send_and_check_err(req, method, exp_status, desc)
- local pass, headers, stream, stream_check
-
- -- main request
- pass = send_data(req, 'stream1', method, req['body'])
- if not pass then
- return
- end
-
- headers, stream, errno = read_data(req, req['stream1'])
- if errno then
- local errmsg = stream
- nok(errmsg, desc .. ': ' .. errmsg)
- return
- end
- local status = tonumber(headers:get(':status'))
- same(status, exp_status, desc .. ': get ' .. status)
+--local function check_err(req, exp_status, desc)
+-- local headers, errmsg, errno = req:go(8) -- randomly chosen timeout by tkrizek
+-- if errno then
+-- nok(errmsg, desc .. ': ' .. errmsg)
+-- return
+-- end
+-- local got_status = headers:get(':status')
+-- same(got_status, exp_status, desc)
+--end
- -- test request
- req['headers']:upsert('content-type', 'application/dns-message')
- if method ~= 'GET' and method ~= 'POST' then
- method = 'GET'
- end
- if method == 'GET' then
- req.headers:upsert(':path', '/dns-query?dns=vMEBAAABAAAAAAAAB25vZXJyb3IEdGVzdAAAAQAB')
- end
- pass = send_data(req, 'stream2', method, method == 'POST' and basexx.from_base64(
- 'vMEBAAABAAAAAAAAB25vZXJyb3IEdGVzdAAAAQAB') or nil) -- noerror.test. A
- if not pass then
- return
- end
- headers, stream_check, errno = read_data(req, req['stream2'])
- if errno then
- local errmsg = stream_check
- nok(errmsg, desc .. ': ' .. errmsg)
- return
+-- check prerequisites
+local bound, port
+local host = '127.0.0.1'
+for _ = 1,10 do
+ port = math.random(30000, 39999)
+ bound = pcall(net.listen, host, port, { kind = 'doh2'})
+ if bound then
+ break
end
- same(tonumber(headers:get(':status')), 200, desc .. ': second stream: status 200 (exp. 200)')
- same(headers:get('content-type'), 'application/dns-message', desc .. ': second stream: content-type')
- req['stream1']:shutdown()
- req['stream2']:shutdown()
end
-
if not bound then
-- skipping doh2 tests (failure to bind may be caused by missing support during build)
os.exit(77)
policy.add(policy.suffix(policy.DENY, policy.todnames({'nxdomain.test.'})))
policy.add(policy.suffix(gen_varying_ttls, policy.todnames({'noerror.test.'})))
+ local req_templ, uri_templ
+ local function start_server()
+ local request = require('http.request')
+ local ssl_ctx = require('openssl.ssl.context')
+ uri_templ = string.format('https://%s:%d/dns-query', host, port)
+ req_templ = assert(request.new_from_uri(uri_templ))
+ req_templ.headers:upsert('content-type', 'application/dns-message')
+ req_templ.ctx = ssl_ctx.new()
+ req_templ.ctx:setVerify(ssl_ctx.VERIFY_NONE)
+ end
+
-- test a valid DNS query using POST
local function test_post_servfail()
local desc = 'valid POST query which ends with SERVFAIL'
- local req = connection_init(timeout)
- request_set_body(req, basexx.from_base64( -- servfail.test. A
+ local req = req_templ:clone()
+ req.headers:upsert(':method', 'POST')
+ req:set_body(basexx.from_base64( -- servfail.test. A
'FZUBAAABAAAAAAAACHNlcnZmYWlsBHRlc3QAAAEAAQ=='))
- local headers, pkt = send_and_check_ok(req, 'POST', desc)
+ local headers, pkt = check_ok(req, desc)
if not (headers and pkt) then
return
end
local function test_post_noerror()
local desc = 'valid POST query which ends with NOERROR'
- local req = connection_init(timeout)
- request_set_body(req, basexx.from_base64( -- noerror.test. A
+ local req = req_templ:clone()
+ req.headers:upsert(':method', 'POST')
+ req:set_body(basexx.from_base64( -- noerror.test. A
'vMEBAAABAAAAAAAAB25vZXJyb3IEdGVzdAAAAQAB'))
- local headers, pkt = send_and_check_ok(req, 'POST', desc)
+ local headers, pkt = check_ok(req, desc)
if not (headers and pkt) then
return
end
local function test_post_nxdomain()
local desc = 'valid POST query which ends with NXDOMAIN'
- local req = connection_init(timeout)
- request_set_body(req, basexx.from_base64( -- nxdomain.test. A
+ local req = req_templ:clone()
+ req.headers:upsert(':method', 'POST')
+ req:set_body(basexx.from_base64( -- nxdomain.test. A
'viABAAABAAAAAAAACG54ZG9tYWluBHRlc3QAAAEAAQ=='))
- local headers, pkt = send_and_check_ok(req, 'POST', desc)
+ local headers, pkt = check_ok(req, desc)
if not (headers and pkt) then
return
end
local function test_huge_answer()
policy.add(policy.suffix(gen_huge_answer, policy.todnames({'huge.test'})))
local desc = 'POST query for a huge answer'
- local req = connection_init(timeout)
- request_set_body(req, basexx.from_base64( -- huge.test. URI, no EDNS
+ local req = req_templ:clone()
+ req.headers:upsert(':method', 'POST')
+ req:set_body(basexx.from_base64( -- huge.test. URI, no EDNS
'HHwBAAABAAAAAAAABGh1Z2UEdGVzdAABAAAB'))
- local _, pkt = send_and_check_ok(req, 'POST', desc)
+ local _, pkt = check_ok(req, desc)
same(pkt:rcode(), kres.rcode.NOERROR, desc .. ': rcode NOERROR')
same(pkt:tc(), false, desc .. ': no TC bit')
same(pkt:ancount(), 2, desc .. ': ANSWER contains both RRs')
end
-- test an invalid DNS query using POST
- local function test_post_short_input()
- local req = connection_init(timeout)
- request_set_body(req, string.rep('0', 11)) -- 11 bytes < DNS msg header
- send_and_check_err(req, 'POST', 400, 'too short POST finishes with 400')
- end
-
- local function test_post_unsupp_type()
- local req = connection_init(timeout)
- req['headers']:upsert('content-type', 'application/dns+json')
- request_set_body(req, string.rep('\0', 12)) -- valid message
- send_and_check_err(req, 'POST', 415, 'unsupported request content type finishes with 415')
- end
-
- local function test_get_right_endpoints()
- local desc = 'GET query with "doh" endpoint'
- local req = connection_init(timeout)
- req['headers']:upsert(':path', '/doh?dns=vMEBAAABAAAAAAAAB25vZXJyb3IEdGVzdAAAAQAB')
- send_and_check_ok(req, 'GET', desc)
-
- desc = 'GET query with "dns-query" endpoint'
- req = connection_init(timeout)
- req['headers']:upsert(':path', '/dns-query?dns=vMEBAAABAAAAAAAAB25vZXJyb3IEdGVzdAAAAQAB')
- send_and_check_ok(req, 'GET', desc)
- end
+-- local function test_post_short_input()
+-- local req = assert(req_templ:clone())
+-- req.headers:upsert(':method', 'POST')
+-- req:set_body(string.rep('0', 11)) -- 11 bytes < DNS msg header
+-- check_err(req, '400', 'too short POST finishes with 400')
+-- end
+--
+-- local function test_post_long_input()
+-- local req = assert(req_templ:clone())
+-- req.headers:upsert(':method', 'POST')
+-- req:set_body(string.rep('s', 1025)) -- > DNS msg over UDP
+-- check_err(req, '413', 'too long POST finishes with 413')
+-- end
+--
+-- local function test_post_unparseable_input()
+-- local req = assert(req_templ:clone())
+-- req.headers:upsert(':method', 'POST')
+-- req:set_body(string.rep('\0', 1024)) -- garbage
+-- check_err(req, '400', 'unparseable DNS message finishes with 400')
+-- end
+--
+-- local function test_post_unsupp_type()
+-- local req = assert(req_templ:clone())
+-- req.headers:upsert(':method', 'POST')
+-- req.headers:upsert('content-type', 'application/dns+json')
+-- req:set_body(string.rep('\0', 12)) -- valid message
+-- check_err(req, '415', 'unsupported request content type finishes with 415')
+-- end
-- test a valid DNS query using GET
local function test_get_servfail()
local desc = 'valid GET query which ends with SERVFAIL'
- local req = connection_init(timeout)
- req['headers']:upsert(':path', '/doh?dns=' -- servfail.test. A
+ local req = req_templ:clone()
+ req.headers:upsert(':method', 'GET')
+ req.headers:upsert(':path', '/doh?dns=' -- servfail.test. A
.. 'FZUBAAABAAAAAAAACHNlcnZmYWlsBHRlc3QAAAEAAQ')
- local headers, pkt = send_and_check_ok(req, 'GET', desc)
+ local headers, pkt = check_ok(req, desc)
if not (headers and pkt) then
return
end
local function test_get_noerror()
local desc = 'valid GET query which ends with NOERROR'
- local req = connection_init(timeout)
- req['headers']:upsert(':path', '/doh?dns=' -- noerror.test. A
+ local req = req_templ:clone()
+ req.headers:upsert(':method', 'GET')
+ req.headers:upsert(':path', '/doh?dns=' -- noerror.test. A
.. 'vMEBAAABAAAAAAAAB25vZXJyb3IEdGVzdAAAAQAB')
- local headers, pkt = send_and_check_ok(req, 'GET', desc)
+ local headers, pkt = check_ok(req, desc)
if not (headers and pkt) then
return
end
local function test_get_nxdomain()
local desc = 'valid GET query which ends with NXDOMAIN'
- local req = connection_init(timeout)
- req['headers']:upsert(':path', '/doh?dns=' -- nxdomain.test. A
+ local req = req_templ:clone()
+ req.headers:upsert(':method', 'GET')
+ req.headers:upsert(':path', '/doh?dns=' -- nxdomain.test. A
.. 'viABAAABAAAAAAAACG54ZG9tYWluBHRlc3QAAAEAAQ')
- local headers, pkt = send_and_check_ok(req, 'GET', desc)
+ local headers, pkt = check_ok(req, desc)
if not (headers and pkt) then
return
end
local function test_get_other_params_before_dns()
local desc = 'GET query with other parameters before dns is valid'
- local req = connection_init(timeout)
- req['headers']:upsert(':path',
- '/doh?other=something&another=something&dns=vMEBAAABAAAAAAAAB25vZXJyb3IEdGVzdAAAAQAB')
- send_and_check_ok(req, 'GET', desc)
+ local req = req_templ:clone()
+ req.headers:upsert(':method', 'GET')
+ req.headers:upsert(':path',
+ '/doh?other=something&another=something&dns=vMEBAAABAAAAAAAAB25vZXJyb3IEdGVzdAAAAQAB')
+ check_ok(req, desc)
end
local function test_get_other_params_after_dns()
local desc = 'GET query with other parameters after dns is valid'
- local req = connection_init(timeout)
- req['headers']:upsert(':path',
- '/doh?dns=vMEBAAABAAAAAAAAB25vZXJyb3IEdGVzdAAAAQAB&other=something&another=something')
- send_and_check_ok(req, 'GET', desc)
+ local req = req_templ:clone()
+ req.headers:upsert(':method', 'GET')
+ req.headers:upsert(':path',
+ '/doh?dns=vMEBAAABAAAAAAAAB25vZXJyb3IEdGVzdAAAAQAB&other=something&another=something')
+ check_ok(req, desc)
end
local function test_get_other_params()
local desc = 'GET query with other parameters than dns on both sides is valid'
- local req = connection_init(timeout)
+ local req = req_templ:clone()
+ req.headers:upsert(':method', 'GET')
req.headers:upsert(':path',
- '/doh?other=something&dns=vMEBAAABAAAAAAAAB25vZXJyb3IEdGVzdAAAAQAB&another=something')
- send_and_check_ok(req, 'GET', desc)
- end
-
- -- test an invalid DNS query using GET
- local function test_get_wrong_endpoints()
- local req = connection_init(timeout)
- req['headers']:upsert(':path', '/bad?dns=vMEBAAABAAAAAAAAB25vZXJyb3IEdGVzdAAAAQAB')
- send_and_check_err(req, 'GET', 400, 'wrong "bad" endpoint finishes with 400')
-
- req = connection_init(timeout)
- req['headers']:upsert(':path', '/dns?dns=vMEBAAABAAAAAAAAB25vZXJyb3IEdGVzdAAAAQAB')
- send_and_check_err(req, 'GET', 400, 'wrong "dns" endpoint finishes with 400')
- end
-
- local function test_get_no_dns_param()
- local req = connection_init(timeout)
- req['headers']:upsert(':path', '/doh?notdns=' .. basexx.to_url64(string.rep('\0', 1024)))
- send_and_check_err(req, 'GET', 400, 'GET without dns parameter finishes with 400')
- end
-
- local function test_get_unparseable()
- local req = connection_init(timeout)
- req['headers']:upsert(':path', '/doh??dns=' .. basexx.to_url64(string.rep('\0', 1024)))
- send_and_check_err(req, 'GET', 400, 'unparseable GET finishes with 400')
- end
-
- local function test_get_invalid_b64()
- local req = connection_init(timeout)
- req['headers']:upsert(':path', '/doh?dns=thisisnotb64')
- send_and_check_err(req, 'GET', 400, 'GET with invalid base64 finishes with 400')
- end
-
- local function test_get_invalid_chars()
- local req = connection_init(timeout)
- req['headers']:upsert(':path', '/doh?dns=' .. basexx.to_url64(string.rep('\0', 200)) .. '@#$%?!')
- send_and_check_err(req, 'GET', 400, 'GET with invalid characters in b64 finishes with 400')
- end
-
- local function test_get_two_ampersands()
- local req = connection_init(timeout)
- req['headers']:upsert(':path',
- '/doh?other=something&&dns=vMEBAAABAAAAAAAAB25vZXJyb3IEdGVzdAAAAQAB')
- send_and_check_err(req, 'GET', 400, 'GET with two ampersands finishes with 400')
-
- req = connection_init(timeout)
- req['headers']:upsert(':path',
- '/doh?other=something&&nodns=vMEBAAABAAAAAAAAB25vZXJyb3IEdGVzdAAAAQAB')
- send_and_check_err(req, 'GET', 400, 'GET with two ampersands finishes with 400')
- end
-
- local function test_unsupp_method()
- local req = connection_init(timeout)
- req['headers']:upsert(':method', 'PUT')
- send_and_check_err(req, 'PUT', 405, 'unsupported method finishes with 405')
- end
+ '/doh?other=something&dns=vMEBAAABAAAAAAAAB25vZXJyb3IEdGVzdAAAAQAB&another=something')
+ check_ok(req, desc)
+ end
+
+-- -- test an invalid DNS query using GET
+-- local function test_get_long_input()
+-- local req = assert(req_templ:clone())
+-- req.headers:upsert(':method', 'GET')
+-- req.headers:upsert(':path', '/doh?dns=' .. basexx.to_url64(string.rep('\0', 1030)))
+-- check_err(req, '414', 'too long GET finishes with 414')
+-- end
+--
+-- local function test_get_no_dns_param()
+-- local req = assert(req_templ:clone())
+-- req.headers:upsert(':method', 'GET')
+-- req.headers:upsert(':path', '/doh?notdns=' .. basexx.to_url64(string.rep('\0', 1024)))
+-- check_err(req, '400', 'GET without dns paramter finishes with 400')
+-- end
+--
+-- local function test_get_unparseable()
+-- local req = assert(req_templ:clone())
+-- req.headers:upsert(':method', 'GET')
+-- req.headers:upsert(':path', '/doh??dns=' .. basexx.to_url64(string.rep('\0', 1024)))
+-- check_err(req, '400', 'unparseable GET finishes with 400')
+-- end
+--
+-- local function test_get_invalid_b64()
+-- local req = assert(req_templ:clone())
+-- req.headers:upsert(':method', 'GET')
+-- req.headers:upsert(':path', '/doh?dns=thisisnotb64')
+-- check_err(req, '400', 'GET with invalid base64 finishes with 400')
+-- end
+--
+-- local function test_get_invalid_chars()
+-- local req = assert(req_templ:clone())
+-- req.headers:upsert(':method', 'GET')
+-- req.headers:upsert(':path', '/doh?dns=' .. basexx.to_url64(string.rep('\0', 200)) .. '@#$%?!')
+-- check_err(req, '400', 'GET with invalid characters in b64 finishes with 400')
+-- end
+--
+-- local function test_unsupp_method()
+-- local req = assert(req_templ:clone())
+-- req.headers:upsert(':method', 'PUT')
+-- check_err(req, '405', 'unsupported method finishes with 405')
+-- end
local function test_dstaddr()
local triggered = false
end
policy.add(policy.suffix(check_dstaddr, policy.todnames({'dstaddr.test'})))
local desc = 'valid POST query has server address available in request'
- local req = connection_init(timeout)
- request_set_body(req, basexx.from_base64( -- dstaddr.test. A
+ local req = req_templ:clone()
+ req.headers:upsert(':method', 'POST')
+ req:set_body(basexx.from_base64( -- dstaddr.test. A
'FnkBAAABAAAAAAAAB2RzdGFkZHIEdGVzdAAAAQAB'))
- send_and_check_ok(req, 'POST', desc)
+ check_ok(req, desc)
ok(triggered, 'dstaddr policy was triggered')
end
view:addr('::/0', policy_refuse)
local desc = 'valid POST query has source address available in request'
- local req = connection_init(timeout)
- request_set_body(req, basexx.from_base64( -- srcaddr.test.knot-resolver.cz TXT
+ local req = req_templ:clone()
+ req.headers:upsert(':method', 'POST')
+ req:set_body(basexx.from_base64( -- srcaddr.test.knot-resolver.cz TXT
'QNQBAAABAAAAAAAAB3NyY2FkZHIEdGVzdA1rbm90LXJlc29sdmVyAmN6AAAQAAE'))
- local _, pkt = send_and_check_ok(req, 'POST', desc)
+ local _, pkt = check_ok(req, desc)
same(pkt:rcode(), kres.rcode.REFUSED, desc .. ': view module caught it')
modules.unload('view')
end
+-- not implemented
+-- local function test_post_unsupp_accept()
+-- local req = assert(req_templ:clone())
+-- req.headers:upsert(':method', 'POST')
+-- req.headers:upsert('accept', 'application/dns+json')
+-- req:set_body(string.rep('\0', 12)) -- valid message
+-- check_err(req, '406', 'unsupported Accept type finishes with 406')
+-- end
+
-- plan tests
+ -- TODO: implement (some) of the error status codes
local tests = {
+ start_server,
test_post_servfail,
test_post_noerror,
test_post_nxdomain,
test_huge_answer,
- test_post_short_input,
- test_post_unsupp_type,
- test_get_right_endpoints,
+ --test_post_short_input,
+ --test_post_long_input,
+ --test_post_unparseable_input,
+ --test_post_unsupp_type,
test_get_servfail,
test_get_noerror,
test_get_nxdomain,
test_get_other_params_before_dns,
test_get_other_params_after_dns,
test_get_other_params,
- test_get_wrong_endpoints,
- test_get_no_dns_param,
- test_get_unparseable,
- test_get_invalid_b64,
- test_get_invalid_chars,
- test_get_two_ampersands,
- test_unsupp_method,
+ --test_get_long_input,
+ --test_get_no_dns_param,
+ --test_get_unparseable,
+ --test_get_invalid_b64,
+ --test_get_invalid_chars,
+ --test_unsupp_method,
test_dstaddr,
test_srcaddr
}