Commit | Line | Data |
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 |
49 | static 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__ |
63 | int 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 |
133 | void panic(char *format, |
134 | char *arg1, char *arg2, char *arg3, char *arg4, |
135 | char *arg5, char *arg6, char *arg7, char *arg8); |
136 | |
0198fd3c |
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 | */ |
3293ebdf |
171 | int 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 | */ |
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 | { |
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 | |
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 | } |
2fd179ab |
754 | return 0; |
0198fd3c |
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); |
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 |
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 | } |
2fd179ab |
814 | return 0; |
0198fd3c |
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); |
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 | */ |
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 | */ |