test1658: add unit test for the HTTPS RR decoder

Made the HTTPS-RR parser a little stricter while at it.

Drop the ALPN escape handling, that was not needed.

Make the hode handle (and ignore) duplicate ALPN entries.

Closes #16972
This commit is contained in:
Daniel Stenberg 2025-04-04 23:21:41 +02:00
parent 023cc8d595
commit badfb951ec
No known key found for this signature in database
GPG key ID: 5CC908FDB71E12C2
9 changed files with 645 additions and 77 deletions

View file

@ -38,11 +38,13 @@
#include "connect.h"
#include "strdup.h"
#include "dynbuf.h"
#include "escape.h"
#include "urlapi-int.h"
/* The last 3 #include files should be in this order */
#include "curl_printf.h"
#include "curl_memory.h"
#include "memdebug.h"
#include "escape.h"
#define DNS_CLASS_IN 0x01
@ -1014,10 +1016,10 @@ UNITTEST void de_cleanup(struct dohentry *d)
* just after the end of the DNS name encoding on output. (And
* that is why it is an "unsigned char **" :-)
*/
static CURLcode doh_decode_rdata_name(unsigned char **buf, size_t *remaining,
char **dnsname)
static CURLcode doh_decode_rdata_name(const unsigned char **buf,
size_t *remaining, char **dnsname)
{
unsigned char *cp = NULL;
const unsigned char *cp = NULL;
size_t rem = 0;
unsigned char clen = 0; /* chunk len */
struct dynbuf thename;
@ -1057,43 +1059,23 @@ static CURLcode doh_decode_rdata_name(unsigned char **buf, size_t *remaining,
return CURLE_OK;
}
#ifdef DEBUGBUILD
static CURLcode doh_test_alpn_escapes(void)
{
/* we will use an example from draft-ietf-dnsop-svcb, figure 10 */
static unsigned char example[] = {
0x08, /* length 8 */
0x66, 0x5c, 0x6f, 0x6f, 0x2c, 0x62, 0x61, 0x72, /* value "f\\oo,bar" */
0x02, /* length 2 */
0x68, 0x32 /* value "h2" */
};
size_t example_len = sizeof(example);
unsigned char aval[MAX_HTTPSRR_ALPNS] = { 0 };
static const char expected[2] = { ALPN_h2, ALPN_none };
UNITTEST CURLcode doh_resp_decode_httpsrr(struct Curl_easy *data,
const unsigned char *cp, size_t len,
struct Curl_https_rrinfo **hrr);
if(Curl_httpsrr_decode_alpn(example, example_len, aval) != CURLE_OK)
return CURLE_BAD_CONTENT_ENCODING;
if(memcmp(aval, expected, sizeof(expected)))
return CURLE_BAD_CONTENT_ENCODING;
return CURLE_OK;
}
#endif
static CURLcode doh_resp_decode_httpsrr(struct Curl_easy *data,
unsigned char *cp, size_t len,
struct Curl_https_rrinfo **hrr)
/* @unittest 1658 */
UNITTEST CURLcode doh_resp_decode_httpsrr(struct Curl_easy *data,
const unsigned char *cp, size_t len,
struct Curl_https_rrinfo **hrr)
{
uint16_t pcode = 0, plen = 0;
uint32_t expected_min_pcode = 0;
struct Curl_https_rrinfo *lhrr = NULL;
char *dnsname = NULL;
CURLcode result = CURLE_OUT_OF_MEMORY;
size_t olen;
#ifdef DEBUGBUILD
/* a few tests of escaping, should not be here but ok for now */
if(doh_test_alpn_escapes() != CURLE_OK)
return CURLE_OUT_OF_MEMORY;
#endif
*hrr = NULL;
if(len <= 2)
return CURLE_BAD_FUNCTION_ARGUMENT;
lhrr = calloc(1, sizeof(struct Curl_https_rrinfo));
@ -1105,6 +1087,11 @@ static CURLcode doh_resp_decode_httpsrr(struct Curl_easy *data,
if(doh_decode_rdata_name(&cp, &len, &dnsname) != CURLE_OK)
goto err;
lhrr->target = dnsname;
if(Curl_junkscan(dnsname, &olen, FALSE)) {
/* unacceptable hostname content */
result = CURLE_WEIRD_SERVER_REPLY;
goto err;
}
lhrr->port = -1; /* until set */
while(len >= 4) {
pcode = doh_get16bit(cp, 0);
@ -1131,9 +1118,12 @@ err:
return result;
}
# ifdef DEBUGBUILD
static void doh_print_httpsrr(struct Curl_easy *data,
struct Curl_https_rrinfo *hrr)
#ifdef DEBUGBUILD
UNITTEST void doh_print_httpsrr(struct Curl_easy *data,
struct Curl_https_rrinfo *hrr);
UNITTEST void doh_print_httpsrr(struct Curl_easy *data,
struct Curl_https_rrinfo *hrr)
{
DEBUGASSERT(hrr);
infof(data, "HTTPS RR: priority %d, target: %s",

View file

@ -38,76 +38,67 @@
#include "curl_memory.h"
#include "memdebug.h"
CURLcode Curl_httpsrr_decode_alpn(const unsigned char *cp, size_t len,
unsigned char *alpns)
#define MAX_ALPN_LENGTH 255
static CURLcode httpsrr_decode_alpn(const char *cp, size_t len,
unsigned char *alpns)
{
/*
* spec here is as per RFC 9460, section-7.1.1
* encoding is a concatenated list of strings each preceded by a one
* octet length
* output is comma-sep list of the strings
* implementations may or may not handle quoting of comma within
* string values, so we might see a comma within the wire format
* version of a string, in which case we will precede that by a
* backslash - same goes for a backslash character, and of course
* we need to use two backslashes in strings when we mean one;-)
* The wire-format value for "alpn" consists of at least one alpn-id
* prefixed by its length as a single octet, and these length-value pairs
* are concatenated to form the SvcParamValue. These pairs MUST exactly fill
* the SvcParamValue; otherwise, the SvcParamValue is malformed.
*/
struct dynbuf dval;
int idnum = 0;
Curl_dyn_init(&dval, DYN_DOH_RESPONSE);
while(len > 0) {
size_t tlen = (size_t) *cp++;
size_t i;
enum alpnid id;
len--;
if(tlen > len)
goto err;
/* add escape char if needed, clunky but easier to read */
for(i = 0; i != tlen; i++) {
if('\\' == *cp || ',' == *cp) {
if(Curl_dyn_addn(&dval, "\\", 1))
goto err;
}
if(Curl_dyn_addn(&dval, cp++, 1))
goto err;
}
len -= tlen;
return CURLE_BAD_CONTENT_ENCODING;
/* we only store ALPN ids we know about */
id = Curl_alpn2alpnid(Curl_dyn_ptr(&dval), Curl_dyn_len(&dval));
id = Curl_alpn2alpnid(cp, tlen);
if(id != ALPN_none) {
if(idnum == MAX_HTTPSRR_ALPNS)
break;
alpns[idnum++] = (unsigned char)id;
if(idnum && memchr(alpns, id, idnum))
/* this ALPN id is already stored */
;
else
alpns[idnum++] = (unsigned char)id;
}
Curl_dyn_reset(&dval);
cp += tlen;
len -= tlen;
}
Curl_dyn_free(&dval);
if(idnum < MAX_HTTPSRR_ALPNS)
alpns[idnum] = ALPN_none; /* terminate the list */
return CURLE_OK;
err:
Curl_dyn_free(&dval);
return CURLE_BAD_CONTENT_ENCODING;
}
CURLcode Curl_httpsrr_set(struct Curl_easy *data,
struct Curl_https_rrinfo *hi,
uint16_t rrkey, const uint8_t *val, size_t vlen)
{
CURLcode result = CURLE_OK;
switch(rrkey) {
case HTTPS_RR_CODE_MANDATORY:
CURL_TRC_DNS(data, "HTTPS RR MANDATORY left to implement");
break;
case HTTPS_RR_CODE_ALPN: /* str_list */
Curl_httpsrr_decode_alpn(val, vlen, hi->alpns);
result = httpsrr_decode_alpn((const char *)val, vlen, hi->alpns);
CURL_TRC_DNS(data, "HTTPS RR ALPN: %u %u %u %u",
hi->alpns[0], hi->alpns[1], hi->alpns[2], hi->alpns[3]);
break;
case HTTPS_RR_CODE_NO_DEF_ALPN:
if(vlen) /* no data */
return CURLE_BAD_FUNCTION_ARGUMENT;
hi->no_def_alpn = TRUE;
CURL_TRC_DNS(data, "HTTPS RR no-def-alpn");
break;
case HTTPS_RR_CODE_IPV4: /* addr4 list */
if(!vlen)
if(!vlen || (vlen & 3)) /* the size must be 4-byte aligned */
return CURLE_BAD_FUNCTION_ARGUMENT;
hi->ipv4hints = Curl_memdup(val, vlen);
if(!hi->ipv4hints)
@ -125,7 +116,7 @@ CURLcode Curl_httpsrr_set(struct Curl_easy *data,
CURL_TRC_DNS(data, "HTTPS RR ECH");
break;
case HTTPS_RR_CODE_IPV6: /* addr6 list */
if(!vlen)
if(!vlen || (vlen & 15)) /* the size must be 16-byte aligned */
return CURLE_BAD_FUNCTION_ARGUMENT;
hi->ipv6hints = Curl_memdup(val, vlen);
if(!hi->ipv6hints)
@ -143,7 +134,7 @@ CURLcode Curl_httpsrr_set(struct Curl_easy *data,
CURL_TRC_DNS(data, "HTTPS RR unknown code");
break;
}
return CURLE_OK;
return result;
}
struct Curl_https_rrinfo *

View file

@ -68,6 +68,7 @@ void Curl_httpsrr_cleanup(struct Curl_https_rrinfo *rrinfo);
/*
* Code points for DNS wire format SvcParams as per RFC 9460
*/
#define HTTPS_RR_CODE_MANDATORY 0x00
#define HTTPS_RR_CODE_ALPN 0x01
#define HTTPS_RR_CODE_NO_DEF_ALPN 0x02
#define HTTPS_RR_CODE_PORT 0x03
@ -75,9 +76,6 @@ void Curl_httpsrr_cleanup(struct Curl_https_rrinfo *rrinfo);
#define HTTPS_RR_CODE_ECH 0x05
#define HTTPS_RR_CODE_IPV6 0x06
CURLcode Curl_httpsrr_decode_alpn(const unsigned char *cp, size_t len,
unsigned char *alpns);
#if defined(USE_ARES)
void Curl_dnsrec_done_cb(void *arg, ares_status_t status,
size_t timeouts,

View file

@ -30,6 +30,8 @@ size_t Curl_is_absolute_url(const char *url, char *buf, size_t buflen,
CURLUcode Curl_url_set_authority(CURLU *u, const char *authority);
CURLUcode Curl_junkscan(const char *url, size_t *urllen, bool allowspace);
#ifdef UNITTESTS
UNITTEST CURLUcode Curl_parse_port(struct Curl_URL *u, struct dynbuf *host,
bool has_scheme);

View file

@ -303,7 +303,7 @@ static CURLUcode redirect_url(const char *base, const char *relurl,
}
/* scan for byte values <= 31, 127 and sometimes space */
static CURLUcode junkscan(const char *url, size_t *urllen, bool allowspace)
CURLUcode Curl_junkscan(const char *url, size_t *urllen, bool allowspace)
{
size_t n = strlen(url);
size_t i;
@ -918,7 +918,7 @@ static CURLUcode parseurl(const char *url, CURLU *u, unsigned int flags)
Curl_dyn_init(&host, CURL_MAX_INPUT_LENGTH);
result = junkscan(url, &urllen, !!(flags & CURLU_ALLOW_SPACE));
result = Curl_junkscan(url, &urllen, !!(flags & CURLU_ALLOW_SPACE));
if(result)
goto fail;