home *** CD-ROM | disk | FTP | other *** search
/ Il CD di internet / CD.iso / SOURCE / CONTRIB / HTTPD / HTTPD_SO.TAR / httpd_1.3 / src / http_dir.c < prev    next >
Encoding:
C/C++ Source or Header  |  1994-05-07  |  16.9 KB  |  669 lines

  1. /*
  2.  * http_dir.c: Handles the on-the-fly html index generation
  3.  * 
  4.  * Rob McCool
  5.  * 3/23/93
  6.  * 
  7.  */
  8.  
  9.  
  10. /* httpd.h includes proper directory file */
  11. #include "httpd.h"
  12.  
  13. struct ent {
  14.     char *name;
  15.     char *icon;
  16.     char *alt;
  17.     char *desc;
  18.     size_t size;
  19.     time_t lm;
  20.     struct ent *next;
  21. };
  22.  
  23.  
  24. struct item {
  25.     int type;
  26.     char *apply_to;
  27.     char *apply_path;
  28.     char *data;
  29.     struct item *next;
  30. };
  31.  
  32.  
  33. static struct item *icon_list, *alt_list, *desc_list, *ign_list;
  34. static struct item *hdr_list, *rdme_list, *opts_list;
  35.  
  36. static int dir_opts;
  37.  
  38. void init_indexing() {
  39.     icon_list = NULL;
  40.     alt_list = NULL;
  41.     desc_list = NULL;
  42.     ign_list = NULL;
  43.  
  44.     hdr_list = NULL;
  45.     rdme_list = NULL;
  46.     opts_list = NULL;
  47. }
  48.  
  49. void kill_item_list(struct item *p) {
  50.     struct item *q;
  51.  
  52.     while(p) {
  53.         if(p->apply_to) free(p->apply_to);
  54.         if(p->apply_path) free(p->apply_path);
  55.         if(p->data) free(p->data);
  56.         q = p;
  57.         p = p->next;
  58.         free(q);
  59.     }
  60. }
  61.  
  62. void kill_indexing() {
  63.     kill_item_list(icon_list);
  64.     kill_item_list(alt_list);
  65.     kill_item_list(desc_list);
  66.     kill_item_list(ign_list);
  67.  
  68.     kill_item_list(hdr_list);
  69.     kill_item_list(rdme_list);
  70.     kill_item_list(opts_list);
  71. }
  72.  
  73. struct item *new_item(int type, char *to, char *path, char *data, FILE *out) 
  74. {
  75.     struct item *p;
  76.  
  77.     if(!(p = (struct item *)malloc(sizeof(struct item))))
  78.         die(NO_MEMORY,"new_item",out);
  79.  
  80.     p->type = type;
  81.     if(data) {
  82.         if(!(p->data = strdup(data)))
  83.             die(NO_MEMORY,"new_item",out);
  84.     } else
  85.         p->data = NULL;
  86.  
  87.     if(to) {
  88.         if(!(p->apply_to = (char *)malloc(strlen(to) + 2)))
  89.             die(NO_MEMORY,"new_item",out);
  90.         if((type == BY_PATH) && (!is_matchexp(to))) {
  91.             p->apply_to[0] = '*';
  92.             strcpy(&p->apply_to[1],to);
  93.         } else
  94.             strcpy(p->apply_to,to);
  95.     } else
  96.         p->apply_to = NULL;
  97.  
  98.     if(!(p->apply_path = (char *)malloc(strlen(path) + 2)))
  99.         die(NO_MEMORY,"new_item",out);
  100.     sprintf(p->apply_path,"%s*",path);
  101.  
  102.     return p;
  103. }
  104.  
  105. void add_alt(int type, char *alt, char *to, char *path, FILE *out) {
  106.     struct item *p;
  107.  
  108.     if(type == BY_PATH) {
  109.         if(!strcmp(to,"**DIRECTORY**"))
  110.             strcpy(to,"^^DIRECTORY^^");
  111.     }
  112.     p = new_item(type,to,path,alt,out);
  113.     p->next = alt_list;
  114.     alt_list = p;
  115. }
  116.  
  117. void add_icon(int type, char *icon, char *to, char *path, FILE *out) {
  118.     struct item *p;
  119.     char iconbak[MAX_STRING_LEN];
  120.  
  121.     strcpy(iconbak,icon);
  122.     if(icon[0] == '(') {
  123.         char alt[MAX_STRING_LEN];
  124.         getword(alt,iconbak,',');
  125.         add_alt(type,&alt[1],to,path,out);
  126.         iconbak[strlen(iconbak) - 1] = '\0';
  127.     }
  128.     if(type == BY_PATH) {
  129.         if(!strcmp(to,"**DIRECTORY**"))
  130.             strcpy(to,"^^DIRECTORY^^");
  131.     }
  132.     p = new_item(type,to,path,iconbak,out);
  133.     p->next = icon_list;
  134.     icon_list = p;
  135. }
  136.  
  137. void add_desc(int type, char *desc, char *to, char *path, FILE *out) {
  138.     struct item *p;
  139.  
  140.     p = new_item(type,to,path,desc,out);
  141.     p->next = desc_list;
  142.     desc_list = p;
  143. }
  144.  
  145. void add_ignore(char *ext, char *path, FILE *out) {
  146.     struct item *p;
  147.  
  148.     p = new_item(0,ext,path,NULL,out);
  149.     p->next = ign_list;
  150.     ign_list = p;
  151. }
  152.  
  153. void add_header(char *name, char *path, FILE *out) {
  154.     struct item *p;
  155.  
  156.     p = new_item(0,NULL,path,name,out);
  157.     p->next = hdr_list;
  158.     hdr_list = p;
  159. }
  160.  
  161. void add_readme(char *name, char *path, FILE *out) {
  162.     struct item *p;
  163.  
  164.     p = new_item(0,NULL,path,name,out);
  165.     p->next = rdme_list;
  166.     rdme_list = p;
  167. }
  168.  
  169.  
  170. void add_opts_int(int opts, char *path, FILE *out) {
  171.     struct item *p;
  172.  
  173.     p = new_item(0,NULL,path,NULL,out);
  174.     p->type = opts;
  175.     p->next = opts_list;
  176.     opts_list = p;
  177. }
  178.  
  179. void add_opts(char *optstr, char *path, FILE *out) {
  180.     char w[MAX_STRING_LEN];
  181.     int opts = 0;
  182.  
  183.     while(optstr[0]) {
  184.         cfg_getword(w,optstr);
  185.         if(!strcasecmp(w,"FancyIndexing"))
  186.             opts |= FANCY_INDEXING;
  187.         else if(!strcasecmp(w,"IconsAreLinks"))
  188.             opts |= ICONS_ARE_LINKS;
  189.         else if(!strcasecmp(w,"ScanHTMLTitles"))
  190.             opts |= SCAN_HTML_TITLES;
  191.         else if(!strcasecmp(w,"SuppressLastModified"))
  192.             opts |= SUPPRESS_LAST_MOD;
  193.         else if(!strcasecmp(w,"SuppressSize"))
  194.             opts |= SUPPRESS_SIZE;
  195.         else if(!strcasecmp(w,"SuppressDescription"))
  196.             opts |= SUPPRESS_DESC;
  197.         else if(!strcasecmp(w,"None"))
  198.             opts = 0;
  199.     }
  200.     add_opts_int(opts,path,out);
  201. }
  202.  
  203.  
  204. char *find_item(struct item *list, char *path, int path_only) {
  205.     struct item *p = list;
  206.     char *t;
  207.  
  208.     while(p) {
  209.         /* Special cased for ^^DIRECTORY^^ and ^^BLANKICON^^ */
  210.         if((path[0] == '^') || (!strcmp_match(path,p->apply_path))) {
  211.             if(!(p->apply_to))
  212.                 return p->data;
  213.             else if(p->type == BY_PATH) {
  214.                 if(!strcmp_match(path,p->apply_to))
  215.                     return p->data;
  216.             } else if(!path_only) {
  217.                 char pathbak[MAX_STRING_LEN];
  218.  
  219.                 strcpy(pathbak,path);
  220.                 content_encoding[0] = '\0';
  221.                 set_content_type(pathbak);
  222.                 if(!content_encoding[0]) {
  223.                     if(p->type == BY_TYPE) {
  224.                         if(!strcmp_match(content_type,p->apply_to))
  225.                             return p->data;
  226.                     }
  227.                 } else {
  228.                     if(p->type == BY_ENCODING) {
  229.                         if(!strcmp_match(content_encoding,p->apply_to))
  230.                             return p->data;
  231.                     }
  232.                 }
  233.             }
  234.         }
  235.         p = p->next;
  236.     }
  237.     return NULL;
  238. }
  239.  
  240. #define find_icon(p,t) find_item(icon_list,p,t)
  241. #define find_alt(p,t) find_item(alt_list,p,t)
  242. #define find_desc(p) find_item(desc_list,p,0)
  243. #define find_header(p) find_item(hdr_list,p,0)
  244. #define find_readme(p) find_item(rdme_list,p,0)
  245.  
  246.  
  247. int ignore_entry(char *path) {
  248.     struct item *p = ign_list;
  249.  
  250.     while(p) {
  251.         if(!strcmp_match(path,p->apply_path))
  252.             if(!strcmp_match(path,p->apply_to))
  253.                 return 1;
  254.         p = p->next;
  255.     }
  256.     return 0;
  257. }
  258.  
  259. int find_opts(char *path) {
  260.     struct item *p = opts_list;
  261.  
  262.     while(p) {
  263.         if(!strcmp_match(path,p->apply_path))
  264.             return p->type;
  265.         p = p->next;
  266.     }
  267.     return 0;
  268. }
  269.  
  270. int insert_readme(char *name, char *readme_fname, int rule, FILE *fd) {
  271.     char fn[MAX_STRING_LEN];
  272.     FILE *r;
  273.     struct stat finfo;
  274.     int plaintext=0;
  275.  
  276.     make_full_path(name,readme_fname,fn);
  277.     strcat(fn,".html");
  278.     if(stat(fn,&finfo) == -1) {
  279.         fn[strlen(fn)-5] = '\0';
  280.         if(stat(fn,&finfo) == -1)
  281.             return 0;
  282.         plaintext=1;
  283.         if(rule) bytes_sent += fprintf(fd,"<HR>%c",LF);
  284.         bytes_sent += fprintf(fd,"<PRE>%c",LF);
  285.     }
  286.     else if(rule) bytes_sent += fprintf(fd,"<HR>%c",LF);
  287.     if(!(r = fopen(fn,"r")))
  288.         return 0;
  289.     send_fd(r,fd,NULL);
  290.     fclose(r);
  291.     if(plaintext)
  292.         bytes_sent += fprintf(fd,"</PRE>%c",LF);
  293.     return 1;
  294. }
  295.  
  296.  
  297. char *find_title(char *filename) {
  298.     char titlebuf[MAX_STRING_LEN], *find = "<TITLE>";
  299.     char filebak[MAX_STRING_LEN];
  300.     FILE *thefile;
  301.     int x,y,n,p;
  302.  
  303.     content_encoding[0] = '\0';
  304.     strcpy(filebak,filename);
  305.     set_content_type(filebak);
  306.     if((!strcmp(content_type,"text/html")) && (!content_encoding[0])) {
  307.         if(!(thefile = fopen(filename,"r")))
  308.             return NULL;
  309.         n = fread(titlebuf,sizeof(char),MAX_STRING_LEN - 1,thefile);
  310.         titlebuf[n] = '\0';
  311.         for(x=0,p=0;titlebuf[x];x++) {
  312.             if(titlebuf[x] == find[p]) {
  313.                 if(!find[++p]) {
  314.                     if((p = ind(&titlebuf[++x],'<')) != -1)
  315.                         titlebuf[x+p] = '\0';
  316.                     /* Scan for line breaks for Tanmoy's secretary */
  317.                     for(y=x;titlebuf[y];y++)
  318.                         if((titlebuf[y] == CR) || (titlebuf[y] == LF))
  319.                             titlebuf[y] = ' ';
  320.                     return strdup(&titlebuf[x]);
  321.                 }
  322.             } else p=0;
  323.         }
  324.         return NULL;
  325.     }
  326.     content_encoding[0] = '\0';
  327.     return NULL;
  328. }
  329.  
  330.  
  331. void escape_html(char *fn) {
  332.     register int x,y;
  333.     char copy[MAX_STRING_LEN];
  334.  
  335.     strcpy(copy,fn);
  336.     for(x=0,y=0;copy[y];x++,y++) {
  337.         if(copy[y] == '<') {
  338.             strcpy(&fn[x],"<");
  339.             x+=3;
  340.         }
  341.         else if(copy[y] == '>') {
  342.             strcpy(&fn[x],">");
  343.             x+=3;
  344.         }
  345.         else if(copy[y] == '&') {
  346.             strcpy(&fn[x],"&");
  347.             x+=4;
  348.         }
  349.         else
  350.             fn[x] = copy[y];
  351.     }
  352.     fn[x] = '\0';
  353. }
  354.  
  355. struct ent *make_dir_entry(char *path, char *name, FILE *out) {
  356.     struct ent *p;
  357.     struct stat finfo;
  358.     char t[MAX_STRING_LEN], t2[MAX_STRING_LEN];
  359.  
  360.     if((name[0] == '.') && (!name[1]))
  361.         return(NULL);
  362.  
  363.     make_full_path(path,name,t);
  364.  
  365.     if(ignore_entry(t))
  366.         return(NULL);
  367.  
  368.     if(!(p=(struct ent *)malloc(sizeof(struct ent))))
  369.         die(NO_MEMORY,"make_dir_entry",out);
  370.     if(!(p->name=(char *)malloc(strlen(name) + 2)))
  371.         die(NO_MEMORY,"make_dir_entry",out);
  372.  
  373.     if(dir_opts & FANCY_INDEXING) {
  374.         if((!(dir_opts & FANCY_INDEXING)) || stat(t,&finfo) == -1) {
  375.             strcpy(p->name,name);
  376.             p->size = -1;
  377.             p->icon = NULL;
  378.             p->alt = NULL;
  379.             p->desc = NULL;
  380.             p->lm = -1;
  381.         }
  382.         else {
  383.             p->lm = finfo.st_mtime;
  384.             if(S_ISDIR(finfo.st_mode)) {
  385.                 if(!(p->icon = find_icon(t,1)))
  386.                     p->icon = find_icon("^^DIRECTORY^^",1);
  387.                 if(!(p->alt = find_alt(t,1)))
  388.                     p->alt = "DIR";
  389.                 p->size = -1;
  390.                 strcpy_dir(p->name,name);
  391.             }
  392.             else {
  393.                 p->icon = find_icon(t,0);
  394.                 p->alt = find_alt(t,0);
  395.                 p->size = finfo.st_size;
  396.                 strcpy(p->name,name);
  397.             }
  398.         }
  399.         if(p->desc = find_desc(t))
  400.             p->desc = strdup(p->desc);
  401.         if((!p->desc) && (dir_opts & SCAN_HTML_TITLES))
  402.             p->desc = find_title(t);
  403.     }
  404.     else {
  405.         p->icon = NULL;
  406.         p->alt = NULL;
  407.         p->desc = NULL;
  408.         p->size = -1;
  409.         p->lm = -1;
  410.         strcpy(p->name,name);
  411.     }
  412.     return(p);
  413. }
  414.  
  415.  
  416. void send_size(size_t size, FILE *fd) {
  417.     char schar;
  418.  
  419.     if(size == -1) {
  420.         fputs("    -",fd);
  421.         bytes_sent += 5;
  422.     }
  423.     else {
  424.         if(!size) {
  425.             fputs("   0K",fd);
  426.             bytes_sent += 5;
  427.         }
  428.         else if(size < 1024) {
  429.             fputs("   1K",fd);
  430.             bytes_sent += 5;
  431.         }
  432.         else if(size < 1048576)
  433.             bytes_sent += fprintf(fd,"%4dK",size / 1024);
  434.         else
  435.             bytes_sent += fprintf(fd,"%4dM",size / 1048576);
  436.     }
  437. }
  438.  
  439. void terminate_description(char *desc) {
  440.     int maxsize = 23;
  441.     register int x;
  442.     
  443.     if(dir_opts & SUPPRESS_LAST_MOD) maxsize += 17;
  444.     if(dir_opts & SUPPRESS_SIZE) maxsize += 7;
  445.  
  446.     for(x=0;desc[x] && maxsize;x++) {
  447.         if(desc[x] == '<') {
  448.             while(desc[x] != '>') {
  449.                 if(!desc[x]) {
  450.                     maxsize = 0;
  451.                     break;
  452.                 }
  453.                 ++x;
  454.             }
  455.         }
  456.         else --maxsize;
  457.     }
  458.     if(!maxsize) {
  459.         desc[x] = '>';
  460.         desc[x+1] = '\0';
  461.     }
  462.  
  463. }
  464.  
  465. void output_directories(struct ent **ar,int n,char *name,FILE *fd)
  466. {
  467.     int x,pad;
  468.     char anchor[HUGE_STRING_LEN],t[MAX_STRING_LEN],t2[MAX_STRING_LEN];
  469.     char *tp;
  470.  
  471.     if(name[0] == '\0') { 
  472.         name[0] = '/'; name[1] = '\0'; 
  473.     }
  474.     /* aaaaargh Solaris sucks. */
  475.     fflush(fd);
  476.  
  477.     if(dir_opts & FANCY_INDEXING) {
  478.         fputs("<PRE>",fd);
  479.         bytes_sent += 5;
  480.         if(tp = find_icon("^^BLANKICON^^",1))
  481.             bytes_sent += fprintf(fd,"<IMG SRC=\"%s\" ALT=\"     \"> ",tp);
  482.         bytes_sent += fprintf(fd,"Name                   ");
  483.         if(!(dir_opts & SUPPRESS_LAST_MOD))
  484.             bytes_sent += fprintf(fd,"Last modified     ");
  485.         if(!(dir_opts & SUPPRESS_SIZE))
  486.             bytes_sent += fprintf(fd,"Size  ");
  487.         if(!(dir_opts & SUPPRESS_DESC))
  488.             bytes_sent += fprintf(fd,"Description");
  489.         bytes_sent += fprintf(fd,"%c<HR>%c",LF,LF);
  490.     }
  491.     else {
  492.         fputs("<UL>",fd);
  493.         bytes_sent += 4;
  494.     }
  495.         
  496.     for(x=0;x<n;x++) {
  497.         if((!strcmp(ar[x]->name,"../")) || (!strcmp(ar[x]->name,".."))) {
  498.             make_full_path(name,"..",t);
  499.             getparents(t);
  500.             if(t[0] == '\0') {
  501.                 t[0] = '/'; t[1] = '\0';
  502.             }
  503.             escape_uri(t);
  504.             sprintf(anchor,"<A HREF=\"%s\">",t);
  505.             strcpy(t2,"Parent Directory</A>");
  506.         }
  507.         else {
  508.             strcpy(t,ar[x]->name);
  509.             strcpy(t2,t);
  510.             if(strlen(t2) > 21) {
  511.                 t2[21] = '>';
  512.                 t2[22] = '\0';
  513.             }
  514.             /* screws up formatting, but some need it. should fix. */
  515. /*            escape_html(t2); */
  516.             strcat(t2,"</A>");
  517.             escape_uri(t);
  518.             sprintf(anchor,"<A NAME=\"%s\" HREF=\"%s\">",t,t);
  519.         }
  520.         escape_url(t);
  521.  
  522.         if(dir_opts & FANCY_INDEXING) {
  523.             if(dir_opts & ICONS_ARE_LINKS)
  524.                 bytes_sent += fprintf(fd,"%s",anchor);
  525.             if((ar[x]->icon) || default_icon[0]) {
  526.                 bytes_sent += fprintf(fd,"<IMG SRC=\"%s\" ALT=\"[%s]\">",
  527.                                       ar[x]->icon ? ar[x]->icon : default_icon,
  528.                                       ar[x]->alt ? ar[x]->alt : "   ");
  529.             }
  530.             if(dir_opts & ICONS_ARE_LINKS) {
  531.                 fputs("</A>",fd);
  532.                 bytes_sent += 4;
  533.             }
  534.             bytes_sent += fprintf(fd," %s",anchor);
  535.             bytes_sent += fprintf(fd,"%-27.27s",t2);
  536.             if(!(dir_opts & SUPPRESS_LAST_MOD)) {
  537.                 if(ar[x]->lm != -1) {
  538.                     struct tm *ts = localtime(&ar[x]->lm);
  539.                     strftime(t,MAX_STRING_LEN,"%d-%b-%y %H:%M  ",ts);
  540.                     fputs(t,fd);
  541.                     bytes_sent += strlen(t);
  542.                 }
  543.                 else {
  544.                     fputs("                 ",fd);
  545.                     bytes_sent += 17;
  546.                 }
  547.             }
  548.             if(!(dir_opts & SUPPRESS_SIZE)) {
  549.                 send_size(ar[x]->size,fd);
  550.                 fputs("  ",fd);
  551.                 bytes_sent += 2;
  552.             }
  553.             if(!(dir_opts & SUPPRESS_DESC)) {
  554.                 if(ar[x]->desc) {
  555.                     terminate_description(ar[x]->desc);
  556.                     bytes_sent += fprintf(fd,"%s",ar[x]->desc);
  557.                 }
  558.             }
  559.         }
  560.         else
  561.             bytes_sent += fprintf(fd,"<LI> %s %s",anchor,t2);
  562.         fputc(LF,fd);
  563.         ++bytes_sent;
  564.     }
  565.     if(dir_opts & FANCY_INDEXING) {
  566.         fputs("</PRE>",fd);
  567.         bytes_sent += 6;
  568.     }
  569.     else {
  570.         fputs("</UL>",fd);
  571.         bytes_sent += 5;
  572.     }
  573. }
  574.  
  575.  
  576. int dsortf(struct ent **s1,struct ent **s2)
  577. {
  578.     return(strcmp((*s1)->name,(*s2)->name));
  579. }
  580.  
  581.     
  582. void index_directory(char *name, FILE *fd)
  583. {
  584.     DIR *d;
  585.     struct DIR_TYPE *dstruct;
  586.     int num_ent=0,x;
  587.     struct ent *head,*p,*q;
  588.     struct ent **ar;
  589.     char unmunged_name[MAX_STRING_LEN];
  590.     char *tmp;
  591.  
  592.     strcpy_dir(unmunged_name,name);
  593.     unmunge_name(unmunged_name);
  594.  
  595.     if(!(d=opendir(name)))
  596.         die(FORBIDDEN,unmunged_name,fd);
  597.  
  598.     strcpy(content_type,"text/html");
  599.     status = 200;
  600.     if(!assbackwards) 
  601.         send_http_header(fd);
  602.  
  603.     if(header_only) return;
  604.  
  605.     bytes_sent = 0;
  606.  
  607.     dir_opts = find_opts(name);
  608.  
  609. /* Spew HTML preamble */
  610.     bytes_sent += fprintf(fd,"<HEAD><TITLE>Index of %s</TITLE></HEAD><BODY>",
  611.                           unmunged_name);
  612.     fputc(LF,fd);
  613.     ++bytes_sent;
  614.  
  615.     if((!(tmp = find_header(name))) || (!(insert_readme(name,tmp,0,fd))))
  616.         bytes_sent += fprintf(fd,"<H1>Index of %s</H1>%c",unmunged_name,LF);
  617.  
  618. /* 
  619.  * Since we don't know how many dir. entries there are, put them into a 
  620.  * linked list and then arrayificate them so qsort can use them. 
  621.  */
  622.     head=NULL;
  623.     while(dstruct=readdir(d)) {
  624.         if(p = make_dir_entry(name,dstruct->d_name,fd)) {
  625.             p->next=head;
  626.             head=p;
  627.             num_ent++;
  628.         }
  629.     }
  630.     if(!(ar=(struct ent **) malloc(num_ent*sizeof(struct ent *))))
  631.         die(NO_MEMORY,"index_directory",fd);
  632.     p=head;
  633.     x=0;
  634.     while(p) {
  635.         ar[x++]=p;
  636.         p = p->next;
  637.     }
  638.     
  639.     qsort((void *)ar,num_ent,sizeof(struct ent *),
  640. #ifdef ULTRIX_BRAIN_DEATH
  641.           (int (*))dsortf);
  642. #else
  643.           (int (*)(const void *,const void *))dsortf);
  644. #endif
  645.      output_directories(ar,num_ent,unmunged_name,fd);
  646.      free(ar);
  647.      q = head;
  648.      while(q) {
  649.          p=q->next;
  650.          free(q->name);
  651.          if(q->desc)
  652.              free(q->desc);
  653.          free(q);
  654.          q=p;
  655.      }
  656.      closedir(d);
  657.      if(dir_opts & FANCY_INDEXING)
  658.          if(tmp = find_readme(name))
  659.              insert_readme(name,tmp,1,fd);
  660.      else {
  661.          fputs("</UL>",fd);
  662.          bytes_sent += 5;
  663.      }
  664.  
  665.      fputs("</BODY>",fd);
  666.      bytes_sent += 7;
  667.      log_transaction();
  668. }
  669.