From d062bed87d69d2cafe52cc5a18cbfa498a919bd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Tue, 31 Jan 2023 23:16:12 +0100 Subject: [PATCH] add Set-Cookie helper func --- demo/server_demo.c | 16 +++- spritehttpd/include/httpd-types.h | 40 ++++++++ spritehttpd/include/httpd.h | 22 ++++- spritehttpd/src/httpd.c | 150 ++++++++++++++++++++++++++++-- 4 files changed, 217 insertions(+), 11 deletions(-) diff --git a/demo/server_demo.c b/demo/server_demo.c index 9168dc5..98b2127 100644 --- a/demo/server_demo.c +++ b/demo/server_demo.c @@ -81,17 +81,29 @@ httpd_cgi_state templateReplacer(HttpdConnData *conn, const char *token) // httpdGetHeader(conn, "Cookie", ) //} + + httpd_cgi_state cgiStartSession(HttpdConnData *conn) { httpdQueueHeader(conn, "X-Foo", "FOO"); httpdQueueHeader(conn, "X-Bar", "Bar"); + httpdSetCookie(conn, &(SetCookie) { + .name = "testCookie", + .value = "lala-lele", +// .domain = "localhost", +// .expires = "BABABABAx", +// .httponly = true, + .maxAge = -1, +// .path = "foo/bar/baz", +// .sameSite = COOKIE_SAMESITE_LAX, +// .secure = true, + }); + return HTTPD_CGI_NOTFOUND; } - - /** * Application routes */ diff --git a/spritehttpd/include/httpd-types.h b/spritehttpd/include/httpd-types.h index e900085..a6ed65f 100644 --- a/spritehttpd/include/httpd-types.h +++ b/spritehttpd/include/httpd-types.h @@ -10,6 +10,10 @@ #include "httpd-config.h" + +// TODO error enum + + // opaque conn type struct struct HttpdConnType; typedef struct HttpdConnType HttpdConnType; @@ -67,6 +71,34 @@ struct httpd_init_options { uint16_t port; }; +/** + * Parameters for Set-Cookie + * + * Leave unused fields as NULL or false for defaults. + * + * If "maxAge" is zero, it's not sent, setting a session cookie. + * This is to allow leaving the field unset by default. + * + * To expire a cookie immediately, use maxAge = -1 + */ +typedef struct SetCookie { + const char *name; + const char *value; + const char *expires; + const int32_t maxAge; + const char *domain; + const char *path; + const char *sameSite; + const bool secure; + const bool httponly; +} SetCookie; + +// Supported values for the "sameSite" attribute of SetCookie. +// Any string can be sent, these are the standard forms suspported by browsers. +#define COOKIE_SAMESITE_LAX "Lax" +#define COOKIE_SAMESITE_STRICT "Strict" +#define COOKIE_SAMESITE_NONE "None" + /* Private types - exposed to allow static alloc */ @@ -100,6 +132,14 @@ struct HttpdPriv { uint8_t flags; }; +//Flags (1 byte) +#define HFL_HTTP11 (1<<0) +#define HFL_CHUNKED (1<<1) +#define HFL_SENDINGBODY (1<<2) +#define HFL_DISCONAFTERSENT (1<<3) +#define HFL_NOCONNECTIONSTR (1<<4) +#define HFL_NOCORS (1<<5) + /// Callback type that releases the user data attached to the connection. /// The format is compatible with regular "free()" or typedef void (* httpdUserDataCleanupCb)(void *userData); diff --git a/spritehttpd/include/httpd.h b/spritehttpd/include/httpd.h index c3c2ea6..9155d92 100644 --- a/spritehttpd/include/httpd.h +++ b/spritehttpd/include/httpd.h @@ -10,7 +10,6 @@ #include "httpd-routes.h" #include "httpd-config.h" - /** * Get the server version string * @@ -113,6 +112,27 @@ int httpdGetHeader(HttpdConnData *conn, const char *header, char *buff, size_t b */ void httpdQueueHeader(HttpdConnData *conn, const char *header, const char *value); +/** + * Queue a header to be sent with the response. This is a variant of `httpdQueueHeader()` + * which uses the raw internal struct to avoid re-alloc. + * + * The variable length field of the struct must contain `HeaderName: HeaderValue + CR LF` + * + * @param conn + * @param queEntry + * @return 1 = OK + */ +bool httpdQueueHeaderRaw(HttpdConnData *conn, HttpdQueuedHeader *queEntry); + +/** + * Set cookie. This queues the cookie header. + * + * @param conn + * @param parm + * @return + */ +bool httpdSetCookie(HttpdConnData *conn, const SetCookie *parm); + /** * Send binary data * diff --git a/spritehttpd/src/httpd.c b/spritehttpd/src/httpd.c index e47f9ee..06cdb30 100644 --- a/spritehttpd/src/httpd.c +++ b/spritehttpd/src/httpd.c @@ -29,14 +29,6 @@ _Static_assert(HTTPD_MAX_CONNECTIONS < 256, "HTTPD_MAX_CONNECTIONS must be at mo static const HttpdBuiltInUrl *s_builtInUrls; static const char *s_serverName = HTTPD_SERVERNAME; -//Flags (1 byte) -#define HFL_HTTP11 (1<<0) -#define HFL_CHUNKED (1<<1) -#define HFL_SENDINGBODY (1<<2) -#define HFL_DISCONAFTERSENT (1<<3) -#define HFL_NOCONNECTIONSTR (1<<4) -#define HFL_NOCORS (1<<5) - //Connection pool @@ -198,6 +190,23 @@ void httpdQueueHeader(HttpdConnData *conn, const char *header, const char *value strcat(queEntry->headerLine, value); strcat(queEntry->headerLine, "\r\n"); + if (!httpdQueueHeaderRaw(conn, queEntry)) { + httpdFree(queEntry); + } +} + +bool httpdQueueHeaderRaw(HttpdConnData *conn, HttpdQueuedHeader *queEntry) +{ + if (!conn || !queEntry) { + return false; + } + if (conn->priv.flags & HFL_SENDINGBODY) { + http_error("Headers already sent."); + return false; + } + + queEntry->next = NULL; + if (!conn->priv.headersToSend) { conn->priv.headersToSend = queEntry; } else { @@ -208,8 +217,133 @@ void httpdQueueHeader(HttpdConnData *conn, const char *header, const char *value } ph->next = queEntry; } + return true; } + +bool httpdSetCookie(HttpdConnData *conn, const SetCookie *parm) +{ + if (!conn || !parm) { + return false; + } + if (!parm->name || !parm->value) { + return false; + } + if (conn->priv.flags & HFL_SENDINGBODY) { + http_error("Headers already sent."); + return false; + } + + /* + Set-Cookie: = + Set-Cookie: =; Domain= + Set-Cookie: =; Expires= + Set-Cookie: =; HttpOnly + Set-Cookie: =; Max-Age= + Set-Cookie: =; Partitioned + Set-Cookie: =; Path= + Set-Cookie: =; Secure + + Set-Cookie: =; SameSite=Strict + Set-Cookie: =; SameSite=Lax + Set-Cookie: =; SameSite=None; Secure + + // Multiple attributes are also possible, for example: + Set-Cookie: =; Domain=; Secure; HttpOnly + */ + +#define COOKIE_PART_SET_COOKIE "Set-Cookie: " +#define COOKIE_PART_EXPIRES "; Expires=" +#define COOKIE_PART_MAX_AGE "; Max-Age=" +#define COOKIE_PART_DOMAIN "; Domain=" +#define COOKIE_PART_PATH "; Path=" +#define COOKIE_PART_SAME_SITE "; SameSite=" +#define COOKIE_PART_SECURE "; Secure" +#define COOKIE_PART_HTTP_ONLY "; HttpOnly" + + // "Set-Cookie: " + size_t buflen = strlen(COOKIE_PART_SET_COOKIE) + + strlen(parm->name) + 1 + strlen(parm->value) + + 5; // cr lf + nul + some spare space + if (parm->expires) { + buflen += strlen(COOKIE_PART_EXPIRES) + strlen(parm->expires); + } + + if (parm->maxAge != 0) { + // 10 chars are needed for 32bit integer + buflen += strlen(COOKIE_PART_MAX_AGE) + 10; + } + + if (parm->domain) { + buflen += strlen(COOKIE_PART_DOMAIN) + strlen(parm->domain); + } + + if (parm->path) { + buflen += strlen(COOKIE_PART_PATH) + strlen(parm->path); + } + + if (parm->sameSite) { + // Strict, Lax, None + buflen += strlen(COOKIE_PART_SAME_SITE) + strlen(parm->sameSite); + } + + if (parm->secure) { + buflen += strlen(COOKIE_PART_SECURE); + } + + if (parm->httponly) { + buflen += strlen(COOKIE_PART_HTTP_ONLY); + } + + HttpdQueuedHeader *queEntry = httpdMalloc(buflen); + if (!queEntry) { + http_error("httpdSetCookie - no mem"); + return false; + } + + queEntry->next = NULL; + queEntry->headerLine[0] = 0; + strcat(queEntry->headerLine, COOKIE_PART_SET_COOKIE); + strcat(queEntry->headerLine, parm->name); + strcat(queEntry->headerLine, "="); + strcat(queEntry->headerLine, parm->value); + if (parm->expires) { + strcat(queEntry->headerLine, COOKIE_PART_EXPIRES); + strcat(queEntry->headerLine, parm->expires); + } + if (parm->maxAge != 0) { + strcat(queEntry->headerLine, COOKIE_PART_MAX_AGE); + sprintf(queEntry->headerLine + strlen(queEntry->headerLine), "%d", parm->maxAge); + } + if (parm->domain) { + strcat(queEntry->headerLine, COOKIE_PART_DOMAIN); + strcat(queEntry->headerLine, parm->domain); + } + if (parm->path) { + strcat(queEntry->headerLine, COOKIE_PART_PATH); + strcat(queEntry->headerLine, parm->path); + } + if (parm->sameSite) { + strcat(queEntry->headerLine, COOKIE_PART_SAME_SITE); + strcat(queEntry->headerLine, parm->sameSite); + } + if (parm->secure) { + strcat(queEntry->headerLine, COOKIE_PART_SECURE); + } + if (parm->httponly) { + strcat(queEntry->headerLine, COOKIE_PART_HTTP_ONLY); + } + strcat(queEntry->headerLine, "\r\n"); + + if (!httpdQueueHeaderRaw(conn, queEntry)) { + // should not be possible + httpdFree(queEntry); + } + + return true; +} + + void httdSetTransferMode(HttpdConnData *conn, httpd_transfer_opt mode) { if (mode == HTTPD_TRANSFER_CLOSE) {