#include #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 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 (buf[len - 1] == ' ' && len > 0) 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) { char cache_name[LWS_COOKIE_MAX_CACHE_NAME_LEN]; const char *ads, *path; struct lws_cache_ttl_lru *l1; struct client_info_stash *stash; char *cookie_string = NULL, *dl; /* 6 tabs + 20 for max time_t + 2 * TRUE/FALSE + null */ size_t size = 6 + 20 + 10 + 1; time_t expires = 0; 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_ADDRESS]; path = stash->cis[CIS_PATH]; } else { 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_EXPIRES]) { /* * Currently we just take the approach to reject session cookies */ lwsl_warn("%s: reject session cookies\n", __func__); return 0; } if (!c->f[CE_DOMAIN]) { c->f[CE_HOSTONLY] = "T"; c->l[CE_HOSTONLY] = 1; c->f[CE_DOMAIN] = ads; c->l[CE_DOMAIN] = strlen(ads); } if (!c->f[CE_PATH]) { c->f[CE_PATH] = path; c->l[CE_PATH] = strlen(path); dl = memchr(c->f[CE_PATH], '?', c->l[CE_PATH]); if (dl) c->l[CE_PATH] = (size_t)(dl - c->f[CE_PATH]); } if (lws_cookie_compile_cache_name(cache_name, sizeof(cache_name), c)) return -1; 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; } 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); if (lws_cache_write_through(l1, cache_name, (const uint8_t *)cookie_string, strlen(cookie_string), (lws_usec_t)((unsigned long long)expires * (lws_usec_t)LWS_US_PER_SEC), 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); return ret; } static int lws_cookie_attach_cookies(struct lws *wsi, char *buf, char *end) { const char *domain, *path, *dl_domain, *dl_path, *po; char cache_name[LWS_COOKIE_MAX_CACHE_NAME_LEN]; size_t domain_len, path_len, size, 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; if (!wsi) return -1; stash = wsi->stash ? wsi->stash : lws_get_network_wsi(wsi)->stash; if (!stash || !stash->cis[CIS_ADDRESS] || !stash->cis[CIS_PATH]) return -1; 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)); domain = stash->cis[CIS_ADDRESS]; path = stash->cis[CIS_PATH]; if (!domain || !path) return -1; path_len = strlen(path); /* remove query string if exist */ dl_path = 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 = strlen(domain); dl_domain = memchr(domain, '.', domain_len); /* don't match top level domain */ if (!dl_domain) break; if (domain_len + path_len + 6 > sizeof(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]; } } 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 = 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 (c.f[CE_NAME]) goto parse_av; /* * find name value, remove leading trailing * WS and DQ for value */ dl = 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)) 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; }