]> git.ipfire.org Git - thirdparty/curl.git/commitdiff
altsvc: rewrite parser using strparse
authorDaniel Stenberg <daniel@haxx.se>
Mon, 24 Feb 2025 14:29:13 +0000 (15:29 +0100)
committerDaniel Stenberg <daniel@haxx.se>
Wed, 26 Feb 2025 07:36:08 +0000 (08:36 +0100)
Extend test 1654.

Closes #16454

lib/altsvc.c
lib/strparse.c
lib/strparse.h
tests/unit/unit1654.c

index 58416ea994f6151dc6d47170927ee35e0df6e17b..b63cd7beac9af9b8fc9226f94255682d388f087d 100644 (file)
@@ -407,26 +407,6 @@ CURLcode Curl_altsvc_save(struct Curl_easy *data,
   return result;
 }
 
-static CURLcode getalnum(const char **ptr, char *alpnbuf, size_t buflen)
-{
-  size_t len;
-  const char *protop;
-  const char *p = *ptr;
-  while(ISBLANK(*p))
-    p++;
-  protop = p;
-  while(*p && !ISBLANK(*p) && (*p != ';') && (*p != '='))
-    p++;
-  len = p - protop;
-  *ptr = p;
-
-  if(!len || (len >= buflen))
-    return CURLE_BAD_FUNCTION_ARGUMENT;
-  memcpy(alpnbuf, protop, len);
-  alpnbuf[len] = 0;
-  return CURLE_OK;
-}
-
 /* hostcompare() returns true if 'host' matches 'check'. The first host
  * argument may have a trailing dot present that will be ignored.
  */
@@ -497,144 +477,124 @@ CURLcode Curl_altsvc_parse(struct Curl_easy *data,
                            unsigned short srcport)
 {
   const char *p = value;
-  char alpnbuf[MAX_ALTSVC_ALPNLEN] = "";
   struct altsvc *as;
   unsigned short dstport = srcport; /* the same by default */
-  CURLcode result = getalnum(&p, alpnbuf, sizeof(alpnbuf));
   size_t entries = 0;
-  size_t alpnlen = strlen(alpnbuf);
-  size_t srchostlen = strlen(srchost);
+  struct Curl_str alpn;
+  const char *sp;
+  time_t maxage = 24 * 3600; /* default is 24 hours */
+  bool persist = FALSE;
 #ifdef CURL_DISABLE_VERBOSE_STRINGS
   (void)data;
 #endif
-  if(result) {
-    infof(data, "Excessive alt-svc header, ignoring.");
-    return CURLE_OK;
-  }
 
   DEBUGASSERT(asi);
 
-  /* "clear" is a magic keyword */
-  if(strcasecompare(alpnbuf, "clear")) {
-    /* Flush cached alternatives for this source origin */
-    altsvc_flush(asi, srcalpnid, srchost, srcport);
-    return CURLE_OK;
+  /* initial check for "clear" */
+  if(!Curl_str_until(&p, &alpn, MAX_ALTSVC_LINE, ';') &&
+     !Curl_str_single(&p, ';')) {
+    Curl_str_trimblanks(&alpn);
+    /* "clear" is a magic keyword */
+    if(Curl_str_casecompare(&alpn, "clear")) {
+      /* Flush cached alternatives for this source origin */
+      altsvc_flush(asi, srcalpnid, srchost, srcport);
+      return CURLE_OK;
+    }
+  }
+
+  p = value;
+
+  if(Curl_str_until(&p, &alpn, MAX_ALTSVC_LINE, '='))
+    return CURLE_OK; /* strange line */
+
+  Curl_str_trimblanks(&alpn);
+
+  /* Handle the optional 'ma' and 'persist' flags once first, as they need to
+     be known for each alternative service. Unknown flags are skipped. */
+  sp = strchr(p, ';');
+  if(sp) {
+    sp++; /* pass the semicolon */
+    for(;;) {
+      struct Curl_str name;
+      struct Curl_str val;
+      const char *vp;
+      curl_off_t num;
+      bool quoted;
+      /* allow some extra whitespaces around name and value */
+      if(Curl_str_until(&sp, &name, 20, '=') ||
+         Curl_str_single(&sp, '=') ||
+         Curl_str_until(&sp, &val, 80, ';'))
+        break;
+      Curl_str_trimblanks(&name);
+      Curl_str_trimblanks(&val);
+      /* the value might be quoted */
+      vp = Curl_str(&val);
+      quoted = (*vp == '\"');
+      if(quoted)
+        vp++;
+      if(!Curl_str_number(&vp, &num, TIME_T_MAX)) {
+        if(Curl_str_casecompare(&name, "ma"))
+          maxage = (time_t)num;
+        else if(Curl_str_casecompare(&name, "persist") && (num == 1))
+          persist = TRUE;
+      }
+      if(quoted && Curl_str_single(&sp, '\"'))
+        break;
+      if(Curl_str_single(&sp, ';'))
+        break;
+    }
   }
 
   do {
-    if(*p == '=') {
-      /* [protocol]="[host][:port]" */
-      enum alpnid dstalpnid = Curl_alpn2alpnid(alpnbuf, alpnlen);
-      p++;
-      if(*p == '\"') {
-        const char *dsthost = "";
-        size_t dstlen = 0; /* destination hostname length */
-        const char *value_ptr;
-        char option[32];
-        curl_off_t num;
-        bool quoted = FALSE;
-        time_t maxage = 24 * 3600; /* default is 24 hours */
-        bool persist = FALSE;
-        bool valid = TRUE;
-        p++;
-        if(*p != ':') {
+    if(!Curl_str_single(&p, '=')) {
+      /* [protocol]="[host][:port], [protocol]="[host][:port]" */
+      enum alpnid dstalpnid =
+        Curl_alpn2alpnid(Curl_str(&alpn), Curl_strlen(&alpn));
+      if(!Curl_str_single(&p, '\"')) {
+        struct Curl_str dsthost;
+        curl_off_t port = 0;
+        if(Curl_str_single(&p, ':')) {
           /* hostname starts here */
-          const char *hostp = p;
-          if(*p == '[') {
-            /* pass all valid IPv6 letters - does not handle zone id */
-            dstlen = strspn(++p, "0123456789abcdefABCDEF:.");
-            if(p[dstlen] != ']')
-              /* invalid host syntax, bail out */
+          if(Curl_str_single(&p, '[')) {
+            if(Curl_str_until(&p, &dsthost, MAX_ALTSVC_HOSTLEN, ':')) {
+              infof(data, "Bad alt-svc hostname, ignoring.");
               break;
-            /* we store the IPv6 numerical address *with* brackets */
-            dstlen += 2;
-            p = &p[dstlen-1];
-          }
-          else {
-            while(*p && (ISALNUM(*p) || (*p == '.') || (*p == '-')))
-              p++;
-            dstlen = p - hostp;
-          }
-          if(!dstlen || (dstlen >= MAX_ALTSVC_HOSTLEN)) {
-            infof(data, "Excessive alt-svc hostname, ignoring.");
-            valid = FALSE;
+            }
           }
           else {
-            dsthost = hostp;
+            /* IPv6 host name */
+            if(Curl_str_until(&p, &dsthost, MAX_IPADR_LEN, ']') ||
+               Curl_str_single(&p, ']')) {
+              infof(data, "Bad alt-svc IPv6 hostname, ignoring.");
+              break;
+            }
           }
+          if(Curl_str_single(&p, ':'))
+            break;
         }
-        else {
+        else
           /* no destination name, use source host */
-          dsthost = srchost;
-          dstlen = strlen(srchost);
-        }
-        if(*p == ':') {
-          curl_off_t port = 0;
-          p++;
-          if(Curl_str_number(&p, &port, 0xffff) || (*p != '\"')) {
-            infof(data, "Unknown alt-svc port number, ignoring.");
-            valid = FALSE;
-          }
-          else
-            dstport = (unsigned short)port;
-        }
-        if(*p++ != '\"')
+          Curl_str_assign(&dsthost, srchost, strlen(srchost));
+
+        if(Curl_str_number(&p, &port, 0xffff)) {
+          infof(data, "Unknown alt-svc port number, ignoring.");
           break;
-        /* Handle the optional 'ma' and 'persist' flags. Unknown flags
-           are skipped. */
-        for(;;) {
-          while(ISBLANK(*p))
-            p++;
-          if(*p != ';')
-            break;
-          p++; /* pass the semicolon */
-          if(!*p || ISNEWLINE(*p))
-            break;
-          result = getalnum(&p, option, sizeof(option));
-          if(result) {
-            /* skip option if name is too long */
-            option[0] = '\0';
-          }
-          while(ISBLANK(*p))
-            p++;
-          if(*p != '=')
-            return CURLE_OK;
-          p++;
-          while(ISBLANK(*p))
-            p++;
-          if(!*p)
-            return CURLE_OK;
-          if(*p == '\"') {
-            /* quoted value */
-            p++;
-            quoted = TRUE;
-          }
-          value_ptr = p;
-          if(quoted) {
-            while(*p && *p != '\"')
-              p++;
-            if(!*p++)
-              return CURLE_OK;
-          }
-          else {
-            while(*p && !ISBLANK(*p) && *p!= ';' && *p != ',')
-              p++;
-          }
-          if(!Curl_str_number(&value_ptr, &num, TIME_T_MAX)) {
-            if(strcasecompare("ma", option))
-              maxage = (time_t)num;
-            else if(strcasecompare("persist", option) && (num == 1))
-              persist = TRUE;
-          }
         }
-        if(dstalpnid && valid) {
+
+        dstport = (unsigned short)port;
+
+        if(Curl_str_single(&p, '\"'))
+          break;
+
+        if(dstalpnid) {
           if(!entries++)
             /* Flush cached alternatives for this source origin, if any - when
                this is the first entry of the line. */
             altsvc_flush(asi, srcalpnid, srchost, srcport);
 
-          as = altsvc_createid(srchost, srchostlen,
-                               dsthost, dstlen,
+          as = altsvc_createid(srchost, strlen(srchost),
+                               Curl_str(&dsthost),
+                               Curl_strlen(&dsthost),
                                srcalpnid, dstalpnid,
                                srcport, dstport);
           if(as) {
@@ -647,26 +607,28 @@ CURLcode Curl_altsvc_parse(struct Curl_easy *data,
               as->expires = maxage + secs;
             as->persist = persist;
             Curl_llist_append(&asi->list, as, &as->node);
-            infof(data, "Added alt-svc: %s:%d over %s", dsthost, dstport,
-                  Curl_alpnid2str(dstalpnid));
+            infof(data, "Added alt-svc: %.*s:%d over %s",
+                  (int)Curl_strlen(&dsthost), Curl_str(&dsthost),
+                  dstport, Curl_alpnid2str(dstalpnid));
           }
         }
       }
       else
         break;
+
       /* after the double quote there can be a comma if there is another
          string or a semicolon if no more */
-      if(*p == ',') {
-        /* comma means another alternative is presented */
-        p++;
-        result = getalnum(&p, alpnbuf, sizeof(alpnbuf));
-        if(result)
-          break;
-      }
+      if(Curl_str_single(&p, ','))
+        break;
+
+      /* comma means another alternative is present */
+      if(Curl_str_until(&p, &alpn, MAX_ALTSVC_LINE, '='))
+        break;
+      Curl_str_trimblanks(&alpn);
     }
     else
       break;
-  } while(*p && (*p != ';') && (*p != '\n') && (*p != '\r'));
+  } while(1);
 
   return CURLE_OK;
 }
index 8a4e08e2bd20e7217e2e13488b7eecd60bdf2237..a78c38490641783e6395085f1403051877c94d30 100644 (file)
@@ -31,6 +31,12 @@ void Curl_str_init(struct Curl_str *out)
   out->len = 0;
 }
 
+void Curl_str_assign(struct Curl_str *out, const char *str, size_t len)
+{
+  out->str = str;
+  out->len = len;
+}
+
 /* Get a word until the first DELIM or end of string. At least one byte long.
    return non-zero on error */
 int Curl_str_until(const char **linep, struct Curl_str *out,
index 5dcd537bd9a2a5a727511d9a8ad6d4b95e858309..e9109c55c2117e17e0de2a60b706ca0681c962c9 100644 (file)
@@ -43,6 +43,7 @@ struct Curl_str {
 };
 
 void Curl_str_init(struct Curl_str *out);
+void Curl_str_assign(struct Curl_str *out, const char *str, size_t len);
 
 #define Curl_str(x) ((x)->str)
 #define Curl_strlen(x) ((x)->len)
index 2f5d1bcc86ebb3ffb4e80aab400dad6fb16d0308..aea3f8a5e5720f453857e375486068441121d61c 100644 (file)
@@ -71,7 +71,8 @@ UNITTEST_START
   fail_unless(Curl_llist_count(&asi->list) == 6, "wrong number of entries");
 
   result = Curl_altsvc_parse(curl, asi,
-                             "h2=\"example.com:8080\", h3=\"yesyes.com\"\r\n",
+                             "h2=\"example.com:8080\", "
+                             "h3=\"yesyes.com:8080\"\r\n",
                              ALPN_h1, "3.example.org", 8080);
   fail_if(result, "Curl_altsvc_parse(3) failed!");
   /* that one should make two entries */
@@ -92,7 +93,8 @@ UNITTEST_START
 
   result =
     Curl_altsvc_parse(curl, asi,
-                      "h2=\":443\", h3=\":443\"; ma = 120; persist = 1\r\n",
+                      "h2=\":443\", h3=\":443\"; "
+                      "persist = \"1\"; ma = 120;\r\n",
                       ALPN_h1, "curl.se", 80);
   fail_if(result, "Curl_altsvc_parse(5) failed!");
   fail_unless(Curl_llist_count(&asi->list) == 12, "wrong number of entries");
@@ -103,6 +105,26 @@ UNITTEST_START
   fail_if(result, "Curl_altsvc_parse(6) failed!");
   fail_unless(Curl_llist_count(&asi->list) == 10, "wrong number of entries");
 
+  /* only a non-existing alpn */
+  result = Curl_altsvc_parse(curl, asi,
+                             "h6=\"example.net:443\"; ma=\"180\";\r\n",
+                             ALPN_h2, "5.example.net", 80);
+
+  /* missing quote in alpn host */
+  result = Curl_altsvc_parse(curl, asi,
+                             "h2=\"example.net:443,; ma=\"180\";\r\n",
+                             ALPN_h2, "6.example.net", 80);
+
+  /* missing port in host name */
+  result = Curl_altsvc_parse(curl, asi,
+                             "h2=\"example.net\"; ma=\"180\";\r\n",
+                             ALPN_h2, "7.example.net", 80);
+
+  /* illegal port in host name */
+  result = Curl_altsvc_parse(curl, asi,
+                             "h2=\"example.net:70000\"; ma=\"180\";\r\n",
+                             ALPN_h2, "8.example.net", 80);
+
   Curl_altsvc_save(curl, asi, outname);
 
   curl_easy_cleanup(curl);