/***************************************************************************
 *                                  _   _ ____  _
 *  Project                     ___| | | |  _ \| |
 *                             / __| | | | |_) | |
 *                            | (__| |_| |  _ <| |___
 *                             \___|\___/|_| \_\_____|
 *
 * Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
 *
 * This software is licensed as described in the file COPYING, which
 * you should have received as part of this distribution. The terms
 * are also available at https://curl.se/docs/copyright.html.
 *
 * You may opt to use, copy, modify, merge, publish, distribute and/or sell
 * copies of the Software, and permit persons to whom the Software is
 * furnished to do so, under the terms of the COPYING file.
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
 * KIND, either express or implied.
 *
 * SPDX-License-Identifier: curl
 *
 ***************************************************************************/
#include "unitcheck.h"

#if !defined(CURL_DISABLE_HTTP) && !defined(CURL_DISABLE_HSTS)
#include "urldata.h"
#include "hsts.h"

static void showsts(struct stsentry *e, const char *chost)
{
  if(!e)
    curl_mprintf("'%s' is not HSTS\n", chost);
  else {
    curl_mprintf("%s [%s]: %" CURL_FORMAT_CURL_OFF_T "%s\n",
                 chost, e->host, e->expires,
                 e->includeSubDomains ? " includeSubDomains" : "");
  }
}

static CURLcode test_unit1660(const char *arg)
{
  UNITTEST_BEGIN_SIMPLE

  struct testit {
    const char *host;
    const char *chost;     /* if non-NULL, use to lookup with */
    const char *hdr;       /* if NULL, do the lookup */
    const CURLcode result; /* parse result */
  };

  static const struct testit headers[] = {
    /* two entries read from disk cache, verify first */
    { "-", "readfrom.example", NULL, CURLE_OK },
    { "-", "old.example", NULL, CURLE_OK },
    /* delete the remaining one read from disk */
    { "readfrom.example", NULL, "max-age=\"0\"", CURLE_OK },

    { "example.com", NULL, "max-age=\"31536000\"\r\n", CURLE_OK },
    { "example.com", NULL, "max-age=\"21536000\"\r\n", CURLE_OK },
    { "example.com", NULL, "max-age=\"21536000\"; \r\n", CURLE_OK },
    { "example.com", NULL, "max-age=\"21536000\"; includeSubDomains\r\n",
      CURLE_OK },
    { "example.org", NULL, "max-age=\"31536000\"\r\n", CURLE_OK },
    { "this.example", NULL, "max=\"31536\";", CURLE_BAD_FUNCTION_ARGUMENT },
    { "this.example", NULL, "max-age=\"31536", CURLE_BAD_FUNCTION_ARGUMENT },
    { "this.example", NULL, "max-age=31536\"", CURLE_OK },
    /* max-age=0 removes the entry */
    { "this.example", NULL, "max-age=0", CURLE_OK },
    { "another.example", NULL, "includeSubDomains; ",
      CURLE_BAD_FUNCTION_ARGUMENT },

    /* Two max-age is illegal */
    { "example.com", NULL,
      "max-age=\"21536000\"; includeSubDomains; max-age=\"3\";",
      CURLE_BAD_FUNCTION_ARGUMENT },
    /* Two includeSubDomains is illegal */
    { "2.example.com", NULL,
      "max-age=\"21536000\"; includeSubDomains; includeSubDomains;",
      CURLE_BAD_FUNCTION_ARGUMENT },
    /* use an unknown directive "include" that should be ignored */
    { "3.example.com", NULL,
      "max-age=\"21536000\"; include; includeSubDomains;", CURLE_OK },
    /* remove the "3.example.com" one, should still match the example.com */
    { "3.example.com", NULL, "max-age=\"0\"; includeSubDomains;", CURLE_OK },
    { "-", "foo.example.com", NULL, CURLE_OK },
    { "-", "foo.xample.com", NULL, CURLE_OK },

    /* should not match */
    { "example.net", "forexample.net", "max-age=\"31536000\"\r\n", CURLE_OK },

    /* should not match either, since forexample.net is not in the example.net
       domain */
    { "example.net", "forexample.net",
      "max-age=\"31536000\"; includeSubDomains\r\n", CURLE_OK },
    /* remove example.net again */
    { "example.net", NULL, "max-age=\"0\"; includeSubDomains\r\n", CURLE_OK },

    /* make this live for 7 seconds */
    { "expire.example", NULL, "max-age=\"7\"\r\n", CURLE_OK },
    { NULL, NULL, NULL, CURLE_OK }
  };

  CURLcode result;
  struct stsentry *e;
  struct hsts *h = Curl_hsts_init();
  int i;
  const char *chost;
  CURL *easy;
  char savename[256];

  abort_unless(h, "Curl_hsts_init()");

  curl_global_init(CURL_GLOBAL_ALL);
  easy = curl_easy_init();
  if(!easy) {
    Curl_hsts_cleanup(&h);
    curl_global_cleanup();
    abort_unless(easy, "curl_easy_init()");
  }

  Curl_hsts_loadfile(easy, h, arg);

  for(i = 0; headers[i].host; i++) {
    if(headers[i].hdr) {
      result = Curl_hsts_parse(h, headers[i].host, headers[i].hdr);

      if(result != headers[i].result) {
        curl_mfprintf(stderr, "Curl_hsts_parse(%s) failed: %d\n",
                      headers[i].hdr, (int)result);
        unitfail++;
        continue;
      }
      else if(result) {
        curl_mprintf("Input %d: error %d\n", i, (int)result);
        continue;
      }
    }

    chost = headers[i].chost ? headers[i].chost : headers[i].host;
    e = hsts_check(h, chost, strlen(chost), TRUE);
    showsts(e, chost);
  }

  result = Curl_hsts_parse(h, "prefix.example",
                           "max-age=60; includeSubDomainsExtra");
  fail_if(result, "failed to parse unknown HSTS directive");
  e = hsts_check(h, "child.prefix.example",
                 strlen("child.prefix.example"), TRUE);
  fail_if(e, "unknown directive enabled subdomain matching");
  result = Curl_hsts_parse(h, "prefix.example", "max-age=0");
  fail_if(result, "failed to remove HSTS test entry");

  result = Curl_hsts_parse(h, "unknown.example",
                           "max-age-extra=1; max-age=60");
  fail_if(result, "failed to ignore unknown HSTS directive");
  e = hsts_check(h, "unknown.example", strlen("unknown.example"), FALSE);
  fail_if(!e, "recognized directive after unknown directive was ignored");
  result = Curl_hsts_parse(h, "unknown.example", "max-age=0");
  fail_if(result, "failed to remove HSTS test entry");

  curl_mprintf("Number of entries: %zu\n", Curl_llist_count(&h->list));

  /* verify that it is exists for 7 seconds */
  chost = "expire.example";
  for(i = 100; i < 110; i++) {
    e = hsts_check(h, chost, strlen(chost), TRUE);
    showsts(e, chost);
    deltatime++; /* another second passed */
  }

  curl_msnprintf(savename, sizeof(savename), "%s.save", arg);
  (void)Curl_hsts_save(easy, h, savename);
  Curl_hsts_cleanup(&h);
  curl_easy_cleanup(easy);
  curl_global_cleanup();

  UNITTEST_END(curl_global_cleanup())
}
#else
static CURLcode test_unit1660(const char *arg)
{
  UNITTEST_BEGIN_SIMPLE
  puts("nothing to do when HTTP or HSTS are disabled");
  UNITTEST_END_SIMPLE
}
#endif
