Server Side Cookies - The CGI Program

Summary

This is a discussion of the actual C source code for writing a CGI for server-side cookies.

The other issues: installation, configuration, and wirting client side code to drive this is covered back in the main Server Side Cookies.

The Program - Server Side

Server side - The CGI must implement GetCookie(name) and SetCookie(name, value). To do this, it must parse a get request and a set request coming in from the web page, as transformed by the web server, perform the action, and update its local data.

CGI Made Really Easy is a good tutorial on writing CGIs in general.

The Model

We'll limit to 26 single character, alphabetic, names, holding string values. So our “model”, our master data structure is:

enum {
  kKeyArrayCount = 26
};

// our dictionary: index (== key - 'a') to string values.
char *gValues[kKeyArrayCount];

Parsing GET

We follow current convention and define a RESTy protocol, we'll use HTTP GET for our get operation and HTTP POST for our post operation.

So, to request the value for name, we’ll just use name as the query parameter in our URL. Example: http://localhost/cgi-bin/servercookie.cgi?name

The CGI spec says that the web esever will parse out the paert of the URL after the question mark and pass it to the cgi in an environment variable, QUERY_STRING. The source code to dispatch for HTTP GET looks like:

  if (NULL == requestMethod ||
      0 == strcmp(requestMethod, "GET") ||
      0 == strcmp(requestMethod, "HEAD")) {
    Echo( GetServerCookie( getenv("QUERY_STRING") ) );
  }
Looking up a string to return is:
// given a lower case letter, key, returns the value. Might return NULL.
char *GetServerCookie(char *qs){
  if (NULL != qs && 1 == strlen(qs)) {
    char key = qs[0];
    if ('a' <= key && key <= 'z') {
      return gValues[key - 'a'];
    }
  }
  return "Not Recognized";
}

Parsing POST

To set the string value for name, we use an HTTP POST (since HTTP GETs aren’t supposed to modify the website.)

Here the URL is just enough to get to our CGI, and all the interesting stuff is in the body of the POST.

To be truly RESTy, just as HTTP GET http://localhost/cgi-bin/servercookie.cgi?name fetches the value of a variable, HTTP PUT http://localhost/cgi-bin/servercookie.cgi?name should set the avlue of the variable, with the actual value sent in the POST body. Well, I botched it. I just reused my file parser. So to set a variable, you do a HTTP PUT http://localhost/cgi-bin/servercookie.cgi?, with name = "value" as the contents of the POST body.

To read the POST body, we get the length, passed as a string represnetaion of a number in the CONTENT_LENGTH environment variable, and read that many characters from standard input (stdin). (Once again, this is the documented convention for how webservers talk to CGIs.)

Much of fetching the input from the user is error checking. The top level dispatch is:

if (0 == strcmp(requestMethod, "POST") ) {
    SetServerCookie();
}
which is:
void SetServerCookie(void){
  char *cgiinput;
  char *s;
  char sbuf[512];
  char *contentLengthS = getenv("CONTENT_LENGTH");
  if (NULL == contentLengthS || 0 == strlen(contentLengthS) ) {
    ExitWithError("No Content-Length was sent with the POST request.");
  }
  int contentLength = atoi(contentLengthS);
  if (contentLength <= 0 ) {
    snprintf(sbuf, sizeof sbuf, 
      "Content-Length too small (%d) was sent with the POST request.", 
      contentLength);
    ExitWithError(sbuf);
  } 
  if (1023 <= contentLength) {
    snprintf(sbuf, sizeof sbuf, 
      "Content-Length too big (%d) was sent with the POST request.", 
      contentLength);
    ExitWithError(sbuf);
  }
  cgiinput = malloc(contentLength+1);
  if (NULL == cgiinput) {
    ExitWithError("Couldn't allocated buffer for POST request.");
  }
  if (1 != fread(cgiinput, contentLength, 1, stdin)) {
    ExitWithError("Couldn't read CGI input from STDIN.\n") ;
  }
  cgiinput[contentLength]= '\0';
  
We’ve finally got the name = "value" in cgiinput
  s = cgiinput;

  if(0 == ReadCookiesReaderFunc((ReaderFunc) StringRead, &s)) {
    if(0 == WriteCookiesToServerFile()) {
      JSONEcho("OK");
    } else {
      JSONEcho("NO - write");
    }
  } else {
    JSONEcho("NO - read");
  }
  free(cgiinput);
}

StringRead behaves like fgetc(inFile) but operates on a string instead. Here it is:

// like fputc for strings. returns successize characters until EOF.
int StringRead(char **sp) {
  char *s;
  int val;
  s = *sp;
  if ('\0' == *s){
    return EOF;
  }
  val = *s++;
  *sp = s;
  return val;
}

Fetch from Disk

Our data file is a series of lines, as long as they need to be, of: name = "value", where name is our single character, lower case, alphabetic key names, and value is a string, which may have carriage returns, where any included double quotes or backslahes are included by escaping them with a preceeding backslash (\) character.

If we could use lex or flex, M.E. Lesk's tool for generating regular expression parsers, we could write our parser as: match a single lower case letter as our name, then look for optional whitespace (runs of spaces, tabs, or returns), an '=', optional whitespace, and finally a double quoted string of arbitrary length that may contain embedded \\ or \". In lex, that's essentially a two line program, that compiles to C source code:

%%
[a-z]   { key = yystring[0] - 'a'; }
[ \t\n]*=[ \t\n]*"([^"]|\\\\|\\")*" { gValues[key] = strdup(yystring); key = -1; }
Well, almost: we should check that key is not -1 before assigning to gValues[key]. We should check that if gValues[key] is not NULL, and free the old value. We should copy yystring and filter out any escaping backslashes.

A more complex lex program would enter the BuildingString state, and handle un-escaping backslashes directly.

But I chose not to write it in lex, but write my own state machine in straight C. That way, the code is more easily ported, and more comprehensible.

Store to Disk

For each line of our model, if the value isn't nil or the empty string, write a name = "value" line to the output file.

// write our data file.
int WriteCookiesToServerFile() {
  int retValue = -1;
  FILE *outFile = fopen(kPath, "w");
  if (outFile) {
    int i;
    for(i = 0; i < kKeyArrayCount; ++i) {
      char *qs = gValues[i];
      if (NULL != qs && 0 < strlen(qs)) {
        fprintf(outFile, "%c=\"", i + 'a');
        char *s;
        for (s = qs; *s;++s){
          char c = *s;
          if ('"' == c || '\\' == c) {
            fputc('\\', outFile);
          }
          fputc(c, outFile);
        }
        fprintf(outFile, "\"\n");
        retValue = 0;
      }
    }
    fclose(outFile);
  }
  return retValue;
}

That's the tour. The complete, actual source code.

Back to Server Side Cookies