#include #include #include #include #include "web.h" /* Copies at most cap-1 bytes of [s, s+n) into out; -1 when it does not fit. */ static int copy_n(char *out, size_t cap, const char *s, size_t n) { if (!cap || n >= cap) return -1; memcpy(out, s, n); out[n] = '\0'; return 0; } /* Value of header `name` (case-insensitive) in [h, end), trimmed. */ static int header_get(const char *h, const char *end, const char *name, char *out, size_t cap) { size_t nl = strlen(name); while (h < end) { const char *eol = memchr(h, '\n', (size_t)(end - h)); if (!eol) eol = end; const char *line_end = eol > h && eol[-1] == '\r' ? eol - 1 : eol; if ((size_t)(line_end - h) > nl && h[nl] == ':' && strncasecmp(h, name, nl) == 0) { const char *v = h + nl + 1; while (v < line_end && (*v == ' ' || *v == '\t')) v++; const char *ve = line_end; while (ve > v && (ve[-1] == ' ' || ve[-1] == '\t')) ve--; return copy_n(out, cap, v, (size_t)(ve - v)); } h = eol + 1; } if (cap) out[0] = '\0'; return -1; } int web_parse_request(const char *buf, size_t n, struct web_req *r) { memset(r, 0, sizeof *r); if (n > WEB_MAX_REQUEST) return -1; const char *hend = NULL; for (size_t i = 0; i + 3 < n; i++) if (memcmp(buf + i, "\r\n\r\n", 4) == 0) { hend = buf + i + 4; break; } if (!hend) return n >= WEB_MAX_REQUEST ? -1 : 1; /* request line: METHOD SP TARGET SP HTTP/x.y */ const char *sp1 = memchr(buf, ' ', (size_t)(hend - buf)); if (!sp1 || copy_n(r->method, sizeof r->method, buf, (size_t)(sp1 - buf)) != 0) return -1; const char *tgt = sp1 + 1; const char *sp2 = memchr(tgt, ' ', (size_t)(hend - tgt)); if (!sp2 || sp2 == tgt || *tgt != '/') return -1; const char *q = memchr(tgt, '?', (size_t)(sp2 - tgt)); const char *pend = q ? q : sp2; if (copy_n(r->path, sizeof r->path, tgt, (size_t)(pend - tgt)) != 0) return -1; if (q && copy_n(r->query, sizeof r->query, q + 1, (size_t)(sp2 - q - 1)) != 0) return -1; if (strncmp(sp2 + 1, "HTTP/1.", 7) != 0) return -1; const char *hdrs = memchr(sp2, '\n', (size_t)(hend - sp2)); if (!hdrs) return -1; hdrs++; header_get(hdrs, hend, "Cookie", r->cookie, sizeof r->cookie); header_get(hdrs, hend, "X-Forwarded-For", r->forwarded_for, sizeof r->forwarded_for); header_get(hdrs, hend, "X-Forwarded-Uri", r->forwarded_uri, sizeof r->forwarded_uri); char cl[32]; size_t body_len = 0; if (header_get(hdrs, hend, "Content-Length", cl, sizeof cl) == 0) { char *e = NULL; long long v = strtoll(cl, &e, 10); if (!*cl || *e || v < 0 || v > WEB_MAX_REQUEST) return -1; body_len = (size_t)v; } char te[32]; if (header_get(hdrs, hend, "Transfer-Encoding", te, sizeof te) == 0) return -1; /* chunked bodies are not needed here */ size_t have = n - (size_t)(hend - buf); if (have < body_len) return (size_t)(hend - buf) + body_len > WEB_MAX_REQUEST ? -1 : 1; r->body = hend; r->body_len = body_len; return 0; } static int hexval(char c) { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'a' && c <= 'f') return c - 'a' + 10; if (c >= 'A' && c <= 'F') return c - 'A' + 10; return -1; } int web_form_get(const char *form, size_t len, const char *key, char *out, size_t cap) { size_t kl = strlen(key); const char *p = form, *end = form + len; while (p < end) { const char *amp = memchr(p, '&', (size_t)(end - p)); const char *pe = amp ? amp : end; const char *eq = memchr(p, '=', (size_t)(pe - p)); if (eq && (size_t)(eq - p) == kl && memcmp(p, key, kl) == 0) { size_t o = 0; for (const char *v = eq + 1; v < pe; v++) { char c = *v; if (c == '+') { c = ' '; } else if (c == '%') { if (pe - v < 3) return -1; int hi = hexval(v[1]), lo = hexval(v[2]); if (hi < 0 || lo < 0) return -1; c = (char)(hi * 16 + lo); if (!c) return -1; v += 2; } if (o + 1 >= cap) return -1; out[o++] = c; } out[o] = '\0'; return 0; } p = pe + 1; } return -1; } int web_cookie_get(const char *header, const char *name, char *out, size_t cap) { size_t nl = strlen(name); const char *p = header; while (p && *p) { while (*p == ' ' || *p == ';') p++; const char *semi = strchr(p, ';'); const char *pe = semi ? semi : p + strlen(p); if ((size_t)(pe - p) > nl && p[nl] == '=' && strncmp(p, name, nl) == 0) return copy_n(out, cap, p + nl + 1, (size_t)(pe - p - nl - 1)); p = semi ? semi + 1 : NULL; } return -1; } void web_html_escape(struct buf *b, const char *s) { for (; s && *s; s++) { switch (*s) { case '&': buf_append(b, "&", 5); break; case '<': buf_append(b, "<", 4); break; case '>': buf_append(b, ">", 4); break; case '"': buf_append(b, """, 6); break; case '\'': buf_append(b, "'", 5); break; default: buf_append(b, s, 1); } } } int web_token_ok(const char *s) { if (!s || !*s) return 0; for (; *s; s++) if (!((*s >= 'A' && *s <= 'Z') || (*s >= 'a' && *s <= 'z') || (*s >= '0' && *s <= '9') || *s == '_' || *s == '-')) return 0; return 1; } /* --- sessions ------------------------------------------------------ */ static int session_live(const struct web_session *s, int64_t now) { return s->token[0] && now - s->created < WEB_SESSION_TTL; } struct web_session *web_store_add(struct web_store *st, const char *bokf, const char *user, int64_t now) { struct web_session *slot = NULL; for (int i = 0; i < WEB_MAX_SESSIONS && !slot; i++) if (!session_live(&st->s[i], now)) slot = &st->s[i]; if (!slot) { /* full: replace the oldest */ slot = &st->s[0]; for (int i = 1; i < WEB_MAX_SESSIONS; i++) if (st->s[i].created < slot->created) slot = &st->s[i]; } memset(slot, 0, sizeof *slot); char *tok = util_random_id("", 32); char *hdl = util_random_id("", 24); snprintf(slot->token, sizeof slot->token, "%s", tok); snprintf(slot->handle, sizeof slot->handle, "%s", hdl); free(tok); free(hdl); snprintf(slot->bokf, sizeof slot->bokf, "%s", bokf); snprintf(slot->user, sizeof slot->user, "%s", user ? user : ""); slot->created = now; return slot; } static struct web_session *store_find(struct web_store *st, const char *v, int by_handle, int64_t now) { if (!web_token_ok(v)) return NULL; size_t vl = strlen(v); for (int i = 0; i < WEB_MAX_SESSIONS; i++) { struct web_session *s = &st->s[i]; const char *k = by_handle ? s->handle : s->token; if (session_live(s, now) && strlen(k) == vl && util_const_eq(k, v, vl)) return s; } return NULL; } struct web_session *web_store_by_token(struct web_store *st, const char *token, int64_t now) { return store_find(st, token, 0, now); } struct web_session *web_store_by_handle(struct web_store *st, const char *handle, int64_t now) { return store_find(st, handle, 1, now); } void web_store_del(struct web_session *s) { if (s) memset(s, 0, sizeof *s); } /* --- limiter ------------------------------------------------------- */ static struct web_rl_entry *rl_find(struct web_rl *rl, const char *addr) { for (int i = 0; i < WEB_RL_SLOTS; i++) if (rl->e[i].addr[0] && strcmp(rl->e[i].addr, addr) == 0) return &rl->e[i]; return NULL; } int64_t web_rl_blocked(const struct web_rl *rl, const char *addr, int64_t now) { for (int i = 0; i < WEB_RL_SLOTS; i++) { const struct web_rl_entry *e = &rl->e[i]; if (e->addr[0] && strcmp(e->addr, addr) == 0) return e->fails >= WEB_RL_MAX_FAILS && e->window_end > now ? e->window_end - now : 0; } return 0; } void web_rl_fail(struct web_rl *rl, const char *addr, int64_t now) { struct web_rl_entry *e = rl_find(rl, addr); if (!e) { /* a free or expired slot, else the one whose window ends first */ e = &rl->e[0]; for (int i = 0; i < WEB_RL_SLOTS; i++) { struct web_rl_entry *c = &rl->e[i]; if (!c->addr[0] || c->window_end <= now) { e = c; break; } if (c->window_end < e->window_end) e = c; } memset(e, 0, sizeof *e); snprintf(e->addr, sizeof e->addr, "%s", addr); } if (e->fails == 0 || e->window_end <= now) { e->fails = 0; e->window_end = now + WEB_RL_WINDOW; } e->fails++; } void web_rl_ok(struct web_rl *rl, const char *addr) { struct web_rl_entry *e = rl_find(rl, addr); if (e) memset(e, 0, sizeof *e); }