summaryrefslogtreecommitdiff
path: root/clients/web.c
diff options
context:
space:
mode:
Diffstat (limited to 'clients/web.c')
-rw-r--r--clients/web.c330
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, "&amp;", 5);
+ break;
+ case '<':
+ buf_append(b, "&lt;", 4);
+ break;
+ case '>':
+ buf_append(b, "&gt;", 4);
+ break;
+ case '"':
+ buf_append(b, "&quot;", 6);
+ break;
+ case '\'':
+ buf_append(b, "&#39;", 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);
+}