/* server side cookes
  by David Phillip Oster 09/22/2007

  limits: key is a single lower case letter of the alphabet
   value: value is a Javascript style string.

  undefined keys return the empty stirng.

  file format:
  a="value"\n
  ...

  Caution: cookie file must be writable by web server.
  driving Javascript must send 'sets' escaped.
 */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

static char *kPath = "/home/oster/example.com.Data/cookies.txt";
//static char *kPath = "./cookies.txt";  // for testing
enum {
  kKeyArrayCount = 26
};

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

// parser states
enum {
  kInitialState,
  kEqualExpected,
  kDoubleQuoteExpected,
  kSlashExpectedState,
  kSkippingCommentState,
  kBuildingStringState
};

typedef int (*ReaderFunc)(void *);

// stores directly into our global dictionary.
/* a statemachine that reads the grammar:
  <key> = <value>
  where  key is a single upper case letter, and
  value is a javascript style double quoted string, potentially with carriage
    returns, with embedded backslahes protectitng backslashes and double
    quotes, at most 1024 chars long.
  // style comments are also handled.
 */
int ReadCookiesReaderFunc(ReaderFunc readerFunc, void *environ){
  int retValue = -1;
  char s[1024];
  char *sp = s;
  char *sEnd = s + sizeof(s) - 1;
  int key = -1;
  int c;
  int state = kInitialState;
  while(EOF != (c = (*readerFunc)(environ))){
    switch(state){
    case kInitialState: // looking for key or comment
      if ('a' <= c && c <= 'z') {
        key = c - 'a';
        state = kEqualExpected;
      } else if ('#' == c) {
        state = kSkippingCommentState;
      } else if ('/' == c) {
        state = kSlashExpectedState;
      }
      break;
    case kEqualExpected:
      if ('=' == c){
        state = kDoubleQuoteExpected;
      }else if ( ! isspace(c)) {
        state = kInitialState;
      }
      break;
    case kDoubleQuoteExpected:
      if ('"' == c){
        state = kBuildingStringState;
        sp = s;
      }else if ( ! isspace(c)) {
        state = kInitialState;
      }
      break;
    case kBuildingStringState:
      if ('"' == c){
        *sp = '\0';
        if (-1 != key){
          gValues[key] = strdup(s);
          key = -1;
          retValue = 0; // did at least one
        }
        state = kInitialState;
      } else if('\\' == c){
        if (EOF != (c = (*readerFunc)(environ)) && sp < sEnd){
          *sp++ = c;
        }
      } else {
        if (sp < sEnd) {
          *sp++ = c;
        }
      }
      break;
    case kSlashExpectedState:
      if ('/' == c) {
        state = kSkippingCommentState;
      } else {
        state = kInitialState;
      }
      break;
    case kSkippingCommentState:
      if ('\n' == c) {
        state = kInitialState;
      }
      break;
    }
  }
  return retValue;
}

// point the state machine at our data file.
int ReadCookiesFromServerFile() {
  int retValue = -1;
  FILE *inFile = fopen(kPath, "r");
  if (inFile) {
    retValue = ReadCookiesReaderFunc((ReaderFunc) fgetc, inFile);
    fclose(inFile);
  }
  return retValue;
}


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

// echo argument as a JSON string: that is, double quote and replace interior
// double quotes, backslashes with escaped versions, using backslash as the
// escape character. Allocates a temp buffer bug enough to handle excaping every
// source character, plus the double quotes at both ends and the terminating null
// byte. Treat NULL as the empty string.
int JSONEcho(char *s) {
  if (NULL == s) {
    s = "";
  }
  int len = strlen(s);
  char *sbuf = malloc(len*2 + 3);
  char *sp;
  sp = sbuf;
  *sp++ = '"';
  for(;*s; ++s) {
    if ('"' == *s || '\\' == *s) {
      *sp++ = '\\';
    }
    *sp++ = *s;
  }
  *sp++ = '"';
  *sp = '\0';
  printf("Content-type: text/text\n\n%s", sbuf) ;
  free(sbuf);
  return 0;
}

// echo its argument as an HTML web page.
void HTTPEcho(char *s) {
    printf("Content-type: text/html\n\n") ;
    printf("<html>\n"
  "<head><title>%s</title></head>\n"
  "<body>\n"
  "<h1>\"%s\"</h1>\n"
  "</body>\n"
  "</html>\n", s ? s : "", s ? s : "") ;
}

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


// print our name, s, then exit.
void ExitWithError(char *s) {
  char s2[512];
  snprintf(s2, sizeof s2, "ShoppingCGI: %s", s);
  HTTPEcho(s2);
  exit(0);
}

// 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";
}


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

int main (int argc, const char * argv[]) {
  char *requestMethod = getenv("REQUEST_METHOD") ;

  ReadCookiesFromServerFile();

  if (NULL == requestMethod ||
      0 == strcmp(requestMethod, "GET") ||
      0 == strcmp(requestMethod, "HEAD")) {
    JSONEcho(GetServerCookie(getenv("QUERY_STRING")));
  } else if (0 == strcmp(requestMethod, "POST") ) {
    SetServerCookie();
  }
  return 0;
}

