#include "curl_setup.h"
#if !defined(CURL_DISABLE_HTTP) && !defined(CURL_DISABLE_ALTSVC)
#include <curl/curl.h>
#include "urldata.h"
#include "altsvc.h"
#include "curl_get_line.h"
#include "strcase.h"
#include "parsedate.h"
#include "sendf.h"
#include "curlx/warnless.h"
#include "fopen.h"
#include "rename.h"
#include "strdup.h"
#include "curlx/inet_pton.h"
#include "curlx/strparse.h"
#include "connect.h"
#include "curl_printf.h"
#include "curl_memory.h"
#include "memdebug.h"
#define MAX_ALTSVC_LINE 4095
#define MAX_ALTSVC_DATELEN 256
#define MAX_ALTSVC_HOSTLEN 2048
#define MAX_ALTSVC_ALPNLEN 10
#define H3VERSION "h3"
const char *Curl_alpnid2str(enum alpnid id)
{
switch(id) {
case ALPN_h1:
return "h1";
case ALPN_h2:
return "h2";
case ALPN_h3:
return H3VERSION;
default:
return "";
}
}
static void altsvc_free(struct altsvc *as)
{
free(as->src.host);
free(as->dst.host);
free(as);
}
static struct altsvc *altsvc_createid(const char *srchost,
size_t hlen,
const char *dsthost,
size_t dlen,
enum alpnid srcalpnid,
enum alpnid dstalpnid,
size_t srcport,
size_t dstport)
{
struct altsvc *as = calloc(1, sizeof(struct altsvc));
if(!as)
return NULL;
DEBUGASSERT(hlen);
DEBUGASSERT(dlen);
if(!hlen || !dlen)
goto error;
if((hlen > 2) && srchost[0] == '[') {
srchost++;
hlen -= 2;
}
else if(srchost[hlen - 1] == '.') {
hlen--;
if(!hlen)
goto error;
}
if((dlen > 2) && dsthost[0] == '[') {
dsthost++;
dlen -= 2;
}
as->src.host = Curl_memdup0(srchost, hlen);
if(!as->src.host)
goto error;
as->dst.host = Curl_memdup0(dsthost, dlen);
if(!as->dst.host)
goto error;
as->src.alpnid = srcalpnid;
as->dst.alpnid = dstalpnid;
as->src.port = (unsigned short)srcport;
as->dst.port = (unsigned short)dstport;
return as;
error:
altsvc_free(as);
return NULL;
}
static struct altsvc *altsvc_create(struct Curl_str *srchost,
struct Curl_str *dsthost,
struct Curl_str *srcalpn,
struct Curl_str *dstalpn,
size_t srcport,
size_t dstport)
{
enum alpnid dstalpnid =
Curl_alpn2alpnid(curlx_str(dstalpn), curlx_strlen(dstalpn));
enum alpnid srcalpnid =
Curl_alpn2alpnid(curlx_str(srcalpn), curlx_strlen(srcalpn));
if(!srcalpnid || !dstalpnid)
return NULL;
return altsvc_createid(curlx_str(srchost), curlx_strlen(srchost),
curlx_str(dsthost), curlx_strlen(dsthost),
srcalpnid, dstalpnid,
srcport, dstport);
}
static CURLcode altsvc_add(struct altsvcinfo *asi, const char *line)
{
struct Curl_str srchost;
struct Curl_str dsthost;
struct Curl_str srcalpn;
struct Curl_str dstalpn;
struct Curl_str date;
curl_off_t srcport;
curl_off_t dstport;
curl_off_t persist;
curl_off_t prio;
if(curlx_str_word(&line, &srcalpn, MAX_ALTSVC_ALPNLEN) ||
curlx_str_singlespace(&line) ||
curlx_str_word(&line, &srchost, MAX_ALTSVC_HOSTLEN) ||
curlx_str_singlespace(&line) ||
curlx_str_number(&line, &srcport, 65535) ||
curlx_str_singlespace(&line) ||
curlx_str_word(&line, &dstalpn, MAX_ALTSVC_ALPNLEN) ||
curlx_str_singlespace(&line) ||
curlx_str_word(&line, &dsthost, MAX_ALTSVC_HOSTLEN) ||
curlx_str_singlespace(&line) ||
curlx_str_number(&line, &dstport, 65535) ||
curlx_str_singlespace(&line) ||
curlx_str_quotedword(&line, &date, MAX_ALTSVC_DATELEN) ||
curlx_str_singlespace(&line) ||
curlx_str_number(&line, &persist, 1) ||
curlx_str_singlespace(&line) ||
curlx_str_number(&line, &prio, 0) ||
curlx_str_newline(&line))
;
else {
struct altsvc *as;
char dbuf[MAX_ALTSVC_DATELEN + 1];
time_t expires;
memcpy(dbuf, curlx_str(&date), curlx_strlen(&date));
dbuf[curlx_strlen(&date)] = 0;
expires = Curl_getdate_capped(dbuf);
as = altsvc_create(&srchost, &dsthost, &srcalpn, &dstalpn,
(size_t)srcport, (size_t)dstport);
if(as) {
as->expires = expires;
as->prio = 0;
as->persist = persist ? 1 : 0;
Curl_llist_append(&asi->list, as, &as->node);
}
}
return CURLE_OK;
}
static CURLcode altsvc_load(struct altsvcinfo *asi, const char *file)
{
CURLcode result = CURLE_OK;
FILE *fp;
free(asi->filename);
asi->filename = strdup(file);
if(!asi->filename)
return CURLE_OUT_OF_MEMORY;
fp = fopen(file, FOPEN_READTEXT);
if(fp) {
struct dynbuf buf;
curlx_dyn_init(&buf, MAX_ALTSVC_LINE);
while(Curl_get_line(&buf, fp)) {
const char *lineptr = curlx_dyn_ptr(&buf);
curlx_str_passblanks(&lineptr);
if(curlx_str_single(&lineptr, '#'))
altsvc_add(asi, lineptr);
}
curlx_dyn_free(&buf);
fclose(fp);
}
return result;
}
static CURLcode altsvc_out(struct altsvc *as, FILE *fp)
{
struct tm stamp;
const char *dst6_pre = "";
const char *dst6_post = "";
const char *src6_pre = "";
const char *src6_post = "";
CURLcode result = Curl_gmtime(as->expires, &stamp);
if(result)
return result;
#ifdef USE_IPV6
else {
char ipv6_unused[16];
if(1 == curlx_inet_pton(AF_INET6, as->dst.host, ipv6_unused)) {
dst6_pre = "[";
dst6_post = "]";
}
if(1 == curlx_inet_pton(AF_INET6, as->src.host, ipv6_unused)) {
src6_pre = "[";
src6_post = "]";
}
}
#endif
fprintf(fp,
"%s %s%s%s %u "
"%s %s%s%s %u "
"\"%d%02d%02d "
"%02d:%02d:%02d\" "
"%u %u\n",
Curl_alpnid2str(as->src.alpnid),
src6_pre, as->src.host, src6_post,
as->src.port,
Curl_alpnid2str(as->dst.alpnid),
dst6_pre, as->dst.host, dst6_post,
as->dst.port,
stamp.tm_year + 1900, stamp.tm_mon + 1, stamp.tm_mday,
stamp.tm_hour, stamp.tm_min, stamp.tm_sec,
as->persist, as->prio);
return CURLE_OK;
}
struct altsvcinfo *Curl_altsvc_init(void)
{
struct altsvcinfo *asi = calloc(1, sizeof(struct altsvcinfo));
if(!asi)
return NULL;
Curl_llist_init(&asi->list, NULL);
asi->flags = CURLALTSVC_H1
#ifdef USE_HTTP2
| CURLALTSVC_H2
#endif
#ifdef USE_HTTP3
| CURLALTSVC_H3
#endif
;
return asi;
}
CURLcode Curl_altsvc_load(struct altsvcinfo *asi, const char *file)
{
DEBUGASSERT(asi);
return altsvc_load(asi, file);
}
CURLcode Curl_altsvc_ctrl(struct altsvcinfo *asi, const long ctrl)
{
DEBUGASSERT(asi);
asi->flags = ctrl;
return CURLE_OK;
}
void Curl_altsvc_cleanup(struct altsvcinfo **altsvcp)
{
if(*altsvcp) {
struct Curl_llist_node *e;
struct Curl_llist_node *n;
struct altsvcinfo *altsvc = *altsvcp;
for(e = Curl_llist_head(&altsvc->list); e; e = n) {
struct altsvc *as = Curl_node_elem(e);
n = Curl_node_next(e);
altsvc_free(as);
}
free(altsvc->filename);
free(altsvc);
*altsvcp = NULL;
}
}
CURLcode Curl_altsvc_save(struct Curl_easy *data,
struct altsvcinfo *altsvc, const char *file)
{
CURLcode result = CURLE_OK;
FILE *out;
char *tempstore = NULL;
if(!altsvc)
return CURLE_OK;
if(!file && altsvc->filename)
file = altsvc->filename;
if((altsvc->flags & CURLALTSVC_READONLYFILE) || !file || !file[0])
return CURLE_OK;
result = Curl_fopen(data, file, &out, &tempstore);
if(!result) {
struct Curl_llist_node *e;
struct Curl_llist_node *n;
fputs("# Your alt-svc cache. https://curl.se/docs/alt-svc.html\n"
"# This file was generated by libcurl! Edit at your own risk.\n",
out);
for(e = Curl_llist_head(&altsvc->list); e; e = n) {
struct altsvc *as = Curl_node_elem(e);
n = Curl_node_next(e);
result = altsvc_out(as, out);
if(result)
break;
}
fclose(out);
if(!result && tempstore && Curl_rename(tempstore, file))
result = CURLE_WRITE_ERROR;
if(result && tempstore)
unlink(tempstore);
}
free(tempstore);
return result;
}
static bool hostcompare(const char *host, const char *check)
{
size_t hlen = strlen(host);
size_t clen = strlen(check);
if(hlen && (host[hlen - 1] == '.'))
hlen--;
if(hlen != clen)
return FALSE;
return strncasecompare(host, check, hlen);
}
static void altsvc_flush(struct altsvcinfo *asi, enum alpnid srcalpnid,
const char *srchost, unsigned short srcport)
{
struct Curl_llist_node *e;
struct Curl_llist_node *n;
for(e = Curl_llist_head(&asi->list); e; e = n) {
struct altsvc *as = Curl_node_elem(e);
n = Curl_node_next(e);
if((srcalpnid == as->src.alpnid) &&
(srcport == as->src.port) &&
hostcompare(srchost, as->src.host)) {
Curl_node_remove(e);
altsvc_free(as);
}
}
}
#if defined(DEBUGBUILD) || defined(UNITTESTS)
static time_t altsvc_debugtime(void *unused)
{
const char *timestr = getenv("CURL_TIME");
(void)unused;
if(timestr) {
curl_off_t val;
curlx_str_number(×tr, &val, TIME_T_MAX);
return (time_t)val;
}
return time(NULL);
}
#undef time
#define time(x) altsvc_debugtime(x)
#endif
CURLcode Curl_altsvc_parse(struct Curl_easy *data,
struct altsvcinfo *asi, const char *value,
enum alpnid srcalpnid, const char *srchost,
unsigned short srcport)
{
const char *p = value;
struct altsvc *as;
unsigned short dstport = srcport;
size_t entries = 0;
struct Curl_str alpn;
const char *sp;
time_t maxage = 24 * 3600;
bool persist = FALSE;
#ifdef CURL_DISABLE_VERBOSE_STRINGS
(void)data;
#endif
DEBUGASSERT(asi);
if(!curlx_str_until(&p, &alpn, MAX_ALTSVC_LINE, ';') &&
!curlx_str_single(&p, ';')) {
curlx_str_trimblanks(&alpn);
if(curlx_str_casecompare(&alpn, "clear")) {
altsvc_flush(asi, srcalpnid, srchost, srcport);
return CURLE_OK;
}
}
p = value;
if(curlx_str_until(&p, &alpn, MAX_ALTSVC_LINE, '='))
return CURLE_OK;
curlx_str_trimblanks(&alpn);
sp = strchr(p, ';');
if(sp) {
sp++;
for(;;) {
struct Curl_str name;
struct Curl_str val;
const char *vp;
curl_off_t num;
bool quoted;
if(curlx_str_until(&sp, &name, 20, '=') ||
curlx_str_single(&sp, '=') ||
curlx_str_until(&sp, &val, 80, ';'))
break;
curlx_str_trimblanks(&name);
curlx_str_trimblanks(&val);
vp = curlx_str(&val);
quoted = (*vp == '\"');
if(quoted)
vp++;
if(!curlx_str_number(&vp, &num, TIME_T_MAX)) {
if(curlx_str_casecompare(&name, "ma"))
maxage = (time_t)num;
else if(curlx_str_casecompare(&name, "persist") && (num == 1))
persist = TRUE;
}
if(quoted && curlx_str_single(&sp, '\"'))
break;
if(curlx_str_single(&sp, ';'))
break;
}
}
do {
if(!curlx_str_single(&p, '=')) {
enum alpnid dstalpnid =
Curl_alpn2alpnid(curlx_str(&alpn), curlx_strlen(&alpn));
if(!curlx_str_single(&p, '\"')) {
struct Curl_str dsthost;
curl_off_t port = 0;
if(curlx_str_single(&p, ':')) {
if(curlx_str_single(&p, '[')) {
if(curlx_str_until(&p, &dsthost, MAX_ALTSVC_HOSTLEN, ':')) {
infof(data, "Bad alt-svc hostname, ignoring.");
break;
}
}
else {
if(curlx_str_until(&p, &dsthost, MAX_IPADR_LEN, ']') ||
curlx_str_single(&p, ']')) {
infof(data, "Bad alt-svc IPv6 hostname, ignoring.");
break;
}
}
if(curlx_str_single(&p, ':'))
break;
}
else
curlx_str_assign(&dsthost, srchost, strlen(srchost));
if(curlx_str_number(&p, &port, 0xffff)) {
infof(data, "Unknown alt-svc port number, ignoring.");
break;
}
dstport = (unsigned short)port;
if(curlx_str_single(&p, '\"'))
break;
if(dstalpnid) {
if(!entries++)
altsvc_flush(asi, srcalpnid, srchost, srcport);
as = altsvc_createid(srchost, strlen(srchost),
curlx_str(&dsthost),
curlx_strlen(&dsthost),
srcalpnid, dstalpnid,
srcport, dstport);
if(as) {
time_t secs = time(NULL);
if(maxage > (TIME_T_MAX - secs))
as->expires = TIME_T_MAX;
else
as->expires = maxage + secs;
as->persist = persist;
Curl_llist_append(&asi->list, as, &as->node);
infof(data, "Added alt-svc: %.*s:%d over %s",
(int)curlx_strlen(&dsthost), curlx_str(&dsthost),
dstport, Curl_alpnid2str(dstalpnid));
}
}
}
else
break;
if(curlx_str_single(&p, ','))
break;
if(curlx_str_until(&p, &alpn, MAX_ALTSVC_LINE, '='))
break;
curlx_str_trimblanks(&alpn);
}
else
break;
} while(1);
return CURLE_OK;
}
bool Curl_altsvc_lookup(struct altsvcinfo *asi,
enum alpnid srcalpnid, const char *srchost,
int srcport,
struct altsvc **dstentry,
const int versions)
{
struct Curl_llist_node *e;
struct Curl_llist_node *n;
time_t now = time(NULL);
DEBUGASSERT(asi);
DEBUGASSERT(srchost);
DEBUGASSERT(dstentry);
for(e = Curl_llist_head(&asi->list); e; e = n) {
struct altsvc *as = Curl_node_elem(e);
n = Curl_node_next(e);
if(as->expires < now) {
Curl_node_remove(e);
altsvc_free(as);
continue;
}
if((as->src.alpnid == srcalpnid) &&
hostcompare(srchost, as->src.host) &&
(as->src.port == srcport) &&
(versions & (int)as->dst.alpnid)) {
*dstentry = as;
return TRUE;
}
}
return FALSE;
}
#endif