08848d1ea0257fb9f2c48b8fe408d1ea3c0ccbe4
[citadel.git] / webcit / src / webserver.c
1 /*
2  * $Id$
3  */
4 /**
5  * \defgroup Webserver This contains a simple multithreaded TCP server manager.  It sits around
6  * waiting on the specified port for incoming HTTP connections.  When a
7  * connection is established, it calls context_loop() from context_loop.c.
8  * \ingroup WebcitHttpServer
9  */
10
11 /*@{*/
12 #include "webcit.h"
13 #include "webserver.h"
14
15 #if HAVE_BACKTRACE
16 #include <execinfo.h>
17 #endif
18
19 #ifndef HAVE_SNPRINTF
20 int vsnprintf(char *buf, size_t max, const char *fmt, va_list argp);
21 #endif
22
23 int verbosity = 9;              /**< Logging level */
24 int msock;                          /**< master listening socket */
25 int is_https = 0;               /**< Nonzero if I am an HTTPS service */
26 int follow_xff = 0;             /**< Follow X-Forwarded-For: header */
27 extern void *context_loop(int);
28 extern void *housekeeping_loop(void);
29 extern pthread_mutex_t SessionListMutex;
30 extern pthread_key_t MyConKey;
31
32
33 char *server_cookie = NULL; /**< our Cookie connection to the client */
34
35 int http_port = PORT_NUM;       /**< Port to listen on */
36
37 char *ctdlhost = DEFAULT_HOST; /**< our name */
38 char *ctdlport = DEFAULT_PORT; /**< our Port */
39 int setup_wizard = 0;          /**< should we run the setup wizard? \todo */
40 char wizard_filename[PATH_MAX];/**< where's the setup wizard? */
41
42 /** 
43  * \brief This is a generic function to set up a master socket for listening on
44  * a TCP port.  The server shuts down if the bind fails.
45  * \param ip_addr ip to bind to
46  * \param port_number the port to bind to 
47  * \param queue_len the size of the input queue ????
48  */
49 int ig_tcp_server(char *ip_addr, int port_number, int queue_len)
50 {
51         struct sockaddr_in sin;
52         int s, i;
53
54         memset(&sin, 0, sizeof(sin));
55         sin.sin_family = AF_INET;
56         if (ip_addr == NULL) {
57                 sin.sin_addr.s_addr = INADDR_ANY;
58         } else {
59                 sin.sin_addr.s_addr = inet_addr(ip_addr);
60         }
61
62         if (sin.sin_addr.s_addr == INADDR_NONE) {
63                 sin.sin_addr.s_addr = INADDR_ANY;
64         }
65
66         if (port_number == 0) {
67                 lprintf(1, "Cannot start: no port number specified.\n");
68                 exit(1);
69         }
70         sin.sin_port = htons((u_short) port_number);
71
72         s = socket(PF_INET, SOCK_STREAM, (getprotobyname("tcp")->p_proto));
73         if (s < 0) {
74                 lprintf(1, "Can't create a socket: %s\n", strerror(errno));
75                 exit(errno);
76         }
77         /** Set some socket options that make sense. */
78         i = 1;
79         setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &i, sizeof(i));
80
81         if (bind(s, (struct sockaddr *) &sin, sizeof(sin)) < 0) {
82                 lprintf(1, "Can't bind: %s\n", strerror(errno));
83                 exit(errno);
84         }
85         if (listen(s, queue_len) < 0) {
86                 lprintf(1, "Can't listen: %s\n", strerror(errno));
87                 exit(errno);
88         }
89         return (s);
90 }
91
92
93
94 /**
95  * \brief Create a Unix domain socket and listen on it
96  * \param sockpath file name of the unix domain socket
97  * \param queue_len queue size of the kernel fifo????
98  */
99 int ig_uds_server(char *sockpath, int queue_len)
100 {
101         struct sockaddr_un addr;
102         int s;
103         int i;
104         int actual_queue_len;
105
106         actual_queue_len = queue_len;
107         if (actual_queue_len < 5) actual_queue_len = 5;
108
109         i = unlink(sockpath);
110         if (i != 0) if (errno != ENOENT) {
111                 lprintf(1, "citserver: can't unlink %s: %s\n",
112                         sockpath, strerror(errno));
113                 exit(errno);
114         }
115
116         memset(&addr, 0, sizeof(addr));
117         addr.sun_family = AF_UNIX;
118         safestrncpy(addr.sun_path, sockpath, sizeof addr.sun_path);
119
120         s = socket(AF_UNIX, SOCK_STREAM, 0);
121         if (s < 0) {
122                 lprintf(1, "citserver: Can't create a socket: %s\n",
123                         strerror(errno));
124                 exit(errno);
125         }
126
127         if (bind(s, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
128                 lprintf(1, "citserver: Can't bind: %s\n",
129                         strerror(errno));
130                 exit(errno);
131         }
132
133         if (listen(s, actual_queue_len) < 0) {
134                 lprintf(1, "citserver: Can't listen: %s\n",
135                         strerror(errno));
136                 exit(errno);
137         }
138
139         chmod(sockpath, 0777);
140         return(s);
141 }
142
143
144
145
146 /**
147  * \brief Read data from the client socket.
148  * \param sock socket fd to read from ???
149  * \param buf buffer to read into 
150  * \param bytes how large is the read buffer?
151  * \param timeout how long should we wait for input?
152  * \return values are\
153  *      1       Requested number of bytes has been read.\
154  *      0       Request timed out.\
155  *         -1           Connection is broken, or other error.
156  */
157 int client_read_to(int sock, char *buf, int bytes, int timeout)
158 {
159         int len, rlen;
160         fd_set rfds;
161         struct timeval tv;
162         int retval;
163
164
165 #ifdef HAVE_OPENSSL
166         if (is_https) {
167                 return (client_read_ssl(buf, bytes, timeout));
168         }
169 #endif
170
171         len = 0;
172         while (len < bytes) {
173                 FD_ZERO(&rfds);
174                 FD_SET(sock, &rfds);
175                 tv.tv_sec = timeout;
176                 tv.tv_usec = 0;
177
178                 retval = select((sock) + 1, &rfds, NULL, NULL, &tv);
179                 if (FD_ISSET(sock, &rfds) == 0) {
180                         return (0);
181                 }
182
183                 rlen = read(sock, &buf[len], bytes - len);
184
185                 if (rlen < 1) {
186                         lprintf(2, "client_read() failed: %s\n",
187                                 strerror(errno));
188                         return (-1);
189                 }
190                 len = len + rlen;
191         }
192
193 #ifdef HTTP_TRACING
194         write(2, "\033[32m", 5);
195         write(2, buf, bytes);
196         write(2, "\033[30m", 5);
197 #endif
198         return (1);
199 }
200
201 /**
202  * \brief write data to the client
203  * \param buf data to write to the client
204  * \param count size of buffer
205  */
206 ssize_t client_write(const void *buf, size_t count)
207 {
208         char *newptr;
209         size_t newalloc;
210
211         if (WC->burst != NULL) {
212                 if ((WC->burst_len + count) >= WC->burst_alloc) {
213                         newalloc = (WC->burst_alloc * 2);
214                         if ((WC->burst_len + count) >= newalloc) {
215                                 newalloc += count;
216                         }
217                         newptr = realloc(WC->burst, newalloc);
218                         if (newptr != NULL) {
219                                 WC->burst = newptr;
220                                 WC->burst_alloc = newalloc;
221                         }
222                 }
223                 if ((WC->burst_len + count) < WC->burst_alloc) {
224                         memcpy(&WC->burst[WC->burst_len], buf, count);
225                         WC->burst_len += count;
226                         return (count);
227                 }
228                 else {
229                         return(-1);
230                 }
231         }
232 #ifdef HAVE_OPENSSL
233         if (is_https) {
234                 client_write_ssl((char *) buf, count);
235                 return (count);
236         }
237 #endif
238 #ifdef HTTP_TRACING
239         write(2, "\033[34m", 5);
240         write(2, buf, count);
241         write(2, "\033[30m", 5);
242 #endif
243         return (write(WC->http_sock, buf, count));
244 }
245
246 /**
247  * \brief what burst???
248  */
249 void begin_burst(void)
250 {
251         if (WC->burst != NULL) {
252                 free(WC->burst);
253                 WC->burst = NULL;
254         }
255         WC->burst_len = 0;
256         WC->burst_alloc = 32768;
257         WC->burst = malloc(WC->burst_alloc);
258 }
259
260
261 /**
262  * \brief uses the same calling syntax as compress2(), but it
263  * creates a stream compatible with HTTP "Content-encoding: gzip"
264  */
265 #ifdef HAVE_ZLIB
266 #define DEF_MEM_LEVEL 8 /**< memlevel??? */
267 #define OS_CODE 0x03    /**< unix */
268 int ZEXPORT compress_gzip(Bytef * dest,         /**< compressed buffer*/
269                                                   uLongf * destLen,     /**< length of the compresed data */
270                                                   const Bytef * source, /**< source to encode */
271                                                   uLong sourceLen,      /**< length of the source to encode */
272                                                   int level)            /**< what level??? */
273 {
274         const int gz_magic[2] = { 0x1f, 0x8b }; /** gzip magic header */
275
276         /** write gzip header */
277         sprintf((char *) dest, "%c%c%c%c%c%c%c%c%c%c",
278                 gz_magic[0], gz_magic[1], Z_DEFLATED,
279                 0 /*flags */ , 0, 0, 0, 0 /*time */ , 0 /** xflags */ ,
280                 OS_CODE);
281
282         /* normal deflate */
283         z_stream stream;
284         int err;
285         stream.next_in = (Bytef *) source;
286         stream.avail_in = (uInt) sourceLen;
287         stream.next_out = dest + 10L;   // after header
288         stream.avail_out = (uInt) * destLen;
289         if ((uLong) stream.avail_out != *destLen)
290                 return Z_BUF_ERROR;
291
292         stream.zalloc = (alloc_func) 0;
293         stream.zfree = (free_func) 0;
294         stream.opaque = (voidpf) 0;
295
296         err = deflateInit2(&stream, level, Z_DEFLATED, -MAX_WBITS,
297                            DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY);
298         if (err != Z_OK)
299                 return err;
300
301         err = deflate(&stream, Z_FINISH);
302         if (err != Z_STREAM_END) {
303                 deflateEnd(&stream);
304                 return err == Z_OK ? Z_BUF_ERROR : err;
305         }
306         *destLen = stream.total_out + 10L;
307
308         /* write CRC and Length */
309         uLong crc = crc32(0L, source, sourceLen);
310         int n;
311         for (n = 0; n < 4; ++n, ++*destLen) {
312                 dest[*destLen] = (int) (crc & 0xff);
313                 crc >>= 8;
314         }
315         uLong len = stream.total_in;
316         for (n = 0; n < 4; ++n, ++*destLen) {
317                 dest[*destLen] = (int) (len & 0xff);
318                 len >>= 8;
319         }
320         err = deflateEnd(&stream);
321         return err;
322 }
323 #endif
324
325 /**
326  * \brief what burst???
327  */
328 void end_burst(void)
329 {
330         size_t the_len;
331         char *the_data;
332
333         if (WC->burst == NULL)
334                 return;
335
336         the_len = WC->burst_len;
337         the_data = WC->burst;
338
339         WC->burst_len = 0;
340         WC->burst_alloc = 0;
341         WC->burst = NULL;
342
343 #ifdef HAVE_ZLIB
344         /* Handle gzip compression */
345         if (WC->gzip_ok) {
346                 char *compressed_data = NULL;
347                 uLongf compressed_len;
348
349                 compressed_len = (uLongf) ((the_len * 101) / 100) + 100;
350                 compressed_data = malloc(compressed_len);
351
352                 if (compress_gzip((Bytef *) compressed_data,
353                                   &compressed_len,
354                                   (Bytef *) the_data,
355                                   (uLongf) the_len, Z_BEST_SPEED) == Z_OK) {
356                         wprintf("Content-encoding: gzip\r\n");
357                         free(the_data);
358                         the_data = compressed_data;
359                         the_len = compressed_len;
360                 } else {
361                         free(compressed_data);
362                 }
363         }
364 #endif                          /* HAVE_ZLIB */
365
366         wprintf("Content-length: %d\r\n\r\n", the_len);
367         client_write(the_data, the_len);
368         free(the_data);
369         return;
370 }
371
372
373
374 /**
375  * \brief Read data from the client socket with default timeout.
376  * (This is implemented in terms of client_read_to() and could be
377  * justifiably moved out of sysdep.c)
378  * \param sock the socket fd to read from???
379  * \param buf the buffer to write to
380  * \param bytes how large is the buffer
381  */
382 int client_read(int sock, char *buf, int bytes)
383 {
384         return (client_read_to(sock, buf, bytes, SLEEPING));
385 }
386
387
388 /**
389  * \brief Get a LF-terminated line of text from the client.
390  * (This is implemented in terms of client_read() and could be
391  * justifiably moved out of sysdep.c)
392  * \param sock socket fd to get client line from???
393  * \param buf buffer to write read data to
394  * \param bufsiz how many bytes to read
395  * \return  numer of bytes read???
396  */
397 int client_getln(int sock, char *buf, int bufsiz)
398 {
399         int i, retval;
400
401         /** Read one character at a time.*/
402         for (i = 0;; i++) {
403                 retval = client_read(sock, &buf[i], 1);
404                 if (retval != 1 || buf[i] == '\n' || i == (bufsiz-1))
405                         break;
406                 if ( (!isspace(buf[i])) && (!isprint(buf[i])) ) {
407                         /** Non printable character recieved from client */
408                         return(-1);
409                 }
410         }
411
412         /** If we got a long line, discard characters until the newline. */
413         if (i == (bufsiz-1))
414                 while (buf[i] != '\n' && retval == 1)
415                         retval = client_read(sock, &buf[i], 1);
416
417         /**
418          * Strip any trailing non-printable characters.
419          */
420         buf[i] = 0;
421         while ((strlen(buf) > 0) && (!isprint(buf[strlen(buf) - 1]))) {
422                 buf[strlen(buf) - 1] = 0;
423         }
424         return (retval);
425 }
426
427
428 /**
429  * \brief       Start running as a daemon.  
430  *
431  * param        do_close_stdio          Only close stdio if set.
432  */
433 void start_daemon(int do_close_stdio)
434 {
435         if (do_close_stdio) {
436                 /* close(0); */
437                 close(1);
438                 close(2);
439         }
440         signal(SIGHUP, SIG_IGN);
441         signal(SIGINT, SIG_IGN);
442         signal(SIGQUIT, SIG_IGN);
443         if (fork() != 0) {
444                 exit(0);
445         }
446 }
447
448 /**
449  * \brief       Spawn an additional worker thread into the pool.
450  */
451 void spawn_another_worker_thread()
452 {
453         pthread_t SessThread;   /**< Thread descriptor */
454         pthread_attr_t attr;    /**< Thread attributes */
455         int ret;
456
457         lprintf(3, "Creating a new thread\n");
458
459         /** set attributes for the new thread */
460         pthread_attr_init(&attr);
461         pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
462
463         /**
464          * Our per-thread stacks need to be bigger than the default size, otherwise
465          * the MIME parser crashes on FreeBSD, and the IMAP service crashes on
466          * 64-bit Linux.
467          */
468         if ((ret = pthread_attr_setstacksize(&attr, 1024 * 1024))) {
469                 lprintf(1, "pthread_attr_setstacksize: %s\n",
470                         strerror(ret));
471                 pthread_attr_destroy(&attr);
472         }
473
474         /** now create the thread */
475         if (pthread_create(&SessThread, &attr,
476                            (void *(*)(void *)) worker_entry, NULL)
477             != 0) {
478                 lprintf(1, "Can't create thread: %s\n", strerror(errno));
479         }
480
481         /** free up the attributes */
482         pthread_attr_destroy(&attr);
483 }
484
485 /**
486  * \brief Here's where it all begins.
487  * \param argc number of commandline args
488  * \param argv the commandline arguments
489  */
490 int main(int argc, char **argv)
491 {
492         pthread_t SessThread;   /**< Thread descriptor */
493         pthread_attr_t attr;    /**< Thread attributes */
494         int a, i;                       /**< General-purpose variables */
495         char tracefile[PATH_MAX];
496         char ip_addr[256];
497         char *webcitdir = PREFIX;
498 #ifdef ENABLE_NLS
499         char *locale = NULL;
500         char *mo = NULL;
501 #endif /* ENABLE_NLS */
502         char uds_listen_path[PATH_MAX]; /**< listen on a unix domain socket? */
503
504         strcpy(uds_listen_path, "");
505
506         /** Parse command line */
507 #ifdef HAVE_OPENSSL
508         while ((a = getopt(argc, argv, "h:i:p:t:x:cfs")) != EOF)
509 #else
510         while ((a = getopt(argc, argv, "h:i:p:t:x:cf")) != EOF)
511 #endif
512                 switch (a) {
513                 case 'h':
514                         webcitdir = strdup(optarg);
515                         break;
516                 case 'i':
517                         safestrncpy(ip_addr, optarg, sizeof ip_addr);
518                         break;
519                 case 'p':
520                         http_port = atoi(optarg);
521                         if (http_port == 0) {
522                                 safestrncpy(uds_listen_path, optarg, sizeof uds_listen_path);
523                         }
524                         break;
525                 case 't':
526                         safestrncpy(tracefile, optarg, sizeof tracefile);
527                         freopen(tracefile, "w", stdout);
528                         freopen(tracefile, "w", stderr);
529                         freopen(tracefile, "r", stdin);
530                         break;
531                 case 'x':
532                         verbosity = atoi(optarg);
533                         break;
534                 case 'f':
535                         follow_xff = 1;
536                         break;
537                 case 'c':
538                         server_cookie = malloc(256);
539                         if (server_cookie != NULL) {
540                                 safestrncpy(server_cookie,
541                                        "Set-cookie: wcserver=",
542                                         256);
543                                 if (gethostname
544                                     (&server_cookie[strlen(server_cookie)],
545                                      200) != 0) {
546                                         lprintf(2, "gethostname: %s\n",
547                                                 strerror(errno));
548                                         free(server_cookie);
549                                 }
550                         }
551                         break;
552                 case 's':
553                         is_https = 1;
554                         break;
555                 default:
556                         fprintf(stderr, "usage: webserver "
557                                 "[-i ip_addr] [-p http_port] "
558                                 "[-t tracefile] [-c] [-f] "
559 #ifdef HAVE_OPENSSL
560                                 "[-s] "
561 #endif
562                                 "[remotehost [remoteport]]\n");
563                         return 1;
564                 }
565
566         if (optind < argc) {
567                 ctdlhost = argv[optind];
568                 if (++optind < argc)
569                         ctdlport = argv[optind];
570         }
571         /** Tell 'em who's in da house */
572         lprintf(1, SERVER "\n");
573         lprintf(1, "Copyright (C) 1996-2006 by the Citadel development team.\n"
574                 "This software is distributed under the terms of the "
575                 "GNU General Public License.\n\n"
576         );
577
578
579         /** initialize the International Bright Young Thing */
580 #ifdef ENABLE_NLS
581
582         initialize_locales();
583
584         locale = setlocale(LC_ALL, "");
585         
586         mo = malloc(strlen(webcitdir) + 20);
587         lprintf(9, "Message catalog directory: %s\n",
588                 bindtextdomain("webcit", PREFIX"/share/locale/")
589         );
590         free(mo);
591         lprintf(9, "Text domain: %s\n",
592                 textdomain("webcit")
593         );
594         lprintf(9, "Text domain Charset: %s\n",
595                         bind_textdomain_codeset("webcit","UTF8")
596         );
597 #endif
598
599         lprintf(9, "Changing directory to %s\n", webcitdir);
600         if (chdir(webcitdir) != 0) {
601                 perror("chdir");
602         }
603         initialize_viewdefs();
604         initialize_axdefs();
605
606         /**
607          * Set up a place to put thread-specific data.
608          * We only need a single pointer per thread - it points to the
609          * wcsession struct to which the thread is currently bound.
610          */
611         if (pthread_key_create(&MyConKey, NULL) != 0) {
612                 lprintf(1, "Can't create TSD key: %s\n", strerror(errno));
613         }
614
615         /**
616          * Set up a place to put thread-specific SSL data.
617          * We don't stick this in the wcsession struct because SSL starts
618          * up before the session is bound, and it gets torn down between
619          * transactions.
620          */
621 #ifdef HAVE_OPENSSL
622         if (pthread_key_create(&ThreadSSL, NULL) != 0) {
623                 lprintf(1, "Can't create TSD key: %s\n", strerror(errno));
624         }
625 #endif
626
627         /**
628          * Bind the server to our favorite port.
629          * There is no need to check for errors, because ig_tcp_server()
630          * exits if it doesn't succeed.
631          */
632
633         if (strlen(uds_listen_path) > 0) {
634                 lprintf(2, "Attempting to create listener socket at %s...\n", uds_listen_path);
635                 msock = ig_uds_server(uds_listen_path, LISTEN_QUEUE_LENGTH);
636         }
637         else {
638                 lprintf(2, "Attempting to bind to port %d...\n", http_port);
639                 msock = ig_tcp_server(ip_addr, http_port, LISTEN_QUEUE_LENGTH);
640         }
641
642         lprintf(2, "Listening on socket %d\n", msock);
643         signal(SIGPIPE, SIG_IGN);
644
645         pthread_mutex_init(&SessionListMutex, NULL);
646
647         /**
648          * Start up the housekeeping thread
649          */
650         pthread_attr_init(&attr);
651         pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
652         pthread_create(&SessThread, &attr,
653                        (void *(*)(void *)) housekeeping_loop, NULL);
654
655
656         /**
657          * If this is an HTTPS server, fire up SSL
658          */
659 #ifdef HAVE_OPENSSL
660         if (is_https) {
661                 init_ssl();
662         }
663 #endif
664
665         /** Start a few initial worker threads */
666         for (i = 0; i < (MIN_WORKER_THREADS); ++i) {
667                 spawn_another_worker_thread();
668         }
669
670         /* now the original thread becomes another worker */
671         worker_entry();
672         return 0;
673 }
674
675
676 /**
677  * Entry point for worker threads
678  */
679 void worker_entry(void)
680 {
681         int ssock;
682         int i = 0;
683         int time_to_die = 0;
684         int fail_this_transaction = 0;
685
686         do {
687                 /** Only one thread can accept at a time */
688                 fail_this_transaction = 0;
689                 ssock = accept(msock, NULL, 0);
690                 if (ssock < 0) {
691                         lprintf(2, "accept() failed: %s\n",
692                                 strerror(errno));
693                 } else {
694                         /** Set the SO_REUSEADDR socket option */
695                         i = 1;
696                         setsockopt(ssock, SOL_SOCKET, SO_REUSEADDR,
697                                    &i, sizeof(i));
698
699                         /** If we are an HTTPS server, go crypto now. */
700 #ifdef HAVE_OPENSSL
701                         if (is_https) {
702                                 if (starttls(ssock) != 0) {
703                                         fail_this_transaction = 1;
704                                         close(ssock);
705                                 }
706                         }
707 #endif
708
709                         if (fail_this_transaction == 0) {
710                                 /** Perform an HTTP transaction... */
711                                 context_loop(ssock);
712                                 /** ...and close the socket. */
713                                 lingering_close(ssock);
714                         }
715
716                 }
717
718         } while (!time_to_die);
719
720         pthread_exit(NULL);
721 }
722
723 /**
724  * \brief logprintf. log messages 
725  * logs to stderr if loglevel is lower than the verbosity set at startup
726  * \param loglevel level of the message
727  * \param format the printf like format string
728  * \param ... the strings to put into format
729  */
730 int lprintf(int loglevel, const char *format, ...)
731 {
732         va_list ap;
733
734         if (loglevel <= verbosity) {
735                 va_start(ap, format);
736                 vfprintf(stderr, format, ap);
737                 va_end(ap);
738                 fflush(stderr);
739         }
740         return 1;
741 }
742
743
744 /**
745  * \brief print the actual stack frame.
746  */
747 void wc_backtrace(void)
748 {
749 #ifdef HAVE_BACKTRACE
750         void *stack_frames[50];
751         size_t size, i;
752         char **strings;
753
754
755         size = backtrace(stack_frames, sizeof(stack_frames) / sizeof(void*));
756         strings = backtrace_symbols(stack_frames, size);
757         for (i = 0; i < size; i++) {
758                 if (strings != NULL)
759                         lprintf(1, "%s\n", strings[i]);
760                 else
761                         lprintf(1, "%p\n", stack_frames[i]);
762         }
763         free(strings);
764 #endif
765 }
766
767 /*@}*/