diff --git a/net/http/http.h b/net/http/http.h index 6c1c6a7c0..e63e44089 100644 --- a/net/http/http.h +++ b/net/http/http.h @@ -198,6 +198,7 @@ char *FormatHttpDateTime(char[hasatleast 30], struct tm *); bool ParseHttpRange(const char *, size_t, long, long *, long *); int64_t ParseHttpDateTime(const char *, size_t); bool IsValidHttpToken(const char *, size_t); +bool IsValidCookieValue(const char *, size_t); bool IsAcceptablePath(const char *, size_t); bool IsAcceptableHost(const char *, size_t); bool IsAcceptablePort(const char *, size_t); diff --git a/net/http/isvalidcookievalue.c b/net/http/isvalidcookievalue.c new file mode 100644 index 000000000..29b7ee7e8 --- /dev/null +++ b/net/http/isvalidcookievalue.c @@ -0,0 +1,43 @@ +/*-*- mode:c;indent-tabs-mode:nil;c-basic-offset:2;tab-width:8;coding:utf-8 -*-│ +│vi: set net ft=c ts=2 sts=2 sw=2 fenc=utf-8 :vi│ +╞══════════════════════════════════════════════════════════════════════════════╡ +│ Copyright 2021 Justine Alexandra Roberts Tunney │ +│ │ +│ Permission to use, copy, modify, and/or distribute this software for │ +│ any purpose with or without fee is hereby granted, provided that the │ +│ above copyright notice and this permission notice appear in all copies. │ +│ │ +│ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL │ +│ WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED │ +│ WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE │ +│ AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL │ +│ DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR │ +│ PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER │ +│ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR │ +│ PERFORMANCE OF THIS SOFTWARE. │ +╚─────────────────────────────────────────────────────────────────────────────*/ +#include "libc/str/str.h" +#include "net/http/http.h" + +static inline bool IsCookieOctet(unsigned char i) { + return i > 0x20 && i < 0x7F && i != ',' && i != ';' && i != '\\' && + i != ' ' && i != '"'; +} + +/** + * Returns true if string is a valid cookie value + * (any ASCII excluding control char, whitespace, + * double quotes, comma, semicolon, and backslash). + * + * @param n if -1 implies strlen + */ +bool IsValidCookieValue(const char *s, size_t n) { + size_t i; + if (n == -1) n = s ? strlen(s) : 0; + for (i = 0; i < n; ++i) { + if (!IsCookieOctet(s[i])) { + return false; + } + } + return true; +} diff --git a/net/http/parsehttpdatetime.c b/net/http/parsehttpdatetime.c index 2e4649330..9a4bcf18b 100644 --- a/net/http/parsehttpdatetime.c +++ b/net/http/parsehttpdatetime.c @@ -18,6 +18,7 @@ ╚─────────────────────────────────────────────────────────────────────────────*/ #include "libc/bits/bits.h" #include "libc/time/time.h" +#include "libc/str/str.h" #include "net/http/http.h" static unsigned ParseMonth(const char *p) { @@ -36,10 +37,12 @@ static unsigned ParseMonth(const char *p) { * Sun, 04 Oct 2020 19:50:10 GMT * * @return seconds from unix epoch + * @param n if -1 implies strlen * @see FormatHttpDateTime() */ int64_t ParseHttpDateTime(const char *p, size_t n) { unsigned weekday, year, month, day, hour, minute, second, yday, leap; + if (n == -1) n = p ? strlen(p) : 0; if (n != 29) return 0; day = (p[5] - '0') * 10 + (p[6] - '0') - 1; month = ParseMonth(p + 8); diff --git a/tool/net/help.txt b/tool/net/help.txt index 86700b362..02f160513 100644 --- a/tool/net/help.txt +++ b/tool/net/help.txt @@ -435,6 +435,30 @@ FUNCTIONS Date, which are abstracted by the transport layer. In such cases, consider calling ServeAsset. + SetCookie(name:str,value:str[,options:table]) + Appends Set-Cookie HTTP header to the response header buffer. + Several Set-Cookie headers can be added to the same response. + __Host- and __Secure- prefixes are supported and may set or + overwrite some of the options (for example, specifying __Host- + prefix sets the Secure option to true, sets the path to "/", and + removes the Domain option). The following options can be used (their + lowercase equivalents are supported as well): + - Expires: sets the maximum lifetime of the cookie as an HTTP-date + timestamp. Can be specified as a Date in the RFC1123 (string) + format or as a UNIX timestamp (number of seconds). + - MaxAge: sets number of seconds until the cookie expires. A zero + or negative number will expire the cookie immediately. If both + Expires and MaxAge are set, MaxAge has precedence. + - Domain: sets the host to which the cookie will be sent. + - Path: sets the path that must be present in the request URL, or + the client will not send the Cookie header. + - Secure: (bool) requests the cookie to be only send to the + server when a request is made with the https: scheme. + - HttpOnly: (bool) forbids JavaScript from accessing the cookie. + - SameSite: (Strict, Lax, or None) controls whether a cookie is + sent with cross-origin requests, providing some protection + against cross-site request forgery attacks. + GetParam(name:str) → value:str Returns first value associated with name. name is handled in a case-sensitive manner. This function checks Request-URL parameters @@ -540,7 +564,7 @@ FUNCTIONS EscapeUser(str) → str Escapes URL username. See kescapeauthority.c. - Fetch(url:str[,body:str|{method=value:str,body=value:str}]) + Fetch(url:str[,body:str|{method=value:str,body=value:str,...}]) → status:int,{header:str=value:str,...},body:str Sends an HTTP/HTTPS request to the specified URL. If only the URL is provided, then a GET request is sent. If both URL and body parameters diff --git a/tool/net/redbean.c b/tool/net/redbean.c index 8d0d7bf98..c3b87342d 100644 --- a/tool/net/redbean.c +++ b/tool/net/redbean.c @@ -4163,6 +4163,111 @@ static int LuaSetHeader(lua_State *L) { return 0; } +static int LuaSetCookie(lua_State *L) { + const char *key, *val; + size_t keylen, vallen; + char *expires, *samesite = ""; + char *buf = 0; + bool ishostpref, issecurepref; + const char *hostpref = "__Host-"; + const char *securepref = "__Secure-"; + + OnlyCallDuringRequest("SetCookie"); + key = luaL_checklstring(L, 1, &keylen); + val = luaL_checklstring(L, 2, &vallen); + + if (!IsValidHttpToken(key, keylen)) { + luaL_argerror(L, 1, "invalid"); + unreachable; + } + if (!IsValidCookieValue(val, vallen)) { + luaL_argerror(L, 2, "invalid"); + unreachable; + } + + ishostpref = keylen > strlen(hostpref) + && SlicesEqual(key, strlen(hostpref), hostpref, strlen(hostpref)); + issecurepref = keylen > strlen(securepref) + && SlicesEqual(key, strlen(securepref), securepref, strlen(securepref)); + if ((ishostpref || issecurepref) && !usessl) { + luaL_argerror(L, 1, "__Host- and __Secure- prefixes require SSL"); + unreachable; + } + + appends(&buf, key); + appends(&buf, "="); + appends(&buf, val); + + if (lua_istable(L, 3)) { + if (lua_getfield(L, 3, "expires") != LUA_TNIL + || lua_getfield(L, 3, "Expires") != LUA_TNIL) { + if (lua_isnumber(L, -1)) { + expires = FormatUnixHttpDateTime( + FreeLater(xmalloc(30)), lua_tonumber(L, -1)); + } else { + expires = lua_tostring(L, -1); + if (!ParseHttpDateTime(expires, -1)) { + luaL_argerror(L, 3, "invalid data format in Expires"); + unreachable; + } + } + appends(&buf, "; Expires="); + appends(&buf, expires); + } + + if ((lua_getfield(L, 3, "maxage") == LUA_TNUMBER + || lua_getfield(L, 3, "MaxAge") == LUA_TNUMBER) + && lua_isinteger(L, -1)) { + appends(&buf, "; Max-Age="); + appends(&buf, lua_tostring(L, -1)); + } + + if (lua_getfield(L, 3, "samesite") == LUA_TSTRING + || lua_getfield(L, 3, "SameSite") == LUA_TSTRING) { + samesite = lua_tostring(L, -1); // also used in the Secure check + appends(&buf, "; SameSite="); + appends(&buf, samesite); + } + + // Secure attribute is required for __Host and __Secure prefixes + // as well as for the SameSite=None + if (ishostpref || issecurepref || !strcmp(samesite, "None") + || ((lua_getfield(L, 3, "secure") == LUA_TBOOLEAN + || lua_getfield(L, 3, "Secure") == LUA_TBOOLEAN) + && lua_toboolean(L, -1))) { + appends(&buf, "; Secure"); + } + + if (!ishostpref + && (lua_getfield(L, 3, "domain") == LUA_TSTRING + || lua_getfield(L, 3, "Domain") == LUA_TSTRING)) { + appends(&buf, "; Domain="); + appends(&buf, lua_tostring(L, -1)); + } + + if (ishostpref + || lua_getfield(L, 3, "path") == LUA_TSTRING + || lua_getfield(L, 3, "Path") == LUA_TSTRING) { + appends(&buf, "; Path="); + appends(&buf, ishostpref ? "/" : lua_tostring(L, -1)); + } + + if ((lua_getfield(L, 3, "httponly") == LUA_TBOOLEAN + || lua_getfield(L, 3, "HttpOnly") == LUA_TBOOLEAN) + && lua_toboolean(L, -1)) { + appends(&buf, "; HttpOnly"); + } + } + DEBUGF("(srvr) Set-Cookie: %s", buf); + + // empty the stack and push header key/value + lua_settop(L, 0); + lua_pushliteral(L, "Set-Cookie"); + lua_pushstring(L, buf); + free(buf); + return LuaSetHeader(L); +} + static int LuaHasParam(lua_State *L) { size_t i, n; const char *s; @@ -5260,6 +5365,7 @@ static const luaL_Reg kLuaFuncs[] = { {"ServeListing", LuaServeListing}, // {"ServeRedirect", LuaServeRedirect}, // {"ServeStatusz", LuaServeStatusz}, // + {"SetCookie", LuaSetCookie}, // {"SetHeader", LuaSetHeader}, // {"SetLogLevel", LuaSetLogLevel}, // {"SetStatus", LuaSetStatus}, //