Add SetCookie method to redbean Lua (#265)

This commit is contained in:
Paul Kulchenko 2021-09-04 02:12:12 -07:00 committed by GitHub
parent 969174e155
commit 31dd714081
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 178 additions and 1 deletions

View file

@ -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);

View file

@ -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;
}

View file

@ -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);

View file

@ -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

View file

@ -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}, //