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