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 |
2fd179ab |
49 | static 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__ |
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 |
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 |
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 | */ |
171 | void 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 | */ |
185 | typedef struct CartObj { |
186 | int inactive; /* This cart not accessed since mark */ |
187 | ListOfString *items; /* Items in cart */ |
188 | } CartObj; |
189 | static Tcl_HashTable *cartTablePtr; /* Table of CartObj, indexed by userId */ |
190 | static Tcl_HashTable cartTable; |
191 | static char *fcgiProcessId; /* Id of this process in affinity group */ |
192 | static char *stateDir; /* Path to dir with snapshot and log */ |
193 | char *snapshotPath, *logPath; /* Paths to current snapshot and log */ |
194 | static int generation; /* Number embedded in paths, inc on ckp */ |
195 | static FILE *logFile = NULL; /* Open for append to current log file */ |
196 | static int numLogRecords; /* Number of records in current log file */ |
197 | static int checkpointThreshold; /* Do ckp when numLogRecords exceeds this */ |
198 | static int purge = TRUE; /* Cart collector is removing inactives */ |
199 | static time_t timeCartsMarked; /* Time all carts marked inactive */ |
200 | static 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 | |
218 | static char *MakePath(char *dir, char *prefix, int gen); |
219 | static void AnalyzeStateDir( |
220 | char *dir, char *prefix, int *largestP, ListOfString **fileListP); |
221 | static int RecoverFile(char *pathname); |
222 | static void Checkpoint(void); |
223 | |
224 | /* |
225 | * Initialize the process by reading environment variables and files |
226 | */ |
227 | static 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 | |
286 | static 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 |
293 | static void ConditionalCheckpoint(void) |
294 | { |
295 | if(numLogRecords >= checkpointThreshold) { |
296 | Checkpoint(); |
297 | } |
298 | } |
299 | static void WriteSnapshot(char *snpPath); |
300 | |
301 | static 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 | */ |
350 | static 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 |
381 | static int DoAddItemToCart(char *userId, char *item, int writeLog); |
382 | static int DoRemoveItemFromCart(char *userId, char *item, int writeLog); |
383 | static 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 | */ |
389 | static 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 |
421 | static 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 | */ |
427 | static 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 |
449 | static 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 |
459 | static int RemoveOneInactiveCart(void); |
460 | static 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 | |
479 | static 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 |
495 | static 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 | |
514 | static Tcl_HashEntry *GetCartEntry(char *userId); |
515 | |
516 | static 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 | |
529 | static 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 | |
542 | static void DisplayStore( |
543 | char *scriptName, char *parent, char *userId, char *processId); |
544 | static void AddItemToCart( |
545 | char *scriptName, char *parent, char *userId, char *processId, |
546 | char *item); |
547 | static void DisplayCart( |
548 | char *scriptName, char *parent, char *userId, char *processId); |
549 | static void RemoveItemFromCart( |
550 | char *scriptName, char *parent, char *userId, char *processId, |
551 | char *item); |
552 | static void Purchase( |
553 | char *scriptName, char *parent, char *userId, char *processId); |
554 | static void InvalidRequest(char *code, char *message); |
555 | static 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 | |
573 | static 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 | |
623 | char *ItemNames[] = { |
624 | "BrooklynBridge", |
625 | "RMSTitanic", |
626 | "CometKohoutec", |
627 | "YellowSubmarine", |
628 | NULL |
629 | }; |
630 | char *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 | |
641 | static 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 |
655 | static void DisplayNumberOfItems(int numberOfItems, char *processId); |
656 | |
657 | static 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 | |
672 | static void DisplayFoot(void) |
673 | { |
674 | printf("<hr>\n</body>\n</html>\n"); |
675 | } |
676 | |
677 | static 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 |
706 | static 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 |
721 | static 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 | |
738 | static 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 |
754 | static 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 |
780 | static 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 | |
797 | static 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 |
814 | static 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 | |
822 | static 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 |
843 | static void NotAuthorized(void); |
844 | |
845 | static 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 |
860 | static 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 |
874 | static 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 | |
882 | static 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 | */ |
899 | static 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 | */ |
909 | static 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 | */ |
920 | static 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 | */ |
931 | static 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 | */ |
940 | static char *StringCat(char *str1, char *str2) |
941 | { |
942 | return StringCat4(str1, str2, NULL, NULL); |
943 | } |
944 | |
945 | static 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 | */ |
964 | static 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 | */ |
995 | static 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 | */ |
1012 | static 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 | */ |
1028 | void 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 | |
1040 | static char *ListOfString_Head(ListOfString *list) |
1041 | { |
1042 | return list->head; |
1043 | } |
1044 | |
1045 | static ListOfString *ListOfString_Tail(ListOfString *list) |
1046 | { |
1047 | return list->tail; |
1048 | } |
1049 | |
1050 | static 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 | |
1059 | static 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 | |
1069 | static 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 | |
1086 | static 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 | */ |