UNPKG

@edge-csrf/node-http

Version:

Edge-CSRF integration library for node's http module

303 lines (302 loc) 9.45 kB
/*! * cookie * Copyright(c) 2012-2014 Roman Shtylman * Copyright(c) 2015 Douglas Christopher Wilson * MIT Licensed */ var E = _, A = D, L = Object.prototype.toString, O = Object.prototype.hasOwnProperty, T = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/, P = /^("?)[\u0021\u0023-\u002B\u002D-\u003A\u003C-\u005B\u005D-\u007E]*\1$/, j = /^([.]?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)([.][a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$/i, H = /^[\u0020-\u003A\u003D-\u007E]*$/; function _(e, r) { if (typeof e != "string") throw new TypeError("argument str must be a string"); var t = {}, n = e.length; if (n < 2) return t; var i = r && r.decode || R, o = 0, a = 0, s = 0; do { if (a = e.indexOf("=", o), a === -1) break; if (s = e.indexOf(";", o), s === -1) s = n; else if (a > s) { o = e.lastIndexOf(";", a - 1) + 1; continue; } var c = w(e, o, a), f = p(e, a, c), d = e.slice(c, f); if (!O.call(t, d)) { var l = w(e, a + 1, s), h = p(e, s, l); e.charCodeAt(l) === 34 && e.charCodeAt(h - 1) === 34 && (l++, h--); var u = e.slice(l, h); t[d] = N(u, i); } o = s + 1; } while (o < n); return t; } function w(e, r, t) { do { var n = e.charCodeAt(r); if (n !== 32 && n !== 9) return r; } while (++r < t); return t; } function p(e, r, t) { for (; r > t; ) { var n = e.charCodeAt(--r); if (n !== 32 && n !== 9) return r + 1; } return t; } function D(e, r, t) { var n = t && t.encode || encodeURIComponent; if (typeof n != "function") throw new TypeError("option encode is invalid"); if (!T.test(e)) throw new TypeError("argument name is invalid"); var i = n(r); if (!P.test(i)) throw new TypeError("argument val is invalid"); var o = e + "=" + i; if (!t) return o; if (t.maxAge != null) { var a = Math.floor(t.maxAge); if (!isFinite(a)) throw new TypeError("option maxAge is invalid"); o += "; Max-Age=" + a; } if (t.domain) { if (!j.test(t.domain)) throw new TypeError("option domain is invalid"); o += "; Domain=" + t.domain; } if (t.path) { if (!H.test(t.path)) throw new TypeError("option path is invalid"); o += "; Path=" + t.path; } if (t.expires) { var s = t.expires; if (!B(s) || isNaN(s.valueOf())) throw new TypeError("option expires is invalid"); o += "; Expires=" + s.toUTCString(); } if (t.httpOnly && (o += "; HttpOnly"), t.secure && (o += "; Secure"), t.partitioned && (o += "; Partitioned"), t.priority) { var c = typeof t.priority == "string" ? t.priority.toLowerCase() : t.priority; switch (c) { case "low": o += "; Priority=Low"; break; case "medium": o += "; Priority=Medium"; break; case "high": o += "; Priority=High"; break; default: throw new TypeError("option priority is invalid"); } } if (t.sameSite) { var f = typeof t.sameSite == "string" ? t.sameSite.toLowerCase() : t.sameSite; switch (f) { case !0: o += "; SameSite=Strict"; break; case "lax": o += "; SameSite=Lax"; break; case "strict": o += "; SameSite=Strict"; break; case "none": o += "; SameSite=None"; break; default: throw new TypeError("option sameSite is invalid"); } } return o; } function R(e) { return e.indexOf("%") !== -1 ? decodeURIComponent(e) : e; } function B(e) { return L.call(e) === "[object Date]"; } function N(e, r) { try { return r(e); } catch { return e; } } class m { constructor(r) { this.domain = "", this.httpOnly = !0, this.maxAge = void 0, this.name = "_csrfSecret", this.partitioned = void 0, this.path = "/", this.sameSite = "strict", this.secure = !0, Object.assign(this, r); } } class g { constructor(r) { this.fieldName = "csrf_token", this.value = void 0, Object.assign(this, r), this._fieldNameRegex = new RegExp(`^(\\d+_)*${this.fieldName}$`); } } class x { constructor(r) { this.excludePathPrefixes = [], this.ignoreMethods = ["GET", "HEAD", "OPTIONS"], this.saltByteLength = 8, this.secretByteLength = 18, this.cookie = new m(), this.token = new g(); const t = r || {}; if (t.cookie && (t.cookie = new m(t.cookie)), t.token && (t.token = new g(t.token)), Object.assign(this, t), this.saltByteLength < 1 || this.saltByteLength > 255) throw Error("saltBytLength must be greater than 0 and less than 256"); if (this.secretByteLength < 1 || this.secretByteLength > 255) throw Error("secretBytLength must be greater than 0 and less than 256"); } } const C = new g(); function U(e) { const r = new Uint8Array(e); return crypto.getRandomValues(r), r; } function v(e) { let r = ""; for (let t = 0; t < e.byteLength; t += 1) r += String.fromCharCode(e[t]); return btoa(r); } function k(e) { let r; try { r = atob(e); } catch (n) { if (n instanceof Error && n.name === "InvalidCharacterError") return new Uint8Array(); throw n; } const t = new Uint8Array(r.length); for (let n = 0; n < r.length; n += 1) t[n] = r.charCodeAt(n); return t; } function $(e, r = C) { for (const [t, n] of e.entries()) if (r._fieldNameRegex.test(t)) return n; } async function z(e, r = C) { if (r.value !== void 0) return r.value(e); const t = e.headers.get("x-csrf-token"); if (t !== null) return t; const { fieldName: n } = r, i = e.headers.get("content-type") || "text/plain"; if (i === "application/x-www-form-urlencoded" || i.startsWith("multipart/form-data")) { const a = await e.formData(), s = $(a, r); return typeof s == "string" ? s : ""; } if (i === "application/json" || i === "application/ld+json") { const s = (await e.json())[n]; return typeof s == "string" ? s : ""; } const o = await e.text(); if (i.startsWith("text/plain")) try { const a = JSON.parse(o); if (!Array.isArray(a) || a.length === 0) return o; const s = a[0], c = typeof s; return c === "string" ? s : c === "object" ? s[n] || "" : s; } catch { return o; } return o; } function V(e) { const r = new Uint8Array(e); for (let t = 0; t < e; t += 1) r[t] = Math.floor(Math.random() * 255); return r; } async function S(e, r) { const t = new Uint8Array(e.byteLength + r.byteLength); t.set(e), t.set(r, e.byteLength); const n = await crypto.subtle.digest("SHA-1", t); return new Uint8Array(n); } async function M(e, r) { const t = V(r), n = await S(e, t), i = new Uint8Array(2 + r + n.byteLength); return i[0] = 0, i[1] = r, i.set(t, 2), i.set(n, r + 2), i; } async function F(e, r) { if (e.byteLength < 22) return !1; const t = e[1], n = e.subarray(2, 2 + t), i = e.subarray(2 + t), o = await S(r, n); if (i.byteLength !== o.byteLength) return !1; for (let a = 0; a < i.byteLength; a += 1) if (i[a] !== o[a]) return !1; return !0; } class I extends Error { } function W(e) { const r = new x(e || {}); return async (t) => { const { request: n, url: i, getCookie: o, setCookie: a } = t; for (const d of r.excludePathPrefixes) if (i.pathname.startsWith(d)) return; const s = o(r.cookie.name); let c; if (s === void 0 ? (c = U(r.secretByteLength), a({ ...r.cookie, value: v(c) })) : c = k(s), !r.ignoreMethods.includes(n.method)) { const d = await z(n, r.token); if (!await F(k(d), c)) throw new I("csrf validation error"); } const f = await M(c, r.saltByteLength); return v(f); }; } function G(e) { return new Promise((r, t) => { const n = [], i = () => { t(new Error("request aborted")); }, o = (f) => { n.push(f); }, a = () => { e.body = Buffer.concat(n), r(e.body.toString()); }, s = (f) => { t(f); }, c = () => { e.removeListener("data", o), e.removeListener("end", a), e.removeListener("err", s), e.removeListener("aborted", i), e.removeListener("close", c); }; e.on("aborted", i), e.on("data", o), e.on("end", a), e.on("err", s), e.on("close", c); }); } class b extends g { constructor(r) { super(r), this.responseHeader = "X-CSRF-Token", Object.assign(this, r); } } class J extends x { constructor(r) { super(r), this.excludePathPrefixes = [], this.token = new b(); const t = r || {}; t.token && (t.token = new b(t.token)), Object.assign(this, t); } } function X(e) { const r = new J(e), t = W(r); return async (n, i) => { const o = E(n.headers.cookie || ""), { url: a, headers: { host: s } } = n, c = new URL(`http://${s}${a || ""}`), f = new Headers(); Object.entries(n.headers).forEach(([h, u]) => { Array.isArray(u) ? u.forEach((y) => f.append(h, y)) : u !== void 0 && f.append(h, u); }); const d = new Request(c, { method: n.method, headers: f, body: ["GET", "HEAD"].includes(n.method || "") ? void 0 : await G(n) }), l = await t({ request: d, url: c, getCookie: (h) => o[h], setCookie: (h) => { const u = A(h.name, h.value, h), y = i.getHeader("Set-Cookie"); Array.isArray(y) ? i.setHeader("Set-Cookie", [...y, u]) : typeof y == "string" ? i.setHeader("Set-Cookie", [y, u]) : i.setHeader("Set-Cookie", u); } }); l && i.setHeader(r.token.responseHeader, l); }; } export { I as CsrfError, J as NodeHttpConfig, b as NodeHttpTokenOptions, X as createCsrfProtect };