This should be replaced with Tom Poindexter's TCL lib or a
[catagits/fcgi2.git] / examples / sample-store.c
CommitLineData
0198fd3c 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
3293ebdf 49static const char rcsid[] = "$Id: sample-store.c,v 1.3 1999/07/26 05:33:00 roberts Exp $";
0198fd3c 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
2fd179ab 62#if defined __linux__
63int fsync(int fd);
64#endif
65
0198fd3c 66/*
2fd179ab 67 * sample-store is designed to be configured as follows (for the OM server):
0198fd3c 68 *
69 * SI_Department SampleStoreDept -EnableAnonymousTicketing 1
3293ebdf 70 * Region /SampleStore/ * { SI_RequireSI SampleStoreDept 1 }
0198fd3c 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
3293ebdf 80 * AuthorizeRegion /SampleStore/Protected/ * SampleStoreAppClass
0198fd3c 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
2fd179ab 133void panic(char *format,
134 char *arg1, char *arg2, char *arg3, char *arg4,
135 char *arg5, char *arg6, char *arg7, char *arg8);
136
0198fd3c 137static void *Malloc(size_t size);
138static void Free(void *ptr);
139static char *StringNCopy(char *str, int strLen);
140static char *StringCopy(char *str);
141static char *StringCat(char *str1, char *str2);
142static char *StringCat4(char *str1, char *str2, char *str3, char *str4);
143static char *QueryLookup(char *query, char *name);
144static char *PathTail(char *path);
145
146typedef struct ListOfString {
147 char *head;
148 struct ListOfString *tail;
149} ListOfString;
150static char *ListOfString_Head(ListOfString *list);
151static ListOfString *ListOfString_Tail(ListOfString *list);
152static int ListOfString_Length(ListOfString *list);
153static int ListOfString_IsElement(ListOfString *list, char *element);
154static ListOfString *ListOfString_AppendElement(
155 ListOfString *list, char *element);
156static ListOfString *ListOfString_RemoveElement(
157 ListOfString *list, char *element);
158
159static int IntGetEnv(char *varName, int defaultValue);
160\f
161static void Initialize(void);
162static void PerformRequest(void);
163static void GarbageCollectStep(void);
164static 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 */
3293ebdf 171int main(void)
0198fd3c 172{
173 Initialize();
3293ebdf 174
175 while (FCGI_Accept() >= 0) {
0198fd3c 176 PerformRequest();
177 FCGI_Finish();
178 GarbageCollectStep();
179 ConditionalCheckpoint();
180 }
3293ebdf 181
182 return 0;
0198fd3c 183}
184\f
185/*
186 * All the global variables
187 */
188typedef struct CartObj {
189 int inactive; /* This cart not accessed since mark */
190 ListOfString *items; /* Items in cart */
191} CartObj;
192static Tcl_HashTable *cartTablePtr; /* Table of CartObj, indexed by userId */
193static Tcl_HashTable cartTable;
194static char *fcgiProcessId; /* Id of this process in affinity group */
195static char *stateDir; /* Path to dir with snapshot and log */
196char *snapshotPath, *logPath; /* Paths to current snapshot and log */
197static int generation; /* Number embedded in paths, inc on ckp */
198static FILE *logFile = NULL; /* Open for append to current log file */
199static int numLogRecords; /* Number of records in current log file */
200static int checkpointThreshold; /* Do ckp when numLogRecords exceeds this */
201static int purge = TRUE; /* Cart collector is removing inactives */
202static time_t timeCartsMarked; /* Time all carts marked inactive */
203static 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
221static char *MakePath(char *dir, char *prefix, int gen);
222static void AnalyzeStateDir(
223 char *dir, char *prefix, int *largestP, ListOfString **fileListP);
224static int RecoverFile(char *pathname);
225static void Checkpoint(void);
226
227/*
228 * Initialize the process by reading environment variables and files
229 */
230static void Initialize(void)
231{
0198fd3c 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
289static 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
296static void ConditionalCheckpoint(void)
297{
298 if(numLogRecords >= checkpointThreshold) {
299 Checkpoint();
300 }
301}
302static void WriteSnapshot(char *snpPath);
303
304static 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 */
353static 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
384static int DoAddItemToCart(char *userId, char *item, int writeLog);
385static int DoRemoveItemFromCart(char *userId, char *item, int writeLog);
386static 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 */
392static 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
424static 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 */
430static 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
452static 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
462static int RemoveOneInactiveCart(void);
463static 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
482static 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
498static 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
517static Tcl_HashEntry *GetCartEntry(char *userId);
518
519static 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
532static 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
545static void DisplayStore(
546 char *scriptName, char *parent, char *userId, char *processId);
547static void AddItemToCart(
548 char *scriptName, char *parent, char *userId, char *processId,
549 char *item);
550static void DisplayCart(
551 char *scriptName, char *parent, char *userId, char *processId);
552static void RemoveItemFromCart(
553 char *scriptName, char *parent, char *userId, char *processId,
554 char *item);
555static void Purchase(
556 char *scriptName, char *parent, char *userId, char *processId);
557static void InvalidRequest(char *code, char *message);
558static 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
576static 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
626char *ItemNames[] = {
627 "BrooklynBridge",
628 "RMSTitanic",
629 "CometKohoutec",
630 "YellowSubmarine",
631 NULL
632 };
633char *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
644static 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
658static void DisplayNumberOfItems(int numberOfItems, char *processId);
659
660static 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
675static void DisplayFoot(void)
676{
677 printf("<hr>\n</body>\n</html>\n");
678}
679
680static 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
709static 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
724static 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
741static 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 }
2fd179ab 754 return 0;
0198fd3c 755}
756\f
757static 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);
0198fd3c 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
783static 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
800static 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 }
2fd179ab 814 return 0;
0198fd3c 815}
816\f
817static 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
825static 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
846static void NotAuthorized(void);
847
848static 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
863static 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
877static 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
885static 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 */
902static 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 */
912static 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 */
923static 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 */
934static 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 */
943static char *StringCat(char *str1, char *str2)
944{
945 return StringCat4(str1, str2, NULL, NULL);
946}
947
948static 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 */
967static char *QueryLookup(char *query, char *name)
968{
969 int nameLen = strlen(name);
2fd179ab 970 char *queryTail, *nameFirst, *valueFirst, *valueLast;
971
0198fd3c 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 */
998static 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 */
1015static 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 */
1031void 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
1043static char *ListOfString_Head(ListOfString *list)
1044{
1045 return list->head;
1046}
1047
1048static ListOfString *ListOfString_Tail(ListOfString *list)
1049{
1050 return list->tail;
1051}
1052
1053static 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
1062static 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
1072static 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
1089static 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 */