
#include <libwebsockets.h>
#include "private-lib-core.h"

//#define LWS_COOKIE_DEBUG

#if defined(LWS_COOKIE_DEBUG)
	#define lwsl_cookie lwsl_notice
#else
	#define lwsl_cookie lwsl_debug
#endif

#define LWS_COOKIE_MAX_CACHE_NAME_LEN	128

#define lws_tolower(_c) (((_c) >= 'A' && (_c) <= 'Z') ? \
					    (char)((_c) + 'a' - 'A') : \
					    (char)(_c))

#define LWS_COOKIE_NSC_FORMAT		  "%.*s\t"\
					  "%s\t"\
					  "%.*s\t"\
					  "%s\t"\
					  "%llu\t"\
					  "%.*s\t"\
					  "%.*s"

static const char *const mon = "janfebmaraprnayjunjulaugsepoctnovdec";

enum lws_cookie_nsc_f {
	LWSC_NSC_DOMAIN,
	LWSC_NSC_HOSTONLY,
	LWSC_NSC_PATH,
	LWSC_NSC_SECURE,
	LWSC_NSC_EXPIRES,
	LWSC_NSC_NAME,
	LWSC_NSC_VALUE,

	LWSC_NSC_COUNT,
};

enum lws_cookie_elements {
	CE_DOMAIN,
	CE_PATH,
	CE_EXPIRES,
	CE_MAXAGE,
	CE_NAME,
	CE_VALUE,

	CE_HOSTONLY, /* these are bool, NULL = 0, non-NULL = 1 */
	CE_SECURE,

	CE_COUNT
};

struct lws_cookie {
	const char	*f[CE_COUNT];
	size_t		l[CE_COUNT];

	unsigned int httponly:1;
};

static size_t
lws_cookie_domain_len(const char *domain)
{
	const char *p;

	if (!domain)
		return 0;

	if (domain[0] == '[') {
		p = (char *)strchr(domain, ']');
		if (p && p[1] == ':')
			return lws_ptr_diff_size_t((p + 1), domain);
	} else {
		p = (char *)strchr(domain, ':');
		if (p)
			return lws_ptr_diff_size_t(p, domain);
	}

	return strlen(domain);
}

static int
lws_cookie_parse_date(const char *d, size_t len, time_t *t)
{
	struct tm date;
	int offset = 0, i;

	memset(&date, 0, sizeof(date));

	while (len) {
		if (isalnum((int)*d)) {
			offset++;
			goto next;
		}
		switch (offset) {
		case 2:
			if (*d == ':' && len >= 6) {
				date.tm_hour = atoi(d - 2);
				if (date.tm_hour < 0 || date.tm_hour > 23)
					return -1;
				date.tm_min = atoi(d + 1);
				if (date.tm_min < 0 || date.tm_min > 60)
					return -1;
				date.tm_sec = atoi(d + 4);
				if (date.tm_sec < 0 || date.tm_sec > 61)
					/* leap second */
					return -1;

				d += 6;
				len -= 6;
				offset = 0;
				continue;
			}

			if (!date.tm_mday) {
				date.tm_mday = atoi(d - 2);
				if (date.tm_mday < 1 || date.tm_mday > 31)
					return -1;
				goto next2;
			}

			if (!date.tm_year) {
				date.tm_year = atoi(d - 2);
				if (date.tm_year < 0 || date.tm_year > 99)
					return -1;
				if (date.tm_year < 70)
					date.tm_year += 100;
			}
			goto next2;

		case 3:
			for (i = 0; i < 36; i += 3) {
				if (lws_tolower(*(d - 3)) == mon[i] &&
				    lws_tolower(*(d - 2)) == mon[i + 1] &&
				    lws_tolower(*(d - 1)) == mon[i + 2]) {
					date.tm_mon = i / 3;
					break;
				}
			}
			goto next2;

		case 4:
			if (!date.tm_year) {
				date.tm_year = atoi(d - 4);
				if (date.tm_year < 1601)
					return -1;
				date.tm_year -= 1900;
			}
			goto next2;

		default:
			goto next2;
		}

next2:
		offset = 0;
next:
		d++;
		len--;
	}

	*t = mktime(&date);

	if (*t < 0)
		return -1;

	return 0;
}

static void
lws_cookie_rm_sws(const char **buf_p, size_t *len_p)
{
	const char *buf;
	size_t len;

	if (!buf_p || !*buf_p || !len_p || !*len_p) {
		lwsl_err("%s: false parameter\n", __func__);
		return;
	}

	buf = *buf_p;
	len = *len_p;

	while (buf[0] == ' ' && len > 0) {
		buf++;
		len--;
	}

	while (len && buf[len - 1] == ' ')
		len--;

	*buf_p = buf;
	*len_p = len;
}

static int
is_iprefix(const char *h, size_t hl, const char *n, size_t nl)
{
	if (!h || !n || nl > hl)
		return 0;

	while (nl) {
		nl--;
		if (lws_tolower(h[nl]) != lws_tolower(n[nl]))
			return 0;
	}
	return 1;
}

static int
lws_cookie_compile_cache_name(char *buf, size_t buf_len, struct lws_cookie *c)
{
	if (!buf || !c->f[CE_DOMAIN] || !c->f[CE_PATH] || !c->f[CE_NAME] ||
	    c->l[CE_DOMAIN] + c->l[CE_PATH] + c->l[CE_NAME] + 6 > buf_len)
		return -1;

	memcpy(buf, c->f[CE_DOMAIN], c->l[CE_DOMAIN]);
	buf += c->l[CE_DOMAIN];
	*buf++ = '|';

	memcpy(buf, c->f[CE_PATH], c->l[CE_PATH]);
	buf += c->l[CE_PATH];
	*buf++ = '|';

	memcpy(buf, c->f[CE_NAME], c->l[CE_NAME]);
	buf += c->l[CE_NAME];
	*buf = '\0';

	return 0;
}

static int
lws_cookie_parse_nsc(struct lws_cookie *c, const char *b, size_t l)
{
	enum lws_cookie_nsc_f state = LWSC_NSC_DOMAIN;
	size_t n = 0;

	if (!c || !b || l < 13)
		return -1;

	memset(c, 0, sizeof(*c));
	lwsl_cookie("%s: parsing (%.*s) \n", __func__, (int)l, b);

	while (l) {
		l--;
		if (b[n] != '\t' && l) {
			n++;
			continue;
		}
		switch (state) {
		case LWSC_NSC_DOMAIN:
			c->f[CE_DOMAIN] = b;
			c->l[CE_DOMAIN] = n;
			break;
		case LWSC_NSC_PATH:
			c->f[CE_PATH] = b;
			c->l[CE_PATH] = n;
			break;
		case LWSC_NSC_EXPIRES:
			c->f[CE_EXPIRES] = b;
			c->l[CE_EXPIRES] = n;
			break;
		case LWSC_NSC_NAME:
			c->f[CE_NAME] = b;
			c->l[CE_NAME] = n;
			break;

		case LWSC_NSC_HOSTONLY:
			if (b[0] == 'T') {
				c->f[CE_HOSTONLY] = b;
				c->l[CE_HOSTONLY] = 1;
			}
			break;
		case LWSC_NSC_SECURE:
			if (b[0] == 'T') {
				c->f[CE_SECURE] = b;
				c->l[CE_SECURE] = 1;
			}
			break;

		case LWSC_NSC_VALUE:
			c->f[CE_VALUE] = b;
			c->l[CE_VALUE] = n + 1;

			for (n = 0; n < LWS_ARRAY_SIZE(c->f); n++)
				lwsl_cookie("%s: %d: %.*s\n", __func__,
						(int)n, (int)c->l[n], c->f[n]);

			return 0;
		default:
			return -1;
		}

		b += n + 1;
		n = 0;
		state++;
	}

	return -1;
}

static int
lws_cookie_write_nsc(struct lws *wsi, struct lws_cookie *c)
{
	const char *ads, *path;
	struct lws_cache_ttl_lru *l1;
	struct client_info_stash *stash;
	char *cookie_string = NULL, *cache_name = NULL;
	const char *dl;
	 /* 6 tabs + 20 for max time_t + 2 * TRUE/FALSE + null */
	size_t size = 6 + 20 + 10 + 1, cnl;
	time_t expires = 0;
	lws_usec_t expiry_us = 0;
	time_t now_s = time(NULL);

	int ret = 0;

	if (!wsi || !c)
		return -1;

	l1 = wsi->a.context->l1;
	if (!l1 || !wsi->a.context->nsc)
		return -1;

	stash = wsi->stash ? wsi->stash : lws_get_network_wsi(wsi)->stash;
	if (stash) {
		ads = stash->cis[CIS_HOST] ? stash->cis[CIS_HOST] : stash->cis[CIS_ADDRESS];
		path = stash->cis[CIS_PATH];
	} else {
		ads = lws_hdr_simple_ptr(wsi, _WSI_TOKEN_CLIENT_HOST);
		if (!ads)
			ads = lws_hdr_simple_ptr(wsi, _WSI_TOKEN_CLIENT_PEER_ADDRESS);
		path = lws_hdr_simple_ptr(wsi, _WSI_TOKEN_CLIENT_URI);
	}
	if (!ads || !path)
		return -1;

	if (!c->f[CE_NAME] || !c->f[CE_VALUE]) {
		lwsl_err("%s: malformed c\n", __func__);

		return -1;
	}



	if (!c->f[CE_DOMAIN]) {
		c->f[CE_HOSTONLY] = "T";
		c->l[CE_HOSTONLY] = 1;
		c->f[CE_DOMAIN] = ads;
		c->l[CE_DOMAIN] = lws_cookie_domain_len(ads);
	}

	if (!c->f[CE_PATH]) {
		c->f[CE_PATH] = path;
		c->l[CE_PATH] = strlen(path);
		dl = (char *)memchr(c->f[CE_PATH], '?', c->l[CE_PATH]);
		if (dl)
			c->l[CE_PATH] = (size_t)(dl - c->f[CE_PATH]);
	}

	cnl = c->l[CE_DOMAIN] + c->l[CE_PATH] + c->l[CE_NAME] + 6;
	cache_name = lws_malloc(cnl, __func__);
	if (!cache_name)
		return -1;

	if (lws_cookie_compile_cache_name(cache_name, cnl, c)) {
		ret = -1;
		goto exit;
	}

	if (c->f[CE_EXPIRES] &&
	    lws_cookie_parse_date(c->f[CE_EXPIRES], c->l[CE_EXPIRES], &expires)) {
		lwsl_err("%s: can't parse date %.*s\n", __func__,
			 (int)c->l[CE_EXPIRES], c->f[CE_EXPIRES]);
		return -1;
	}

	if (c->f[CE_EXPIRES])
		lwsl_cookie("%s: name=%s expires='%.*s'@ t=%lld now=%lld\n", __func__, cache_name,
			    (int)c->l[CE_EXPIRES], c->f[CE_EXPIRES],
			    (long long)expires, (long long)now_s);
	else
		lwsl_cookie("%s: name=%s expires='<unset>'@ t=%lld now=%lld\n", __func__, cache_name,
			    (long long)expires, (long long)now_s);

	/*
	 * RFC 6265 treats a past-dated Expires as a delete signal for the
	 * cookie. Ensure we handle expires=0 (PARSED at actual UNIX epoch, t=0)
	 * as a delete signal as well.
	 */
	if (c->f[CE_EXPIRES] && (expires <= now_s)) {
		lwsl_cookie("%s: dropping already-expired cookie\n", __func__);
		(void)lws_cache_item_remove(l1, cache_name);
		ret = 0;
		goto exit;
	}

	size += c->l[CE_NAME] + c->l[CE_VALUE] + c->l[CE_DOMAIN] + c->l[CE_PATH];
	cookie_string = (char *)lws_malloc(size, __func__);
	if (!cookie_string) {
		lwsl_err("%s: OOM\n",__func__);

		return -1;	
	}

	lws_snprintf(cookie_string, size, LWS_COOKIE_NSC_FORMAT,
			(int)c->l[CE_DOMAIN], c->f[CE_DOMAIN],
			c->f[CE_HOSTONLY] ? "TRUE" : "FALSE",
			(int)c->l[CE_PATH], c->f[CE_PATH],
			c->f[CE_SECURE] ? "TRUE" : "FALSE",
			(unsigned long long)expires,
			(int)c->l[CE_NAME], c->f[CE_NAME],
			(int)c->l[CE_VALUE], c->f[CE_VALUE]);

	lwsl_cookie("%s: name %s\n", __func__, cache_name);
	lwsl_cookie("%s: c %s\n", __func__, cookie_string);

	/*
	 * Convert expiry in UNIX time to lws_usec_t (monotonic clock).
	 * expires == 0 is a session cookie (no expiry).
	 */
	if (expires) {
		expiry_us = lws_now_usecs() +
				(lws_usec_t)(expires - now_s) *
				(lws_usec_t)LWS_US_PER_SEC;
	}

	if (lws_cache_write_through(l1, cache_name,
					(const uint8_t *)cookie_string,
					strlen(cookie_string),
					expiry_us, NULL)) {
		ret = -1;
		goto exit;
	}

#if defined(LWS_COOKIE_DEBUG)
	char *po;
	if (lws_cache_item_get(l1, cache_name, (const void **)&po, &size) ||
	    size != strlen(cookie_string) || memcmp(po, cookie_string, size)) {
		lwsl_err("%s: L1 '%s' missing\n", __func__, cache_name);
	}

	if (lws_cache_item_get(wsi->a.context->nsc, cache_name,
			       (const void **)&po, &size) ||
			       size != strlen(cookie_string) ||
			       memcmp(po, cookie_string, size)) {
		lwsl_err("%s: NSC '%s' missing, size %llu, po %s\n", __func__,
			 cache_name, (unsigned long long)size, po);
	}
#endif

exit:
	lws_free(cookie_string);
	lws_free(cache_name);

	return ret;
}

static int
lws_cookie_attach_cookies(struct lws *wsi, char *buf, char *end)
{
	const char *domain, *path, *dl_domain, *dl_path, *po;
	size_t domain_len, path_len, size, cnl, ret = 0;
	struct lws_cache_ttl_lru *l1;
	struct client_info_stash *stash;
	lws_cache_results_t cr;
	struct lws_cookie c;
	int hostdomain = 1;
	char *p, *p1, *cache_name;

	if (!wsi)
		return -1;

	/*
	 * stash is only guaranteed during the connect phase; lws frees it
	 * early when LWS_WITH_SOCKS5 is on. Fall back to HTTP headers
	 * (same pattern as lws_cookie_write_nsc()).
	 */
	{
		const char *fb_domain, *fb_path;

		stash = wsi->stash ? wsi->stash :
			lws_get_network_wsi(wsi)->stash;
		if (stash && stash->cis[CIS_HOST] &&
			    stash->cis[CIS_PATH]) {
			fb_domain = stash->cis[CIS_HOST] ?
				stash->cis[CIS_HOST] :
				stash->cis[CIS_ADDRESS];
			fb_path   = stash->cis[CIS_PATH];
		} else {
			fb_domain = lws_hdr_simple_ptr(wsi,
					_WSI_TOKEN_CLIENT_HOST);
			if (!fb_domain)
				fb_domain = lws_hdr_simple_ptr(wsi,
					_WSI_TOKEN_CLIENT_PEER_ADDRESS);
			fb_path = lws_hdr_simple_ptr(wsi,
					_WSI_TOKEN_CLIENT_URI);
		}
		if (!fb_domain || !fb_path)
			return -1;

		domain = fb_domain;
		path = fb_path;
	}

	l1 = wsi->a.context->l1;
	if (!l1 || !wsi->a.context->nsc){
		lwsl_err("%s:no cookiejar\n", __func__);
		return -1;
	}

	memset(&c, 0, sizeof(c));



	path_len = strlen(path);

	/* remove query string if exist */
	dl_path = (char *)memchr(path, '?', path_len);
	if (dl_path)
		path_len = lws_ptr_diff_size_t(dl_path,  path);

	/* remove last slash if exist */
	if (path_len != 1 && path[path_len - 1] == '/')
		path_len--;

	if (!path_len)
		return -1;

	lwsl_cookie("%s: path %.*s len %d\n", __func__, (int)path_len, path, (int)path_len);

	/* when dest buf is not provided, we only return size of cookie string */
	if (!buf || !end)
		p = NULL;
	else
		p = buf;

	/* iterate through domain and path levels to find matching cookies */
	dl_domain = domain;
	while (dl_domain) {
		domain_len = lws_cookie_domain_len(domain);
		dl_domain = (char *)memchr(domain, '.', domain_len);
		/* don't match top level domain */
		if (!dl_domain)
			break;

		cnl = domain_len + path_len + 6;
		cache_name = lws_malloc(cnl, __func__);
		if (!cache_name)
			return -1;

		/* compile key string "[domain]|[path]|*"" */
		p1 = cache_name;
		memcpy(p1, domain, domain_len);
		p1 += domain_len;
		*p1 = '|';
		p1++;
		memcpy(p1, path, path_len);
		p1 += path_len;
		*p1 = '|';
		p1++;
		*p1 = '*';
		p1++;
		*p1 = '\0';

		lwsl_cookie("%s: looking for %s\n", __func__, cache_name);

		if (!lws_cache_lookup(l1, cache_name,
				      (const void **)&cr.ptr, &cr.size)) {

			while (!lws_cache_results_walk(&cr)) {
				lwsl_cookie(" %s (%d)\n", (const char *)cr.tag,
						(int)cr.payload_len);

				if (lws_cache_item_get(l1, (const char *)cr.tag,
						   (const void **)&po, &size) ||
					lws_cookie_parse_nsc(&c, po, size)) {
					lwsl_err("%s: failed to get c '%s'\n",
							__func__, cr.tag);
					break;
				}

				if (c.f[CE_HOSTONLY] && !hostdomain){
					lwsl_cookie("%s: not sending this\n",
							__func__);
					continue;
				}

				if (p) {
					if (ret) {
						*p = ';';
						p++;
						*p = ' ';
						p++;
					}

					memcpy(p, c.f[CE_NAME], c.l[CE_NAME]);
					p += c.l[CE_NAME];
					*p = '=';
					p++;
					memcpy(p, c.f[CE_VALUE], c.l[CE_VALUE]);
					p += c.l[CE_VALUE];
				}

				if (ret)
					ret += 2;
				ret += c.l[CE_NAME] + 1 + c.l[CE_VALUE];

			}
		}

		lws_free(cache_name);

		domain = dl_domain + 1;
		hostdomain = 0;
	}

	lwsl_notice("%s: c len (%d)\n", __func__, (int)ret);

	return (int)ret;
}

static struct {
	const char		*const name;
	uint8_t			len;
} cft[] = {
	{ "domain=",  7 },
	{ "path=",    5 },
	{ "expires=", 8 },
	{ "max-age=", 8 },
	{ "httponly", 8 },
	{ "secure",   6 }
};

int
lws_parse_set_cookie(struct lws *wsi)
{
	char *tk_head, *tk_end, *buf_head, *buf_end, *cookiep, *dl;
	struct lws_cache_ttl_lru *l1;
	struct lws_cookie c;
	size_t fl;
	int f, n;

	if (!wsi)
		return -1;

	l1 = wsi->a.context->l1;
	if (!l1)
		return -1;

	f = wsi->http.ah->frag_index[WSI_TOKEN_HTTP_SET_COOKIE];

	while (f) {
		cookiep = wsi->http.ah->data + wsi->http.ah->frags[f].offset;
		fl = wsi->http.ah->frags[f].len;
		f = wsi->http.ah->frags[f].nfrag;

		if (!cookiep || !fl)
			continue;

#if defined(LWS_COOKIE_DEBUG)
		lwsl_notice("%s:parsing: %.*s\n", __func__, (int)fl, cookiep);
#endif

		buf_head = cookiep;
		buf_end = cookiep + fl - 1;
		memset(&c, 0, sizeof(struct lws_cookie));

		do {
			tk_head = buf_head;
			tk_end = (char *)memchr(buf_head, ';',
					(size_t)(buf_end - buf_head + 1));
			if (!tk_end) {
				tk_end = buf_end;
				buf_head = buf_end;
			} else {
				buf_head = tk_end + 1;
				tk_end--;
			}

			if (tk_end < tk_head) {
				if (!c.f[CE_NAME])
					return -1;
				continue;
			}

			if (c.f[CE_NAME])
				goto parse_av;

			/*
			 * find name value, remove leading trailing
			 * WS and DQ for value
			 */

			dl = (char *)memchr(tk_head, '=',
				    lws_ptr_diff_size_t(tk_end, tk_head) + 1);
			if (!dl || dl == tk_head)
				return -1;

			c.f[CE_NAME] = tk_head;
			c.l[CE_NAME] = lws_ptr_diff_size_t(dl, tk_head);
			lws_cookie_rm_sws(&c.f[CE_NAME], &c.l[CE_NAME]);

			if (!c.l[CE_NAME])
				return -1;

			lwsl_cookie("%s: c name l %d v:%.*s\n", __func__,
					(int)c.l[CE_NAME],
					(int)c.l[CE_NAME], c.f[CE_NAME]);
			c.f[CE_VALUE] = dl + 1;
			c.l[CE_VALUE] = lws_ptr_diff_size_t(tk_end,
						   c.f[CE_VALUE]) + 1;

			lws_cookie_rm_sws(&c.f[CE_VALUE], &c.l[CE_VALUE]);
			if (c.l[CE_VALUE] >= 2 && c.f[CE_VALUE][0] == '\"') {
				c.f[CE_VALUE]++;
				c.l[CE_VALUE] -= 2;
			}
			lwsl_cookie("%s: c value l %d v:%.*s\n", __func__,
				    (int)c.l[CE_VALUE], (int)c.l[CE_VALUE],
				    c.f[CE_VALUE]);
			continue;

parse_av:
			while (*tk_head == ' ') {
				if (tk_head == tk_end)
					return -1;

				tk_head++;
			}

			for (n = 0; n < (int)LWS_ARRAY_SIZE(cft); n++) {
				if (lws_tolower(*tk_head) != cft[n].name[0])
					continue;

				if (!is_iprefix(tk_head,
						lws_ptr_diff_size_t(tk_end,
								   tk_head) + 1,
						cft[n].name, cft[n].len))
					continue;

				if (n == 4 || n == 5) {
					c.f[n] = "T";
					c.l[n] = 1;
					break;
				}

				c.f[n] = tk_head + cft[n].len;
				c.l[n] = lws_ptr_diff_size_t(tk_end, c.f[n]) + 1;
				lws_cookie_rm_sws(&c.f[n], &c.l[n]);

				if (n == CE_DOMAIN && c.l[0] &&
				    c.f[n][0] == '.'){
					c.f[n]++;
					c.l[n]--;
				}

				lwsl_cookie("%s: %s l %d v:%.*s\n", __func__,
					    cft[n].name, (int)c.l[n],
					    (int)c.l[n], c.f[n]);
				break;
			}

		} while (tk_end != buf_end);

		if (lws_cookie_write_nsc(wsi, &c))
			lwsl_err("%s:failed to write nsc\n", __func__);
	}

	return 0;
}

int
lws_cookie_send_cookies(struct lws *wsi, char **pp, char *end)
{
	char *p;
	int size;

	if (!wsi || !pp || !(*pp) || !end)
		return -1;

	size = lws_cookie_attach_cookies(wsi, NULL, NULL);

	if (!size)
		return 0;
	if (size < 0) {
		lwsl_err("%s:failed to get cookie string size\n", __func__);
		return -1;
	}

	lwsl_notice("%s: size %d\n", __func__, size);

#if defined(LWS_COOKIE_DEBUG)
		char *p_dbg = *pp;
#endif

	if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_COOKIE, NULL, size,
								(unsigned char **)pp, (unsigned char *)end))
		return -1;

#if defined(LWS_COOKIE_DEBUG)
		lwsl_notice("%s: dummy copy (%.*s) \n", __func__, (int)(*pp - p_dbg), p_dbg);
#endif


#ifdef LWS_WITH_HTTP2
	if (lws_wsi_is_h2(wsi)
#ifdef LWS_ROLE_H3
	    || lws_wsi_is_h3(wsi)
#endif
	)
		p = *pp - size;
	else
#endif
		p = *pp - size - 2;

	if (lws_cookie_attach_cookies(wsi, p, p + size) <= 0) {
		lwsl_err("%s:failed to attach cookies\n", __func__);
		return -1;
	}

#if defined(LWS_COOKIE_DEBUG)
		lwsl_notice("%s: real copy (%.*s) total len %d\n", __func__, (int)(*pp - p_dbg), p_dbg, (int)(*pp - p_dbg));
		lwsl_hexdump_notice(p_dbg, (size_t)(*pp - p_dbg));
#endif

	return 0;
}
