Getting and Setting CGI Cookies in C

Source Code

Setting and getting cookies with kcgi is easy. It uses the same logic as setting and getting form fields. Cookies consist of name-value pairs that we can grok from a table. I'll lead this tutorial as if we were reading a source file from top to bottom, so let's start with headers. We'll obviously need kcgi and stdint.h, which is necessary for some types found in the header file.

#include <sys/types.h> /* size_t, ssize_t */
#include <stdarg.h> /* va_list */
#include <stddef.h> /* NULL */
#include <stdint.h> /* int64_t */
#include <time.h> /* time(3) */
#include <kcgi.h>

Next, let's define identifiers for our cookies. These will later be mapped to the cookie names and the validators for their values.

enum cookie {
  COOKIE_STRING,
  COOKIE_INTEGER,
  COOKIE__MAX
};

The enumeration will allow us to bound an array to COOKIE__MAX and refer to individual buckets in the array by the enumeration value. I'll assume that COOKIE_STRING is assigned 0 and COOKIE_INTEGER, 1.

Next, connect the indices with validation functions and names. The validation function is run by khttp_parse(3); the name is the cookie key name. Built-in validation functions, which we'll use, are described in kvalid_string(3). In this example, kvalid_stringne will validate a non-empty (nil-terminated) C string, while kvalid_int will validate a signed 64-bit integer.

static const struct kvalid cookies[COOKIE__MAX] = {
  { kvalid_stringne, "string" }, /* COOKIE_STRING */
  { kvalid_int, "integer" }, /* COOKIE_INTEGER */
};

Before doing any parsing, I sanitise the HTTP context. I'll let any page request pass, but will make sure our MIME type and HTTP method are sane.

static enum khttp sanitise(const struct kreq *r) {
  if (r->mime != KMIME_TEXT_HTML)
    return KHTTP_404;
  else if (r->method != KMETHOD_GET)
    return KHTTP_405;
  else
    return KHTTP_200;
}

Now the scaffolding is done. What about the cookies? To begin with, you should glance at the RFC 6265, HTTP State Management Mechanism, to gain an understanding of how cookies work. You may also want to read about the HttpOnly and Secure flags also available. In our application, let's just attempt to read the cookie; and if it doesn't exist, write the cookie along with the page. If it does exist, we'll indicate that in our page. We'll focus only on the COOKIE_STRING cookie, and will set the cookie to be visible to the path root and expire in an hour. We'll use kutil_epoch2str(3) to format the date. Headers are output using khttp_head(3), with the document body started with khttp_body(3).

static void process(struct kreq *r) {
  char buf[32];			 
  khttp_head(r, kresps[KRESP_STATUS],
    "%s", khttps[KHTTP_200]);
  khttp_head(r, kresps[KRESP_CONTENT_TYPE],
    "%s", kmimetypes[r->mime]);
  if (r->cookiemap[COOKIE_STRING] == NULL)
    khttp_head(r, kresps[KRESP_SET_COOKIE],
      "%s=%s; Path=/; expires=%s", 
      cookies[COOKIE_STRING].name, 
      "Hello, world!",
      kutil_epoch2str(time(NULL) + 60 * 60, 
        buf, sizeof(buf)));
  khttp_body(r);
  khttp_puts(r, 
    "<!DOCTYPE html>"
    "<title>Foo</title>");
  if (r->cookiemap[COOKIE_STRING] != NULL)
    khttp_puts(r, "Cookie found!");
  else
    khttp_puts(r, "Cookie set.");
}

Most of the above code is just to handle the HTML5 bits, and we deliberately used the smallest possible page. (Yes, this is a valid page—validate it yourself to find out!) For any significant page, you'd want to use kcgihtml(3).

Putting all of these together: parse the HTTP context, validate it, process it, then free the resources. The HTTP context is closed with khttp_free(3). Note that the identifiers for the cookies, enum cookie, are also used to identify any form input. So if you have both form input and cookies (which is common), they can either share identifiers or use unique ones. In other words, COOKIE__MAX defines the size of both fieldmap and cookiemap, so the validator for COOKIE_STRING is also valid for form inputs of the same name.

int main(void) {
  struct kreq r;
  enum khttp er;
  if (khttp_parse(&r, cookies, COOKIE__MAX, NULL, 0, 0) != KCGI_OK)
    return 0;
  if ((er = sanitise(&r)) != KHTTP_200) {
    khttp_head(&r, kresps[KRESP_STATUS],
      "%s", khttps[er]);
    khttp_head(&r, kresps[KRESP_CONTENT_TYPE],
      "%s", kmimetypes[KMIME_TEXT_PLAIN]);
    khttp_body(&r);
    if (r.mime == KMIME_TEXT_HTML)
      khttp_puts(&r, "Could not service request.");
  } else
    process(&r);
  khttp_free(&r);
  return 0;
}

For compilation, linking, and installation, see Getting Started with CGI in C.