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.
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.
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];
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"; }
To set the string value for name, we use an HTTP POST
(since HTTP GET
s 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; }
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.
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