--- /dev/null
+/*
+ * Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * See the COPYRIGHT file distributed with this work for additional
+ * information regarding copyright ownership.
+ */
+
+# bad HTTP location
+http local-http-server {
+ endpoints { "dns-query"; };
+};
+
+options {
+ listen-on port 8080 tls none http local-http-server { 10.53.0.1; };
+};
--- /dev/null
+/*
+ * Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * See the COPYRIGHT file distributed with this work for additional
+ * information regarding copyright ownership.
+ */
+
+# bad HTTP location
+http local-http-server {
+ endpoints { "//"; };
+};
+
+options {
+ listen-on port 8080 tls none http local-http-server { 10.53.0.1; };
+};
--- /dev/null
+/*
+ * Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * See the COPYRIGHT file distributed with this work for additional
+ * information regarding copyright ownership.
+ */
+
+# bad HTTP location
+http local-http-server {
+ endpoints { "/dns-query?dns="; };
+};
+
+options {
+ listen-on port 8080 tls none http local-http-server { 10.53.0.1; };
+};
The following options can be specified in an ``http`` statement:
``endpoints``
- A list of HTTP query paths on which to listen. A typical path
- is "/dns-query".
+ A list of HTTP query paths on which to listen. This is the portion
+ of an :rfc:`3986`-compliant URI following the hostname; it must be
+ an absolute path, beginning with "/". A typical endpoint is "/dns-query".
-for example, the following configuration enables DNS-over-HTTPS queries on
+For example, the following configuration enables DNS-over-HTTPS queries on
all local addresses:
::
return (result);
}
+#if HAVE_LIBNGHTTP2
+static isc_result_t
+bind9_check_httpserver(const cfg_obj_t *http, isc_log_t *logctx,
+ isc_symtab_t *symtab, isc_mem_t *mctx) {
+ isc_result_t result, tresult;
+ const char *name = cfg_obj_asstring(cfg_map_getname(http));
+ char *tmp = isc_mem_strdup(mctx, name);
+ const cfg_obj_t *eps = NULL;
+ const cfg_listelt_t *elt = NULL;
+ isc_symvalue_t symvalue;
+
+ /* Check for duplicates */
+ symvalue.as_cpointer = http;
+ result = isc_symtab_define(symtab, tmp, 1, symvalue,
+ isc_symexists_reject);
+ if (result == ISC_R_EXISTS) {
+ const char *file = NULL;
+ unsigned int line;
+
+ tresult = isc_symtab_lookup(symtab, tmp, 1, &symvalue);
+ RUNTIME_CHECK(tresult == ISC_R_SUCCESS);
+
+ line = cfg_obj_line(symvalue.as_cpointer);
+ file = cfg_obj_file(symvalue.as_cpointer);
+ if (file == NULL) {
+ file = "<unknown file>";
+ }
+
+ cfg_obj_log(http, logctx, ISC_LOG_ERROR,
+ "http '%s' is duplicated: "
+ "also defined at %s:%u",
+ name, file, line);
+ }
+ isc_mem_free(mctx, tmp);
+
+ /* Check endpoints are valid */
+ tresult = cfg_map_get(http, "endpoints", &eps);
+ RUNTIME_CHECK(tresult == ISC_R_SUCCESS);
+ for (elt = cfg_list_first(eps); elt != NULL; elt = cfg_list_next(elt)) {
+ const cfg_obj_t *ep = cfg_listelt_value(elt);
+ const char *path = cfg_obj_asstring(ep);
+ if (!isc_nm_http_path_isvalid(path)) {
+ cfg_obj_log(eps, logctx, ISC_LOG_ERROR,
+ "endpoint '%s' is not a "
+ "valid absolute HTTP path",
+ path);
+ if (result == ISC_R_SUCCESS) {
+ result = ISC_R_FAILURE;
+ }
+ }
+ }
+
+ return (result);
+}
+
+static isc_result_t
+bind9_check_httpservers(const cfg_obj_t *config, isc_log_t *logctx,
+ isc_mem_t *mctx) {
+ isc_result_t result, tresult;
+ const cfg_obj_t *obj = NULL;
+ const cfg_listelt_t *elt = NULL;
+ isc_symtab_t *symtab = NULL;
+
+ result = isc_symtab_create(mctx, 100, NULL, NULL, false, &symtab);
+ if (result != ISC_R_SUCCESS) {
+ return (result);
+ }
+
+ result = cfg_map_get(config, "http", &obj);
+ if (result != ISC_R_SUCCESS) {
+ result = ISC_R_SUCCESS;
+ goto done;
+ }
+
+ for (elt = cfg_list_first(obj); elt != NULL; elt = cfg_list_next(elt)) {
+ obj = cfg_listelt_value(elt);
+ tresult = bind9_check_httpserver(obj, logctx, symtab, mctx);
+ if (result == ISC_R_SUCCESS) {
+ result = tresult;
+ }
+ }
+
+done:
+ isc_symtab_destroy(&symtab);
+ return (result);
+}
+#endif /* HAVE_LIBNGHTTP2 */
+
static isc_result_t
get_remotes(const cfg_obj_t *cctx, const char *list, const char *name,
const cfg_obj_t **ret) {
result = ISC_R_FAILURE;
}
+#if HAVE_LIBNGHTTP2
+ if (bind9_check_httpservers(config, logctx, mctx) != ISC_R_SUCCESS) {
+ result = ISC_R_FAILURE;
+ }
+#endif /* HAVE_LIBNGHTTP2 */
+
(void)cfg_map_get(config, "view", &views);
if (views != NULL && options != NULL) {
isc_result_t
isc_nm_http_endpoint(isc_nmsocket_t *sock, const char *uri, isc_nm_recv_cb_t cb,
void *cbarg, size_t extrahandlesize);
-#endif
bool
isc_nm_is_http_handle(isc_nmhandle_t *handle);
+bool
+isc_nm_http_path_isvalid(const char *path);
+#endif /* HAVE_LIBNGHTTP2 */
+
void
isc_nm_bad_request(isc_nmhandle_t *handle);
/*%<
}
socket->h2.request_path = isc_mem_strndup(
socket->mgr->mctx, (const char *)value, vlen + 1);
+
+ if (!isc_nm_http_path_isvalid(socket->h2.request_path)) {
+ isc_mem_free(socket->mgr->mctx, socket->h2.request_path);
+ socket->h2.request_path = NULL;
+ return (ISC_HTTP_ERROR_BAD_REQUEST);
+ }
+
handler = find_server_request_handler(socket->h2.request_path,
socket->h2.session->serversocket);
if (handler != NULL) {
REQUIRE(VALID_NMSOCK(sock));
REQUIRE(sock->type == isc_nm_httplistener);
- REQUIRE(uri != NULL && *uri != '\0');
+ REQUIRE(isc_nm_http_path_isvalid(uri));
httpcbarg = isc_mem_get(sock->mgr->mctx, sizeof(isc_nm_httpcbarg_t));
*httpcbarg = (isc_nm_httpcbarg_t){ .cb = cb, .cbarg = cbarg };
#define MATCH(ch) (st->str[0] == (ch))
#define MATCH_ALPHA() isalpha((unsigned char)(st->str[0]))
+#define MATCH_DIGIT() isdigit((unsigned char)(st->str[0]))
#define MATCH_ALNUM() isalnum((unsigned char)(st->str[0]))
#define MATCH_XDIGIT() isxdigit((unsigned char)(st->str[0]))
#define ADVANCE() st->str++
return (true);
}
+
+/*
+ * DoH URL Location Verifier. Based on the following grammar (EBNF/WSN
+ * notation):
+ *
+ * S = path_absolute.
+ * path_absolute = '/' [ segments ] '\0'.
+ * segments = segment_nz { slash_segment }.
+ * slash_segment = '/' segment.
+ * segment = { pchar }.
+ * segment_nz = pchar { pchar }.
+ * pchar = unreserved | pct_encoded | sub_delims | ':' | '@'.
+ * unreserved = ALPHA | DIGIT | '-' | '.' | '_' | '~'.
+ * pct_encoded = '%' XDIGIT XDIGIT.
+ * sub_delims = '!' | '$' | '&' | '\'' | '(' | ')' | '*' | '+' |
+ * ',' | ';' | '='.
+ *
+ * The grammar is extracted from RFC 3986. It is slightly modified to
+ * aid in parser creation, but the end result is the same
+ * (path_absolute is defined slightly differently - split into
+ * multiple productions).
+ *
+ * https://datatracker.ietf.org/doc/html/rfc3986#appendix-A
+ */
+
+typedef struct isc_http_location_parser_state {
+ const char *str;
+} isc_http_location_parser_state_t;
+
+static bool
+rule_loc_path_absolute(isc_http_location_parser_state_t *);
+
+static bool
+rule_loc_segments(isc_http_location_parser_state_t *);
+
+static bool
+rule_loc_slash_segment(isc_http_location_parser_state_t *);
+
+static bool
+rule_loc_segment(isc_http_location_parser_state_t *);
+
+static bool
+rule_loc_segment_nz(isc_http_location_parser_state_t *);
+
+static bool
+rule_loc_pchar(isc_http_location_parser_state_t *);
+
+static bool
+rule_loc_unreserved(isc_http_location_parser_state_t *);
+
+static bool
+rule_loc_pct_encoded(isc_http_location_parser_state_t *);
+
+static bool
+rule_loc_sub_delims(isc_http_location_parser_state_t *);
+
+static bool
+rule_loc_path_absolute(isc_http_location_parser_state_t *st) {
+ if (MATCH('/')) {
+ ADVANCE();
+ } else {
+ return (false);
+ }
+
+ (void)rule_loc_segments(st);
+
+ if (MATCH('\0')) {
+ ADVANCE();
+ } else {
+ return (false);
+ }
+
+ return (true);
+}
+
+static bool
+rule_loc_segments(isc_http_location_parser_state_t *st) {
+ if (!rule_loc_segment_nz(st)) {
+ return (false);
+ }
+
+ while (rule_loc_slash_segment(st)) {
+ /* zero or more */;
+ }
+
+ return (true);
+}
+
+static bool
+rule_loc_slash_segment(isc_http_location_parser_state_t *st) {
+ if (MATCH('/')) {
+ ADVANCE();
+ } else {
+ return (false);
+ }
+
+ return (rule_loc_segment(st));
+}
+
+static bool
+rule_loc_segment(isc_http_location_parser_state_t *st) {
+ while (rule_loc_pchar(st)) {
+ /* zero or more */;
+ }
+
+ return (true);
+}
+
+static bool
+rule_loc_segment_nz(isc_http_location_parser_state_t *st) {
+ if (!rule_loc_pchar(st)) {
+ return (false);
+ }
+
+ while (rule_loc_pchar(st)) {
+ /* zero or more */;
+ }
+
+ return (true);
+}
+
+static bool
+rule_loc_pchar(isc_http_location_parser_state_t *st) {
+ if (rule_loc_unreserved(st)) {
+ return (true);
+ } else if (rule_loc_pct_encoded(st)) {
+ return (true);
+ } else if (rule_loc_sub_delims(st)) {
+ return (true);
+ } else if (MATCH(':') || MATCH('@')) {
+ ADVANCE();
+ return (true);
+ }
+
+ return (false);
+}
+
+static bool
+rule_loc_unreserved(isc_http_location_parser_state_t *st) {
+ if (MATCH_ALPHA() | MATCH_DIGIT() | MATCH('-') | MATCH('.') |
+ MATCH('_') | MATCH('~'))
+ {
+ ADVANCE();
+ return (true);
+ }
+ return (false);
+}
+
+static bool
+rule_loc_pct_encoded(isc_http_location_parser_state_t *st) {
+ if (!MATCH('%')) {
+ return (false);
+ }
+ ADVANCE();
+
+ if (!MATCH_XDIGIT()) {
+ return (false);
+ }
+ ADVANCE();
+
+ if (!MATCH_XDIGIT()) {
+ return (false);
+ }
+ ADVANCE();
+
+ return (true);
+}
+
+static bool
+rule_loc_sub_delims(isc_http_location_parser_state_t *st) {
+ if (MATCH('!') | MATCH('$') | MATCH('&') | MATCH('\'') | MATCH('(') |
+ MATCH(')') | MATCH('*') | MATCH('+') | MATCH(',') | MATCH(';') |
+ MATCH('='))
+ {
+ ADVANCE();
+ return (true);
+ }
+
+ return (false);
+}
+
+bool
+isc_nm_http_path_isvalid(const char *path) {
+ isc_http_location_parser_state_t state = { 0 };
+
+ REQUIRE(path != NULL);
+
+ state.str = path;
+
+ return (rule_loc_path_absolute(&state));
+}
}
}
+static void
+doh_path_validation(void **state) {
+ UNUSED(state);
+
+ assert_true(isc_nm_http_path_isvalid("/"));
+ assert_true(isc_nm_http_path_isvalid(DOH_PATH));
+ assert_false(isc_nm_http_path_isvalid("laaaa"));
+ assert_false(isc_nm_http_path_isvalid(""));
+ assert_false(isc_nm_http_path_isvalid("//"));
+ assert_true(isc_nm_http_path_isvalid("/lala///"));
+ assert_true(isc_nm_http_path_isvalid("/lalaaaaaa"));
+ assert_true(isc_nm_http_path_isvalid("/lalaaa/la/la/la"));
+ assert_true(isc_nm_http_path_isvalid("/la/a"));
+ assert_true(isc_nm_http_path_isvalid("/la+la"));
+ assert_true(isc_nm_http_path_isvalid("/la&la/la*la/l-a_/la!/la\'"));
+ assert_true(isc_nm_http_path_isvalid("/la/(la)/la"));
+ assert_true(isc_nm_http_path_isvalid("/la,la,la"));
+ assert_true(isc_nm_http_path_isvalid("/la-'la'-la"));
+ assert_true(isc_nm_http_path_isvalid("/la:la=la"));
+ assert_true(isc_nm_http_path_isvalid("/l@l@l@"));
+ assert_false(isc_nm_http_path_isvalid("/#lala"));
+ assert_true(isc_nm_http_path_isvalid("/lala;la"));
+ assert_false(
+ isc_nm_http_path_isvalid("la&la/laalaala*lala/l-al_a/lal!/"));
+ assert_true(isc_nm_http_path_isvalid("/Lal/lAla.jpg"));
+
+ /* had to replace ? with ! because it does not verify a query string */
+ assert_true(isc_nm_http_path_isvalid("/watch!v=oavMtUWDBTM"));
+ assert_false(isc_nm_http_path_isvalid("/watch?v=dQw4w9WgXcQ"));
+ assert_true(isc_nm_http_path_isvalid("/datatracker.ietf.org/doc/html/"
+ "rfc2616"));
+ assert_true(isc_nm_http_path_isvalid("/doc/html/rfc8484"));
+ assert_true(isc_nm_http_path_isvalid("/123"));
+}
+
int
main(void) {
const struct CMUnitTest tests_short[] = {
NULL),
cmocka_unit_test_setup_teardown(doh_base64_to_base64url, NULL,
NULL),
+ cmocka_unit_test_setup_teardown(doh_path_validation, NULL,
+ NULL),
cmocka_unit_test_setup_teardown(doh_noop_POST, nm_setup,
nm_teardown),
cmocka_unit_test_setup_teardown(doh_noop_GET, nm_setup,
NULL),
cmocka_unit_test_setup_teardown(doh_base64_to_base64url, NULL,
NULL),
+ cmocka_unit_test_setup_teardown(doh_path_validation, NULL,
+ NULL),
cmocka_unit_test_setup_teardown(doh_noop_POST, nm_setup,
nm_teardown),
cmocka_unit_test_setup_teardown(doh_noop_GET, nm_setup,
static cfg_type_t cfg_type_bracketed_namesockaddrkeylist;
static cfg_type_t cfg_type_bracketed_netaddrlist;
static cfg_type_t cfg_type_bracketed_sockaddrnameportlist;
-static cfg_type_t cfg_type_bracketed_qstring_list;
+static cfg_type_t cfg_type_bracketed_http_endpoint_list;
static cfg_type_t cfg_type_controls;
static cfg_type_t cfg_type_controls_sockaddr;
static cfg_type_t cfg_type_destinationlist;
/* http and https */
-static cfg_type_t cfg_type_bracketed_qstring_list = { "bracketed_qstring_list",
- cfg_parse_bracketed_list,
- cfg_print_bracketed_list,
- cfg_doc_bracketed_list,
- &cfg_rep_list,
- &cfg_type_qstring };
+static cfg_type_t cfg_type_bracketed_http_endpoint_list = {
+ "bracketed_http_endpoint_list",
+ cfg_parse_bracketed_list,
+ cfg_print_bracketed_list,
+ cfg_doc_bracketed_list,
+ &cfg_rep_list,
+ &cfg_type_qstring
+};
static cfg_clausedef_t cfg_http_description_clauses[] = {
- { "endpoints", &cfg_type_bracketed_qstring_list, 0 }, { NULL, NULL, 0 }
+ { "endpoints", &cfg_type_bracketed_http_endpoint_list, 0 },
};
static cfg_clausedef_t *http_description_clausesets[] = {
#include <isc/mem.h>
#include <isc/net.h>
#include <isc/netaddr.h>
+#include <isc/netmgr.h>
#include <isc/netscope.h>
#include <isc/print.h>
#include <isc/sockaddr.h>
if (result == ISC_R_SUCCESS) {
for (size_t i = 0; i < neps; i++) {
+ INSIST(isc_nm_http_path_isvalid(eps[i]));
result = isc_nm_http_endpoint(sock, eps[i],
ns__client_request, ifp,
sizeof(ns_client_t));