This should be replaced with Tom Poindexter's TCL lib or a
[catagits/fcgi2.git] / examples / sample-store.c
1 /* 
2  * sample-store.c --
3  *
4  *      FastCGI example program using fcgi_stdio library
5  *
6  *
7  * Copyright (c) 1996 Open Market, Inc.
8  *
9  * See the file "LICENSE.TERMS" for information on usage and redistribution
10  * of this file, and for a DISCLAIMER OF ALL WARRANTIES.
11  *
12  *
13  * sample-store is a program designed to illustrate one technique
14  * for writing a high-performance FastCGI application that maintains
15  * permanent state.  It is real enough to demonstrate a range of issues
16  * that can arise in FastCGI application programming.
17  *
18  * sample-store implements per-user shopping carts.  These carts are kept
19  * in memory for speed but are backed up on disk for reliability; the
20  * program can restart at any time, affecting at most one request.  Unlike
21  * CGI applications, the performance impact of sample-store's disk
22  * use is negligible: no I/O for query requests, no reads and one write
23  * for a typical update request.
24  *
25  * sample-store's on-disk representation is extremely simple.  The
26  * current state of all shopping carts managed by a process is kept
27  * in two files, a snapshot and a log.  Both files have the same format,
28  * a sequence of ASCII records.  On restart the current state is restored
29  * by replaying the snapshot and the log.  When the log grows to a certain
30  * length, sample-store writes a new snapshot and empties the log.
31  * This prevents the time needed for restart from growing without
32  * bound.
33  * 
34  * Since users "visit" Web sites, but never "leave", sample-store
35  * deletes a shopping cart after the cart has been inactive
36  * for a certain period of time.  This policy prevents sample-store's
37  * memory requirements from growing without bound.
38  *
39  * sample-store operates both as a FastCGI Responder and as an
40  * Authorizer, showing how one program can play two roles.
41  *
42  * The techniques used in sample-store are not specific to shopping
43  * carts; they apply equally well to maintaining all sorts of
44  * information.
45  *
46  */
47
48 #ifndef lint
49 static const char rcsid[] = "$Id: sample-store.c,v 1.3 1999/07/26 05:33:00 roberts Exp $";
50 #endif /* not lint */
51
52 #include "fcgi_stdio.h"  /* FCGI_Accept, FCGI_Finish, stdio */
53 #include <stdlib.h>      /* malloc/free, getenv, strtol */
54 #include <string.h>      /* strcmp, strncmp, strlen, strstr, strchr */
55 #include <tcl.h>         /* Tcl_*Hash* functions */
56 #include <time.h>        /* time, time_t */
57 #include <assert.h>      /* assert */
58 #include <errno.h>       /* errno, ENOENT */
59 #include <dirent.h>      /* readdir, closedir, DIR, dirent */
60 #include <unistd.h>      /* fsync */
61
62 #if defined __linux__
63 int fsync(int fd);
64 #endif
65
66 /*
67  * sample-store is designed to be configured as follows (for the OM server):
68  *
69  * SI_Department SampleStoreDept -EnableAnonymousTicketing 1
70  * Region /SampleStore/ *  { SI_RequireSI SampleStoreDept 1 }
71  *
72  * Filemap /SampleStore $fcgi-devel-kit/examples/SampleStore
73  * AppClass  SampleStoreAppClass \
74  *     $fcgi-devel-kit/examples/sample-store \
75  *     -initial-env STATE_DIR=$fcgi-devel-kit/examples/SampleStore.state \
76  *     -initial-env CKP_THRESHOLD=100 \
77  *     -initial-env CART_HOLD_MINUTES=240 \
78  *     -processes 2 -affinity
79  * Responder SampleStoreAppClass /SampleStore/App
80  * AuthorizeRegion /SampleStore/Protected/ *  SampleStoreAppClass
81  *
82  * sample-store looks for three initial environment variables:
83  *
84  *  STATE_DIR
85  *    When sample-store is run as a single process without affinity
86  *    this is the directory containing the permanent state of the
87  *    process.  When sample-store is run as multiple processes
88  *    using session affinity, the state directory is
89  *    $STATE_DIR.$FCGI_PROCESS_ID, e.g. SampleStore.state.0
90  *    and SampleStore.state.1 in the config above.  The process
91  *    state directory must exist, but may be empty.
92  *
93  *  CKP_THRESHOLD
94  *    When the log grows to contain this many records the process
95  *    writes a new snapshot and truncates the log.  Defaults
96  *    to CKP_THRESHOLD_DEFAULT.
97  *
98  *  CART_HOLD_MINUTES
99  *    When a cart has not been accessed for this many minutes it
100  *    may be deleted.  Defaults to CART_HOLD_MINUTES_DEFAULT.
101  *
102  * The program is prepared to run as multiple processes using
103  * session affinity (illustrated in config above) or as a single process.
104  *
105  * The program does not depend upon the specific URL prefix /SampleStore.
106  *
107  */
108 \f
109 /*
110  * This code is organized top-down, trying to put the most interesting
111  * parts first.  Unfortunately, organizing the program in this way requires
112  * lots of extra declarations to take care of forward references.
113  *
114  * Utility functions for string/list processing and such
115  * are left to the very end.  The program uses the Tcl hash table
116  * package because it is both adequate and readily available.
117  */
118
119 #ifndef FALSE
120 #define FALSE (0)
121 #endif
122
123 #ifndef TRUE
124 #define TRUE  (1)
125 #endif
126
127 #ifndef max
128 #define max(a,b) ((a) > (b) ? (a) : (b))
129 #endif
130
131 #define Strlen(str) (((str) == NULL) ? 0 : strlen(str))
132
133 void panic(char *format,
134         char *arg1, char *arg2, char *arg3, char *arg4,
135         char *arg5, char *arg6, char *arg7, char *arg8);
136
137 static void *Malloc(size_t size);
138 static void Free(void *ptr);
139 static char *StringNCopy(char *str, int strLen);
140 static char *StringCopy(char *str);
141 static char *StringCat(char *str1, char *str2);
142 static char *StringCat4(char *str1, char *str2, char *str3, char *str4);
143 static char *QueryLookup(char *query, char *name);
144 static char *PathTail(char *path);
145
146 typedef struct ListOfString {
147     char *head;
148     struct ListOfString *tail;
149 } ListOfString;
150 static char *ListOfString_Head(ListOfString *list);
151 static ListOfString *ListOfString_Tail(ListOfString *list);
152 static int ListOfString_Length(ListOfString *list);
153 static int ListOfString_IsElement(ListOfString *list, char *element);
154 static ListOfString *ListOfString_AppendElement(
155         ListOfString *list, char *element);
156 static ListOfString *ListOfString_RemoveElement(
157         ListOfString *list, char *element);
158
159 static int IntGetEnv(char *varName, int defaultValue);
160 \f
161 static void Initialize(void);
162 static void PerformRequest(void);
163 static void GarbageCollectStep(void);
164 static void ConditionalCheckpoint(void);
165
166 /*
167  * A typical FastCGI main program: Initialize, then loop
168  * calling FCGI_Accept and performing the accepted request.
169  * Do cleanup operations incrementally between requests.
170  */
171 int main(void)
172 {
173     Initialize();
174
175     while (FCGI_Accept() >= 0) {
176         PerformRequest();
177         FCGI_Finish();
178         GarbageCollectStep();
179         ConditionalCheckpoint();
180     }
181
182     return 0;
183 }
184 \f
185 /*
186  * All the global variables
187  */
188 typedef struct CartObj {
189     int inactive;                   /* This cart not accessed since mark      */
190     ListOfString *items;            /* Items in cart                          */
191 } CartObj;
192 static Tcl_HashTable *cartTablePtr; /* Table of CartObj, indexed by userId    */
193 static Tcl_HashTable cartTable;
194 static char *fcgiProcessId;         /* Id of this process in affinity group   */
195 static char *stateDir;              /* Path to dir with snapshot and log      */
196 char *snapshotPath, *logPath;       /* Paths to current snapshot and log      */
197 static int generation;              /* Number embedded in paths, inc on ckp   */
198 static FILE *logFile = NULL;        /* Open for append to current log file    */
199 static int numLogRecords;           /* Number of records in current log file  */
200 static int checkpointThreshold;     /* Do ckp when numLogRecords exceeds this */
201 static int purge = TRUE;            /* Cart collector is removing inactives   */
202 static time_t timeCartsMarked;      /* Time all carts marked inactive         */
203 static int cartHoldSeconds;         /* Begin purge when this interval elapsed */
204 \f
205 #define STATE_DIR_VAR             "STATE_DIR"
206 #define PID_VAR                   "FCGI_PROCESS_ID"
207 #define CKP_THRESHOLD_VAR         "CKP_THRESHOLD"
208 #define CKP_THRESHOLD_DEFAULT     200
209 #define CART_HOLD_MINUTES_VAR     "CART_HOLD_MINUTES"
210 #define CART_HOLD_MINUTES_DEFAULT 300
211
212 #define SNP_PREFIX    "snapshot"
213 #define LOG_PREFIX    "log"
214 #define TMP_SNP_NAME  "tmp-snapshot"
215
216 #define LR_ADD_ITEM    "Add"
217 #define LR_REMOVE_ITEM "Rem"
218 #define LR_EMPTY_CART  "Emp"
219
220
221 static char *MakePath(char *dir, char *prefix, int gen);
222 static void AnalyzeStateDir(
223     char *dir, char *prefix, int *largestP, ListOfString **fileListP);
224 static int RecoverFile(char *pathname);
225 static void Checkpoint(void);
226
227 /*
228  * Initialize the process by reading environment variables and files
229  */
230 static void Initialize(void)
231 {
232     ListOfString *fileList;
233     int stateDirLen;
234     /*
235      * Process miscellaneous parameters from the initial environment.
236      */
237     checkpointThreshold =
238             IntGetEnv(CKP_THRESHOLD_VAR, CKP_THRESHOLD_DEFAULT);
239     cartHoldSeconds =
240             IntGetEnv(CART_HOLD_MINUTES_VAR, CART_HOLD_MINUTES_DEFAULT)*60;
241     /*
242      * Create an empty in-memory shopping cart data structure.
243      */
244     cartTablePtr = &cartTable;
245     Tcl_InitHashTable(cartTablePtr, TCL_STRING_KEYS);
246     /*
247      * Compute the state directory name from the initial environment
248      * variables.
249      */
250     stateDir = getenv(STATE_DIR_VAR);
251     stateDirLen = Strlen(stateDir);
252     assert(stateDirLen > 0);
253     if(stateDir[stateDirLen - 1] == '/') {
254         stateDir[stateDirLen - 1] = '\000';
255     }
256     fcgiProcessId = getenv(PID_VAR);
257     if(fcgiProcessId != NULL) {
258         stateDir = StringCat4(stateDir, ".", fcgiProcessId, "/");
259     } else {
260         stateDir = StringCat(stateDir, "/");
261     }
262     /*
263      * Read the state directory to determine the current
264      * generation number and a list of files that may
265      * need to be deleted (perhaps left over from an earlier
266      * system crash).  Recover the current generation
267      * snapshot and log (either or both may be missing),
268      * populating the in-memory shopping cart data structure.
269      * Take a checkpoint, making the current log empty.
270      */
271     AnalyzeStateDir(stateDir, SNP_PREFIX, &generation, &fileList);
272     snapshotPath = MakePath(stateDir, SNP_PREFIX, generation);
273     RecoverFile(snapshotPath);
274     logPath = MakePath(stateDir, LOG_PREFIX, generation);
275     numLogRecords = RecoverFile(logPath);
276     Checkpoint();
277     /*
278      * Clean up stateDir without removing the current snapshot and log.
279      */
280     while(fileList != NULL) {
281         char *cur = ListOfString_Head(fileList);
282         if(strcmp(snapshotPath, cur) && strcmp(logPath, cur)) {
283             remove(cur);
284         }
285         fileList = ListOfString_RemoveElement(fileList, cur);
286     }
287 }
288
289 static char *MakePath(char *dir, char *prefix, int gen)
290 {
291     char nameBuffer[24];
292     sprintf(nameBuffer, "%s.%d", prefix, gen);
293     return  StringCat(dir, nameBuffer);
294 }
295 \f
296 static void ConditionalCheckpoint(void)
297 {
298     if(numLogRecords >= checkpointThreshold) {
299         Checkpoint();
300     }
301 }
302 static void WriteSnapshot(char *snpPath);
303
304 static void Checkpoint(void)
305 {
306     char *tempSnapshotPath, *newLogPath, *newSnapshotPath;
307     /*
308      * Close the current log file.
309      */
310     if(logFile != NULL) {
311         fclose(logFile);
312     }
313     /*
314      * Create a new snapshot with a temporary name.
315      */
316     tempSnapshotPath = StringCat(stateDir, TMP_SNP_NAME);
317     WriteSnapshot(tempSnapshotPath);
318     ++generation;
319     /*
320      * Ensure that the new log file doesn't already exist by removing it.
321      */
322     newLogPath = MakePath(stateDir, LOG_PREFIX, generation);
323     remove(newLogPath);
324     /*
325      * Commit by renaming the snapshot.  The rename atomically
326      * makes the old snapshot and log obsolete.
327      */
328     newSnapshotPath = MakePath(stateDir, SNP_PREFIX, generation);
329     rename(tempSnapshotPath, newSnapshotPath);
330     /*
331      * Clean up the old snapshot and log.
332      */
333     Free(tempSnapshotPath);
334     remove(snapshotPath);
335     Free(snapshotPath);
336     snapshotPath = newSnapshotPath;
337     remove(logPath);
338     Free(logPath);
339     logPath = newLogPath;
340     /*
341      * Open the new, empty log.
342      */
343     logFile = fopen(logPath, "a");
344     numLogRecords = 0;
345 }
346 \f
347 /*
348  * Return *largestP     = the largest int N such that the name prefix.N
349  *                        is in the directory dir.  0 if no such name
350  *        *fileListP    = list of all files in the directory dir,
351  *                        excluding '.' and '..'
352  */
353 static void AnalyzeStateDir(
354     char *dir, char *prefix, int *largestP, ListOfString **fileListP)
355 {
356     DIR *dp;
357     struct dirent *dirp;
358     int prefixLen = strlen(prefix);
359     int largest = 0;
360     int cur;
361     char *curName;
362     ListOfString *fileList = NULL;
363     dp = opendir(dir);
364     assert(dp != NULL);
365     while((dirp = readdir(dp)) != NULL) {
366         if(!strcmp(dirp->d_name, ".") || !strcmp(dirp->d_name, "..")) {
367             continue;
368         }
369         curName = StringCat(dir, dirp->d_name);
370         fileList = ListOfString_AppendElement(fileList, curName);
371         if(!strncmp(dirp->d_name, prefix, prefixLen)
372                 && (dirp->d_name)[prefixLen] == '.') {
373             cur = strtol(dirp->d_name + prefixLen + 1, NULL, 10);
374             if(cur > largest) {
375                 largest = cur;
376             }
377         }
378     }
379     assert(closedir(dp) >= 0);
380     *largestP = largest;
381     *fileListP = fileList;
382 }
383 \f
384 static int DoAddItemToCart(char *userId, char *item, int writeLog);
385 static int DoRemoveItemFromCart(char *userId, char *item, int writeLog);
386 static int DoEmptyCart(char *userId, int writeLog);
387
388 /*
389  * Read either a snapshot or a log and perform the specified
390  * actions on the in-memory representation.
391  */
392 static int RecoverFile(char *pathname)
393 {
394     int numRecords;
395     FILE *recoveryFile = fopen(pathname, "r");
396     if(recoveryFile == NULL) {
397         assert(errno == ENOENT);
398         return 0;
399     }
400     for(numRecords = 0; ; numRecords++) {
401         char buff[128];
402         char op[32], userId[32], item[64];
403         int count;
404         char *status = fgets(buff, sizeof(buff), recoveryFile);
405         if(status == NULL) {
406             assert(feof(recoveryFile));
407             fclose(recoveryFile);
408             return numRecords;
409         }
410         count = sscanf(buff, "%31s %31s %63s", op, userId, item);
411         assert(count == 3);
412         if(!strcmp(op, LR_ADD_ITEM)) {
413             assert(DoAddItemToCart(userId, item, FALSE) >= 0);
414         } else if(!strcmp(op, LR_REMOVE_ITEM)) {
415             assert(DoRemoveItemFromCart(userId, item, FALSE) >= 0);
416         } else if(!strcmp(op, LR_EMPTY_CART)) {
417             assert(DoEmptyCart(userId, FALSE) >= 0);
418         } else {
419             assert(FALSE);
420         }
421     }
422 }
423 \f
424 static void WriteLog(char *command, char *userId, char *item, int force);
425
426 /*
427  * Read the in-memory representation and write a snapshot file
428  * that captures it.
429  */
430 static void WriteSnapshot(char *snpPath)
431 {
432     Tcl_HashSearch search;
433     Tcl_HashEntry *cartEntry;
434     ListOfString *items;
435     char *userId;
436     logFile = fopen(snpPath, "w");
437     assert(logFile != NULL);
438     cartEntry = Tcl_FirstHashEntry(cartTablePtr, &search);
439     for(cartEntry = Tcl_FirstHashEntry(cartTablePtr, &search);
440             cartEntry != NULL; cartEntry = Tcl_NextHashEntry(&search)) {
441         userId = Tcl_GetHashKey(cartTablePtr, cartEntry);
442         for(items = ((CartObj *) Tcl_GetHashValue(cartEntry))->items;
443                 items != NULL; items = ListOfString_Tail(items)) {
444             WriteLog(LR_ADD_ITEM, userId, ListOfString_Head(items), FALSE);
445         }
446     }
447     fflush(logFile);
448     fsync(fileno(logFile));
449     fclose(logFile);
450 }
451 \f
452 static void WriteLog(char *command, char *userId, char *item, int force)
453 {
454     fprintf(logFile, "%s %s %s\n", command, userId, item);
455     ++numLogRecords;
456     if(force) {
457         fflush(logFile);
458         fsync(fileno(logFile));
459     }
460 }
461 \f
462 static int RemoveOneInactiveCart(void);
463 static void MarkAllCartsInactive(void);
464
465 /*
466  * Incremental garbage collection of inactive shopping carts:
467  *
468  * Each user access to a shopping cart clears its "inactive" bit via a
469  * call to MarkThisCartActive.  When restart creates a cart it
470  * also marks the cart active.
471  *
472  * If purge == TRUE, each call to GarbageCollectStep scans for and removes
473  * the first inactive cart found.  If there are no inactive carts,
474  * GarbageCollectStep marks *all* carts inactive, records the time in
475  * timeCartsMarked, and sets purge = FALSE.
476  *
477  * If purge == FALSE, each call to GarbageCollectStep checks the
478  * elapsed time since timeCartsMarked.  If the elapsed time
479  * exceeds a threshold, GarbageCollectStep sets purge = TRUE.
480  */
481
482 static void GarbageCollectStep(void)
483 {
484     if(purge) {
485         if(!RemoveOneInactiveCart()) {
486             MarkAllCartsInactive();
487             timeCartsMarked = time(NULL);
488             purge = FALSE;
489         }
490     } else {
491         int diff = time(NULL)-timeCartsMarked;
492         if(diff > cartHoldSeconds) {
493             purge = TRUE;
494         }
495     }
496 }
497 \f
498 static int RemoveOneInactiveCart(void)
499 {
500     Tcl_HashSearch search;
501     Tcl_HashEntry *cartEntry;
502     CartObj *cart;
503     char *userId;
504     cartEntry = Tcl_FirstHashEntry(cartTablePtr, &search);
505     for(cartEntry = Tcl_FirstHashEntry(cartTablePtr, &search);
506             cartEntry != NULL; cartEntry = Tcl_NextHashEntry(&search)) {
507         cart = Tcl_GetHashValue(cartEntry);
508         if(cart->inactive) {
509             userId = Tcl_GetHashKey(cartTablePtr, cartEntry);
510             DoEmptyCart(userId, TRUE);
511             return TRUE;
512         }
513     }
514     return FALSE;
515 }
516
517 static Tcl_HashEntry *GetCartEntry(char *userId);
518
519 static void MarkAllCartsInactive(void)
520 {
521     Tcl_HashSearch search;
522     Tcl_HashEntry *cartEntry;
523     CartObj *cart;
524     cartEntry = Tcl_FirstHashEntry(cartTablePtr, &search);
525     for(cartEntry = Tcl_FirstHashEntry(cartTablePtr, &search);
526             cartEntry != NULL; cartEntry = Tcl_NextHashEntry(&search)) {
527         cart = Tcl_GetHashValue(cartEntry);
528         cart->inactive = TRUE;
529     }
530 }
531
532 static void MarkThisCartActive(char *userId)
533 {
534     Tcl_HashEntry *cartEntry = GetCartEntry(userId);
535     CartObj *cart = Tcl_GetHashValue(cartEntry);
536     cart->inactive = FALSE;
537 }
538 \f
539 #define OP_DISPLAY_STORE "DisplayStore"
540 #define OP_ADD_ITEM      "AddItemToCart"
541 #define OP_DISPLAY_CART  "DisplayCart"
542 #define OP_REMOVE_ITEM   "RemoveItemFromCart"
543 #define OP_PURCHASE      "Purchase"
544
545 static void DisplayStore(
546         char *scriptName, char *parent, char *userId, char *processId);
547 static void AddItemToCart(
548         char *scriptName, char *parent, char *userId, char *processId,
549         char *item);
550 static void DisplayCart(
551         char *scriptName, char *parent, char *userId, char *processId);
552 static void RemoveItemFromCart(
553         char *scriptName, char *parent, char *userId, char *processId,
554         char *item);
555 static void Purchase(
556         char *scriptName, char *parent, char *userId, char *processId);
557 static void InvalidRequest(char *code, char *message);
558 static void Authorize(char *userId);
559
560 /*
561  * As a Responder, this application expects to be called with the
562  * GET method and a URL of the form
563  *
564  *     http://<host-port>/<script-name>?op=<op>&item=<item>
565  *
566  * The application expects the SI_UID variable to provide
567  * a user ID, either authenticated or anonymous.
568  *
569  * The application expects the directory *containing* <script-name>
570  * to contain various static HTML files related to the application.
571  *
572  * As an Authorizer, the application expects to be called with
573  * SID_UID and URL_PATH set.
574  */
575
576 static void PerformRequest(void)
577 {
578     char *method = getenv("REQUEST_METHOD");
579     char *role = getenv("FCGI_ROLE");
580     char *scriptName = PathTail(getenv("SCRIPT_NAME"));
581     char *parent = "";
582     char *op = QueryLookup(getenv("QUERY_STRING"), "op");
583     char *item = QueryLookup(getenv("QUERY_STRING"), "item");
584     char *userId =  getenv("SI_UID");
585     if(userId == NULL) {
586         InvalidRequest("405", "Incorrect configuration, no user id");
587         goto done;
588     } else {
589         MarkThisCartActive(userId);
590     }
591     if(!strcmp(role, "RESPONDER")) {
592         if(strcmp(method, "GET")) {
593             InvalidRequest("405", "Only GET Method Allowed");
594         } else if(op == NULL || !strcmp(op, OP_DISPLAY_STORE)) {
595             DisplayStore(scriptName, parent, userId, fcgiProcessId);
596         } else if(!strcmp(op, OP_ADD_ITEM)) {
597             AddItemToCart(scriptName, parent, userId, fcgiProcessId, item);
598         } else if(!strcmp(op, OP_DISPLAY_CART)) {
599             DisplayCart(scriptName, parent, userId, fcgiProcessId);
600         } else if(!strcmp(op, OP_REMOVE_ITEM)) {
601             RemoveItemFromCart(scriptName, parent, userId, fcgiProcessId, item);
602         } else if(!strcmp(op, OP_PURCHASE)) {
603             Purchase(scriptName, parent, userId, fcgiProcessId);
604         } else {
605             InvalidRequest("404", "Invalid 'op' argument");
606         }
607     } else if(!strcmp(role, "AUTHORIZER")) {
608         Authorize(userId);
609     } else {
610         InvalidRequest("404", "Invalid FastCGI Role");
611     }
612   done:
613     Free(scriptName);
614     Free(op);
615     Free(item);
616 }
617 \f
618 /*
619  * Tiny database of shop inventory.  The first form is the
620  * item identifier used in a request, the second form is used
621  * for HTML display.  REQUIRED_ITEM is the item required
622  * the the Authorizer.  SPECIAL_ITEM is the item on the protected
623  * page (must follow unprotected items in table).
624  */
625
626 char *ItemNames[] = {
627         "BrooklynBridge",
628         "RMSTitanic",
629         "CometKohoutec",
630         "YellowSubmarine",
631         NULL
632         };
633 char *ItemDisplayNames[] = {
634         "<i>Brooklyn Bridge</i>",
635         "<i>RMS Titanic</i>",
636         "<i>Comet Kohoutec</i>",
637         "<i>Yellow Submarine</i>",
638         NULL
639         };
640 #define REQUIRED_ITEM 1
641 #define SPECIAL_ITEM 3
642
643
644 static char *ItemDisplayName(char *item)
645 {
646     int i;
647     if(item == NULL) {
648         return NULL;
649     }
650     for(i = 0; ItemNames[i] != NULL; i++) {
651         if(!strcmp(item, ItemNames[i])) {
652             return ItemDisplayNames[i];
653         }
654     }
655     return NULL;
656 }
657 \f
658 static void DisplayNumberOfItems(int numberOfItems, char *processId);
659
660 static void DisplayHead(char *title, char *parent, char *gif)
661 {
662     printf("Content-type: text/html\r\n"
663            "\r\n"
664            "<html>\n<head>\n<title>%s</title>\n</head>\n\n"
665            "<body bgcolor=\"ffffff\" text=\"000000\" link=\"39848c\"\n"
666            "      vlink=\"808080\" alink=\"000000\">\n", title);
667     if(parent != NULL && gif != NULL) {
668         printf("<center>\n<img src=\"%s%s\" alt=\"[%s]\">\n</center>\n\n",
669                parent, gif, title);
670     } else {
671         printf("<h2>%s</h2>\n<hr>\n\n", title);
672     }
673 }
674
675 static void DisplayFoot(void)
676 {
677     printf("<hr>\n</body>\n</html>\n");
678 }
679
680 static void DisplayStore(
681         char *scriptName, char *parent, char *userId, char *processId)
682 {
683     Tcl_HashEntry *cartEntry = GetCartEntry(userId);
684     ListOfString *items = ((CartObj *) Tcl_GetHashValue(cartEntry))->items;
685     int numberOfItems = ListOfString_Length(items);
686     int i;
687
688     DisplayHead("FastCGI Shop!", parent, "Images/main-hd.gif");
689     DisplayNumberOfItems(numberOfItems, processId);
690     printf("<h3>Goods for sale:</h3>\n<ul>\n");
691     for(i = 0; i < SPECIAL_ITEM; i++) {
692         printf("  <li>Add the <a href=\"%s?op=AddItemToCart&item=%s\">%s</a>\n"
693                "      to your shopping cart.\n",
694                scriptName, ItemNames[i], ItemDisplayNames[i]);
695     }
696     printf("</ul><p>\n\n");
697     printf("If the %s is in your shopping cart,\n"
698            "<a href=\"%sProtected/%s.html\">go see a special offer</a>\n"
699            "available only to %s purchasers.<p>\n\n",
700            ItemDisplayNames[REQUIRED_ITEM], parent,
701            ItemNames[REQUIRED_ITEM], ItemDisplayNames[REQUIRED_ITEM]);
702     printf("<a href=\"%sUnprotected/Purchase.html\">Purchase\n"
703            "the contents of your shopping cart.</a><p>\n\n", parent);
704     printf("<a href=\"%s?op=DisplayCart\">View the contents\n"
705            "of your shopping cart.</a><p>\n\n", scriptName);
706     DisplayFoot();
707 }
708 \f
709 static Tcl_HashEntry *GetCartEntry(char *userId)
710 {
711     Tcl_HashEntry *cartEntry = Tcl_FindHashEntry(cartTablePtr, userId);
712     int new;
713     if(cartEntry == NULL) {
714         CartObj *cart = Malloc(sizeof(CartObj));
715         cart->inactive = FALSE;
716         cart->items = NULL;
717         cartEntry = Tcl_CreateHashEntry(cartTablePtr, userId, &new);
718         assert(new);
719         Tcl_SetHashValue(cartEntry, cart);
720     }
721     return cartEntry;
722 }
723 \f
724 static void AddItemToCart(
725         char *scriptName, char *parent, char *userId, char *processId,
726         char *item)
727 {
728     if(DoAddItemToCart(userId, item, TRUE) < 0) {
729         InvalidRequest("404", "Invalid 'item' argument");
730     } else {
731         /*
732          * Would call
733          *   DisplayStore(scriptName, parent, userId, processId);
734          * except for browser reload issue.  Redirect instead.
735          */
736         printf("Location: %s?op=%s\r\n"
737                "\r\n", scriptName, OP_DISPLAY_STORE);
738     }
739 }  
740
741 static int DoAddItemToCart(char *userId, char *item, int writeLog)
742 {
743     if(ItemDisplayName(item) == NULL) {
744         return -1;
745     } else {
746         Tcl_HashEntry *cartEntry = GetCartEntry(userId);
747         CartObj *cart = Tcl_GetHashValue(cartEntry);
748         cart->items = ListOfString_AppendElement(
749                               cart->items, StringCopy(item));
750         if(writeLog) {
751             WriteLog(LR_ADD_ITEM, userId, item, TRUE);
752         }
753     }
754     return 0;
755 }
756 \f
757 static void DisplayCart(
758         char *scriptName, char *parent, char *userId, char *processId)
759 {
760     Tcl_HashEntry *cartEntry = GetCartEntry(userId);
761     CartObj *cart = Tcl_GetHashValue(cartEntry);
762     ListOfString *items = cart->items;
763     int numberOfItems = ListOfString_Length(items);
764
765     DisplayHead("Your shopping cart", parent, "Images/cart-hd.gif");
766     DisplayNumberOfItems(numberOfItems, processId);
767     printf("<ul>\n");
768     for(; items != NULL; items = ListOfString_Tail(items)) {
769         char *item = ListOfString_Head(items);
770         printf("  <li>%s . . . . . \n"
771                "    <a href=\"%s?op=RemoveItemFromCart&item=%s\">Click\n"
772                "    to remove</a> from your shopping cart.\n",
773                ItemDisplayName(item), scriptName, item);
774     }
775     printf("</ul><p>\n\n");
776     printf("<a href=\"%sUnprotected/Purchase.html\">Purchase\n"
777            "the contents of your shopping cart.</a><p>\n\n", parent);
778     printf("<a href=\"%s?op=DisplayStore\">Return to shop.</a><p>\n\n",
779            scriptName);
780     DisplayFoot();
781 }
782 \f
783 static void RemoveItemFromCart(
784         char *scriptName, char *parent, char *userId, char *processId,
785         char *item)
786 {
787     if(DoRemoveItemFromCart(userId, item, TRUE) < 0) {
788         InvalidRequest("404", "Invalid 'item' argument");
789     } else {
790         /*
791          * Would call
792          *   DisplayCart(scriptName, parent, userId, processId);
793          * except for browser reload issue.  Redirect instead.
794          */
795         printf("Location: %s?op=%s\r\n"
796                "\r\n", scriptName, OP_DISPLAY_CART);
797     }
798 }
799
800 static int DoRemoveItemFromCart(char *userId, char *item, int writeLog)
801 {
802     if(ItemDisplayName(item) == NULL) {
803         return -1;
804     } else {
805         Tcl_HashEntry *cartEntry = GetCartEntry(userId);
806         CartObj *cart = Tcl_GetHashValue(cartEntry);
807         if(ListOfString_IsElement(cart->items, item)) {
808             cart->items = ListOfString_RemoveElement(cart->items, item);
809             if (writeLog) {
810                 WriteLog(LR_REMOVE_ITEM, userId, item, TRUE);
811             }
812         }
813     }
814     return 0;
815 }
816 \f
817 static void Purchase(
818         char *scriptName, char *parent, char *userId, char *processId)
819 {
820     DoEmptyCart(userId, TRUE);
821     printf("Location: %sUnprotected/ThankYou.html\r\n"
822            "\r\n", parent);
823 }
824
825 static int DoEmptyCart(char *userId, int writeLog)
826 {
827     Tcl_HashEntry *cartEntry = GetCartEntry(userId);
828     CartObj *cart = Tcl_GetHashValue(cartEntry);
829     ListOfString *items = cart->items;
830     /*
831      * Write log *before* tearing down cart structure because userId
832      * is part of the structure.  (Thanks, Purify.)
833      */
834     if (writeLog) {
835         WriteLog(LR_EMPTY_CART, userId, "NullItem", TRUE);
836     }
837     while(items != NULL) {
838         items = ListOfString_RemoveElement(
839                 items, ListOfString_Head(items));
840     }
841     Free(cart);
842     Tcl_DeleteHashEntry(cartEntry);
843     return 0;
844 }
845 \f
846 static void NotAuthorized(void);
847
848 static void Authorize(char *userId)
849 {
850     Tcl_HashEntry *cartEntry = GetCartEntry(userId);
851     ListOfString *items = ((CartObj *) Tcl_GetHashValue(cartEntry))->items;
852     for( ; items != NULL; items = ListOfString_Tail(items)) {
853         if(!strcmp(ListOfString_Head(items), ItemNames[REQUIRED_ITEM])) {
854             printf("Status: 200 OK\r\n"
855                    "Variable-Foo: Bar\r\n"
856                    "\r\n");
857            return;
858         }
859     }
860     NotAuthorized();
861 }
862 \f
863 static void DisplayNumberOfItems(int numberOfItems, char *processId)
864 {
865     if(processId != NULL) {
866         printf("FastCGI process %s is serving you today.<br>\n", processId);
867     }
868     if(numberOfItems == 0) {
869         printf("Your shopping cart is empty.<p>\n\n");
870     } else if(numberOfItems == 1) {
871         printf("Your shopping cart contains 1 item.<p>\n\n");
872     } else {
873         printf("Your shopping cart contains %d items.<p>\n\n", numberOfItems);
874     };
875 }
876 \f
877 static void InvalidRequest(char *code, char *message)
878 {
879     printf("Status: %s %s\r\n", code, message);
880     DisplayHead("Invalid request", NULL, NULL);
881     printf("%s.\n\n", message);
882     DisplayFoot();
883 }
884
885 static void NotAuthorized(void)
886 {
887     printf("Status: 403 Forbidden\r\n");
888     DisplayHead("Access Denied", NULL, NULL);
889     printf("Put the %s in your cart to access this page.\n\n",
890            ItemDisplayNames[REQUIRED_ITEM]);
891     DisplayFoot();
892 }
893 \f
894 /*
895  * Mundane utility functions, not specific to this application:
896  */
897
898
899 /*
900  * Fail-fast version of 'malloc'
901  */
902 static void *Malloc(size_t size)
903 {
904     void *result = malloc(size);
905     assert(size == 0 || result != NULL);
906     return result;
907 }
908
909 /*
910  * Protect against old, broken implementations of 'free'
911  */
912 static void Free(void *ptr)
913 {
914     if(ptr != NULL) {
915         free(ptr);
916       }
917 }
918
919 /*
920  * Return a new string created by calling Malloc, copying strLen
921  * characters from str to the new string, then appending a null.
922  */
923 static char *StringNCopy(char *str, int strLen)
924 {
925     char *newString = Malloc(strLen + 1);
926     memcpy(newString, str, strLen);
927     newString[strLen] = '\000';
928     return newString;
929 }
930
931 /*
932  * Return a new string that's a copy of str, including the null
933  */
934 static char *StringCopy(char *str)
935 {
936     return StringNCopy(str, strlen(str));
937 }
938
939 /*
940  * Return a new string that's a copy of str1 followed by str2,
941  * including the null
942  */
943 static char *StringCat(char *str1, char *str2)
944 {
945     return StringCat4(str1, str2, NULL, NULL);
946 }
947
948 static char *StringCat4(char *str1, char *str2, char *str3, char *str4)
949 {
950     int str1Len = Strlen(str1);
951     int str2Len = Strlen(str2);
952     int str3Len = Strlen(str3);
953     int str4Len = Strlen(str4);
954     char *newString = Malloc(str1Len + str2Len + str3Len + str4Len + 1);
955     memcpy(newString, str1, str1Len);
956     memcpy(newString + str1Len, str2, str2Len);
957     memcpy(newString + str1Len + str2Len, str3, str3Len);
958     memcpy(newString + str1Len + str2Len + str3Len, str4, str4Len);
959     newString[str1Len + str2Len + str3Len + str4Len] = '\000';
960     return newString;
961 }
962
963 /*
964  * Return a copy of the value associated with 'name' in 'query'.
965  * XXX: does not perform URL-decoding of query.
966  */
967 static char *QueryLookup(char *query, char *name)
968 {
969     int nameLen = strlen(name);
970     char *queryTail, *nameFirst, *valueFirst, *valueLast;
971
972     if(query == NULL) {
973         return NULL;
974     }
975     queryTail = query;
976     for(;;) {
977         nameFirst = strstr(queryTail, name);
978         if(nameFirst == NULL) {
979             return NULL;
980         }
981         if(((nameFirst == query) || (nameFirst[-1] == '&')) &&
982                 (nameFirst[nameLen] == '=')) {
983             valueFirst = nameFirst + nameLen + 1;
984             valueLast = strchr(valueFirst, '&');
985             if(valueLast == NULL) {
986                 valueLast = strchr(valueFirst, '\000');
987             };
988             return StringNCopy(valueFirst, valueLast - valueFirst);
989         }
990         queryTail = nameFirst + 1;
991     }
992 }
993
994 /*
995  * Return a copy of the characters following the final '/' character
996  * of path.
997  */
998 static char *PathTail(char *path)
999 {
1000     char *afterSlash, *slash;
1001     if(path == NULL) {
1002         return NULL;
1003     }
1004     afterSlash = path;
1005     while((slash = strchr(afterSlash, '/')) != NULL) {
1006         afterSlash = slash + 1;
1007     }
1008     return StringCopy(afterSlash);
1009 }
1010
1011 /*
1012  * Return the integer value of the specified environment variable,
1013  * or a specified default value if the variable is unbound.
1014  */
1015 static int IntGetEnv(char *varName, int defaultValue)
1016 {
1017     char *strValue = getenv(varName);
1018     int value = 0;
1019     if(strValue != NULL) {
1020         value = strtol(strValue, NULL, 10);
1021     }
1022     if(value <= 0) {
1023         value = defaultValue;
1024     }
1025     return value;
1026 }
1027
1028 /*
1029  * Should the Tcl hash package detect an unrecoverable error(!), halt.
1030  */
1031 void panic(char *format,
1032         char *arg1, char *arg2, char *arg3, char *arg4,
1033         char *arg5, char *arg6, char *arg7, char *arg8)
1034 {
1035     assert(FALSE);
1036 }
1037    
1038
1039 /*
1040  * ListOfString abstraction
1041  */
1042
1043 static char *ListOfString_Head(ListOfString *list)
1044 {
1045     return list->head;
1046 }
1047
1048 static ListOfString *ListOfString_Tail(ListOfString *list)
1049 {
1050     return list->tail;
1051 }
1052
1053 static int ListOfString_Length(ListOfString *list)
1054 {
1055     int length = 0;
1056     for(; list != NULL; list = list->tail) {
1057         length++;
1058     }
1059     return length;
1060 }
1061
1062 static int ListOfString_IsElement(ListOfString *list, char *element)
1063 {
1064     for(; list != NULL; list = list->tail) {
1065         if(!strcmp(list->head, element)) {
1066             return TRUE;
1067         }
1068     }
1069     return FALSE;
1070 }
1071
1072 static ListOfString *ListOfString_AppendElement(
1073         ListOfString *list, char *element)
1074 {
1075     ListOfString *cur;
1076     ListOfString *newCell = Malloc(sizeof(ListOfString));
1077     newCell->head = element;
1078     newCell->tail = NULL;
1079     if(list == NULL) {
1080         return newCell;
1081     } else {
1082         for(cur = list; cur->tail != NULL; cur = cur->tail) {
1083         }
1084         cur->tail = newCell;
1085         return list;
1086     }
1087 }
1088
1089 static ListOfString *ListOfString_RemoveElement(
1090         ListOfString *list, char *element)
1091 {
1092     ListOfString *cur;
1093     ListOfString *prevCell = NULL;
1094     for(cur = list; cur != NULL; cur = cur->tail) {
1095         if(!strcmp(cur->head, element)) {
1096             if(prevCell == NULL) {
1097                 list = cur->tail;
1098             } else {
1099                 prevCell->tail = cur->tail;
1100             }
1101             free(cur->head);
1102             free(cur);
1103             return list;
1104         }
1105         prevCell = cur;
1106     }
1107     return list;
1108 }
1109
1110
1111 /*
1112  * End
1113  */