diff options
Diffstat (limited to 'clients/web.c')
| -rw-r--r-- | clients/web.c | 330 |
1 files changed, 330 insertions, 0 deletions
diff --git a/clients/web.c b/clients/web.c new file mode 100644 index 0000000..cc70caf --- /dev/null +++ b/clients/web.c @@ -0,0 +1,330 @@ +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <strings.h> + +#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); +} |
