/[hydra]/hydra/src/get.c
ViewVC logotype

Annotation of /hydra/src/get.c

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1.20 - (hide annotations)
Wed Oct 2 12:43:58 2002 UTC (21 years, 6 months ago) by nmav
Branch: MAIN
CVS Tags: hydra_0_0_6
Changes since 1.19: +3 -2 lines
File MIME type: text/plain
some fixes

1 nmav 1.1 /*
2 nmav 1.13 * Hydra, an http server
3 nmav 1.1 * Copyright (C) 1995 Paul Phillips <paulp@go2net.com>
4     * Some changes Copyright (C) 1996,99 Larry Doolittle <ldoolitt@boa.org>
5     * Some changes Copyright (C) 1996-2002 Jon Nelson <jnelson@boa.org>
6 nmav 1.6 * Portions Copyright (C) 2002 Nikos Mavroyanopoulos <nmav@gnutls.org>
7 nmav 1.1 *
8     * This program is free software; you can redistribute it and/or modify
9     * it under the terms of the GNU General Public License as published by
10     * the Free Software Foundation; either version 1, or (at your option)
11     * any later version.
12     *
13     * This program is distributed in the hope that it will be useful,
14     * but WITHOUT ANY WARRANTY; without even the implied warranty of
15     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16     * GNU General Public License for more details.
17     *
18     * You should have received a copy of the GNU General Public License
19     * along with this program; if not, write to the Free Software
20     * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
21     *
22     */
23    
24 nmav 1.20 /* $Id: get.c,v 1.19 2002/10/01 22:38:24 nmav Exp $*/
25 nmav 1.1
26     #include "boa.h"
27     #include "socket.h"
28    
29     /* local prototypes */
30     int get_cachedir_file(request * req, struct stat *statbuf);
31     int index_directory(request * req, char *dest_filename);
32 nmav 1.17 static int check_if_stuff(request * req);
33 nmav 1.1
34     /*
35     * Name: init_get
36     * Description: Initializes a non-script GET or HEAD request.
37     *
38     * Return values:
39     * 0: finished or error, request will be freed
40     * 1: successfully initialized, added to ready queue
41     */
42    
43     int init_get(server_params * params, request * req)
44     {
45     int data_fd, saved_errno;
46     struct stat statbuf;
47     volatile int bytes;
48    
49     data_fd = open(req->pathname, O_RDONLY);
50     saved_errno = errno; /* might not get used */
51    
52     if (data_fd == -1) {
53     log_error_doc(req);
54     errno = saved_errno;
55     perror("document open");
56    
57     if (saved_errno == ENOENT)
58     send_r_not_found(req);
59     else if (saved_errno == EACCES)
60     send_r_forbidden(req);
61     else
62     send_r_bad_request(req);
63     return 0;
64     }
65    
66 nmav 1.17 if (fstat(data_fd, &statbuf) == -1) {
67 nmav 1.1 /* this is quite impossible, since the file
68     * was opened before.
69     */
70     close(data_fd);
71 nmav 1.5 send_r_not_found(req);
72 nmav 1.1 return 0;
73     }
74    
75     if (S_ISDIR(statbuf.st_mode)) { /* directory */
76     close(data_fd); /* close dir */
77    
78     if (req->pathname[strlen(req->pathname) - 1] != '/') {
79     char buffer[3 * MAX_PATH_LENGTH + 128];
80 nmav 1.7
81 nmav 1.17 create_url(buffer, sizeof(buffer), req->secure, req->hostname,
82     params->server_s[req->secure].port, req->request_uri);
83 nmav 1.1
84     send_r_moved_perm(req, buffer);
85     return 0;
86     }
87     data_fd = get_dir(req, &statbuf); /* updates statbuf */
88    
89     if (data_fd == -1) /* couldn't do it */
90     return 0; /* errors reported by get_dir */
91     else if (data_fd <= 1)
92     /* data_fd == 0 -> close it down, 1 -> continue */
93     return data_fd;
94     /* else, data_fd contains the fd of the file... */
95     }
96 nmav 1.5
97 nmav 1.1 req->filesize = statbuf.st_size;
98     req->last_modified = statbuf.st_mtime;
99 nmav 1.15
100     /* Check the If-Match, If-Modified etc stuff.
101     */
102 nmav 1.18 if (req->if_types)
103 nmav 1.17 if (check_if_stuff(req) == 0) {
104     close(data_fd);
105     return 0;
106     }
107 nmav 1.15 /* Move on */
108    
109 nmav 1.5 if (req->range_stop == 0)
110     req->range_stop = statbuf.st_size;
111    
112     /* out of range! */
113 nmav 1.17 if (req->range_start > statbuf.st_size ||
114     req->range_stop > statbuf.st_size ||
115     req->range_stop < req->range_start) {
116 nmav 1.5 send_r_range_unsatisfiable(req);
117     close(data_fd);
118     return 0;
119     }
120 nmav 1.1
121     if (req->method == M_HEAD || req->filesize == 0) {
122     send_r_request_ok(req);
123     close(data_fd);
124     return 0;
125     }
126    
127 nmav 1.9 if (req->range_stop > max_file_size_cache) {
128 nmav 1.5
129 nmav 1.17 if (req->range_start == 0 && req->range_stop == statbuf.st_size)
130     send_r_request_ok(req); /* All's well */
131 nmav 1.5 else {
132 nmav 1.17 /* if ranges were used, then lseek to the start given
133     */
134     if (lseek(data_fd, req->range_start, SEEK_SET) == (off_t) - 1) {
135     close(data_fd);
136     send_r_not_found(req);
137     return 0;
138     }
139     send_r_request_partial(req); /* All's well */
140 nmav 1.5 }
141    
142 nmav 1.1 req->status = PIPE_READ;
143     req->cgi_status = CGI_BUFFER;
144     req->data_fd = data_fd;
145     req_flush(req); /* this should *always* complete due to
146     the size of the I/O buffers */
147     req->header_line = req->header_end = req->buffer;
148 nmav 1.5 req->pipe_range_stop = req->range_stop;
149 nmav 1.1 return 1;
150     }
151    
152 nmav 1.5 if (req->range_stop == 0) { /* done */
153 nmav 1.1 send_r_request_ok(req); /* All's well *so far* */
154     close(data_fd);
155     return 1;
156     }
157    
158     /* NOTE: I (Jon Nelson) tried performing a read(2)
159     * into the output buffer provided the file data would
160     * fit, before mmapping, and if successful, writing that
161     * and stopping there -- all to avoid the cost
162     * of a mmap. Oddly, it was *slower* in benchmarks.
163     */
164 nmav 1.9 if (max_files_cache > 0) {
165     req->mmap_entry_var = find_mmap(data_fd, &statbuf);
166     if (req->mmap_entry_var == NULL) {
167 nmav 1.17 req->buffer_end = 0;
168     if (errno == ENOENT)
169     send_r_not_found(req);
170     else if (errno == EACCES)
171     send_r_forbidden(req);
172     else
173 nmav 1.9 send_r_bad_request(req);
174 nmav 1.17 close(data_fd);
175     return 0;
176 nmav 1.9 }
177     req->data_mem = req->mmap_entry_var->mmap;
178 nmav 1.17 } else { /* File caching is disabled.
179     */
180     req->data_mem =
181     mmap(0, req->range_stop, PROT_READ, MAP_OPTIONS, data_fd, 0);
182 nmav 1.1 }
183 nmav 1.9
184 nmav 1.1 close(data_fd); /* close data file */
185    
186 nmav 1.17 if (req->data_mem == MAP_FAILED) {
187 nmav 1.1 boa_perror(req, "mmap");
188     return 0;
189     }
190    
191 nmav 1.17 if (req->range_start == 0 && req->range_stop == statbuf.st_size)
192 nmav 1.5 send_r_request_ok(req); /* All's well */
193     else
194 nmav 1.17 send_r_request_partial(req); /* All's well */
195 nmav 1.1
196     bytes = BUFFER_SIZE - req->buffer_end;
197    
198     /* bytes is now how much the buffer can hold
199     * after the headers
200     */
201 nmav 1.5 req->filepos = req->range_start;
202 nmav 1.17
203 nmav 1.1 if (bytes > 0) {
204 nmav 1.5 if (bytes > req->range_stop - req->range_start)
205     bytes = req->range_stop - req->range_start;
206 nmav 1.1
207     if (setjmp(params->env) == 0) {
208     params->handle_sigbus = 1;
209 nmav 1.17 memcpy(req->buffer + req->buffer_end,
210     &req->data_mem[req->filepos], bytes);
211 nmav 1.1 params->handle_sigbus = 0;
212     /* OK, SIGBUS **after** this point is very bad! */
213     } else {
214 nmav 1.17 char buf[30];
215 nmav 1.1 /* sigbus! */
216     log_error_doc(req);
217     reset_output_buffer(req);
218     send_r_error(req);
219 nmav 1.17 get_commonlog_time(buf);
220     fprintf(stderr, "%sGot SIGBUS in memcpy!\n", buf);
221 nmav 1.1 return 0;
222     }
223     req->buffer_end += bytes;
224     req->filepos += bytes;
225 nmav 1.5 if (req->range_stop == req->filepos) {
226 nmav 1.1 req_flush(req);
227     req->status = DONE;
228     }
229     }
230    
231     /* We lose statbuf here, so make sure response has been sent */
232     return 1;
233     }
234    
235 nmav 1.17 /* Breaks a list of "xxx", "yyy", to a character array, of
236     * MAX_ETAGS size; Note that the given string is modified.
237     */
238     #define MAX_COMMA_SEP_ELEMENTS 6
239     inline
240 nmav 1.18 static void break_comma_list(char *etag,
241     char *broken_etag[MAX_COMMA_SEP_ELEMENTS],
242     int *elements)
243 nmav 1.17 {
244 nmav 1.20 char *p = etag;
245    
246 nmav 1.18 *elements = 0;
247 nmav 1.17
248 nmav 1.18 do {
249     broken_etag[*elements] = p;
250 nmav 1.17
251 nmav 1.18 (*elements)++;
252 nmav 1.17
253 nmav 1.18 p = strchr(p, ',');
254     if (p) {
255     *p = 0;
256     p++; /* move to next entry and skip white
257     * space.
258     */
259     while (*p == ' ')
260     p++;
261     }
262     } while (p != NULL && *elements < MAX_COMMA_SEP_ELEMENTS);
263 nmav 1.17 }
264    
265 nmav 1.15 /*
266     * Name: check_if_stuff
267     * Description: Checks the If-Match, If-None-Match headers
268     *
269     * req->last_modified, and req->filesize MUST have been set
270 nmav 1.17 * before calling this function. This function should be called
271 nmav 1.18 * if req->if_types != 0.
272 nmav 1.15 *
273     * Return values:
274     * 1: Successful, continue sending the file
275     * 0: unsuccessful. We send the appropriate stuff. Close the connection.
276     */
277    
278 nmav 1.17 static int check_if_stuff(request * req)
279 nmav 1.15 {
280 nmav 1.17 int comp = 0;
281 nmav 1.18 char *broken_etag[MAX_COMMA_SEP_ELEMENTS];
282     int broken_etag_size, i;
283     char new_etag[MAX_ETAG_LENGTH];
284    
285 nmav 1.17 /* Although we allow multiple If-* directives to be used, we
286     * actually use only one. The priority used is shown below.
287     */
288    
289     /* First try IF_MODIFIED_SINCE
290     */
291 nmav 1.18 if (req->if_types & IF_MODIFIED_SINCE) {
292 nmav 1.17 if (!modified_since(req->last_modified, req->if_modified_since)) {
293     send_r_not_modified(req);
294     return 0;
295     }
296     return 1;
297     }
298    
299 nmav 1.18
300     /* Then try IF_MATCH
301 nmav 1.17 */
302 nmav 1.18 if (req->if_types & IF_MATCH) {
303 nmav 1.17
304     /* Check for the "*"
305     */
306 nmav 1.18 if (strncmp(req->if_match_etag, "\"*\"", 3) == 0) {
307 nmav 1.17 comp = 0; /* comparison is always ok */
308     } else {
309    
310 nmav 1.18 /* Create the current ETag of the file.
311 nmav 1.17 */
312     create_etag(req->filesize, req->last_modified, new_etag);
313    
314     /* Check if one of the ETags sent, match ours
315     */
316 nmav 1.18 break_comma_list(req->if_match_etag, broken_etag,
317     &broken_etag_size);
318 nmav 1.17
319 nmav 1.18 comp = 1;
320     for (i = 0; i < broken_etag_size; i++) {
321     comp = strcmp(broken_etag[i], new_etag);
322     if (comp == 0) /* matches! */
323     break;
324     }
325 nmav 1.17
326 nmav 1.15
327     }
328    
329 nmav 1.17 if (comp == 0)
330     return 1;
331     send_r_precondition_failed(req);
332     return 0;
333     }
334 nmav 1.15
335 nmav 1.17 /* Then try IF_RANGE
336     */
337 nmav 1.18 if (req->if_types & IF_RANGE) {
338     if (req->if_range_etag[0] == '"') { /* ETag may contain a date, if If-Range
339     * was used.
340     */
341     /* Check for the "*"
342     */
343     if (strncmp(req->if_range_etag, "\"*\"", 3) == 0) {
344     comp = 0; /* comparison is always ok */
345     } else {
346    
347     /* Create the current ETag
348     */
349     create_etag(req->filesize, req->last_modified, new_etag);
350    
351     /* Check if one of the ETags sent, match ours
352     */
353    
354     break_comma_list(req->if_range_etag, broken_etag,
355     &broken_etag_size);
356    
357     comp = 1;
358     for (i = 0; i < broken_etag_size; i++) {
359     comp = strcmp(broken_etag[i], new_etag);
360     if (comp == 0) /* matches! */
361     break;
362     }
363    
364    
365 nmav 1.17 }
366 nmav 1.18 } else {
367     comp = modified_since(req->last_modified, req->if_range_etag);
368 nmav 1.17 }
369    
370 nmav 1.18
371     /* File didn't change */
372 nmav 1.17 if (comp == 0)
373     return 1;
374 nmav 1.15
375 nmav 1.18 /* File has been changed, but it is Ok, so send the whole
376     * file.
377 nmav 1.17 */
378     req->range_start = req->range_stop = 0;
379     return 1;
380     }
381    
382     /* Then try IF_NONE_MATCH
383     */
384 nmav 1.18 if (req->if_types & IF_NONE_MATCH) {
385     /* Check for the "*"
386     */
387     if (strncmp(req->if_none_match_etag, "\"*\"", 3) == 0) {
388     comp = 0; /* comparison is always ok */
389     } else {
390    
391     /* Create the current ETag
392     */
393     create_etag(req->filesize, req->last_modified, new_etag);
394    
395     /* Check if one of the ETags sent, match ours
396     */
397    
398     break_comma_list(req->if_none_match_etag, broken_etag,
399     &broken_etag_size);
400    
401     comp = 1;
402     for (i = 0; i < broken_etag_size; i++) {
403     comp = strcmp(broken_etag[i], new_etag);
404     if (comp == 0) /* matches! */
405     break;
406     }
407    
408    
409     }
410    
411 nmav 1.17 if (comp == 0) {
412     send_r_not_modified(req);
413     return 0;
414     } else { /* it was modified */
415     send_r_precondition_failed(req);
416     return 0;
417     }
418     }
419    
420     /* Unsupported type ? */
421    
422     return 1; /* do the request */
423 nmav 1.15 }
424 nmav 1.1
425     /*
426     * Name: process_get
427     * Description: Writes a chunk of data to the socket.
428     *
429     * Return values:
430     * -1: request blocked, move to blocked queue
431     * 0: EOF or error, close it down
432     * 1: successful write, recycle in ready queue
433     */
434    
435     int process_get(server_params * params, request * req)
436     {
437     int bytes_written;
438     volatile int bytes_to_write;
439    
440 nmav 1.5 bytes_to_write = req->range_stop - req->filepos;
441 nmav 1.1 if (bytes_to_write > SOCKETBUF_SIZE)
442     bytes_to_write = SOCKETBUF_SIZE;
443    
444    
445     if (setjmp(params->env) == 0) {
446     params->handle_sigbus = 1;
447    
448     bytes_written =
449     socket_send(req, req->data_mem + req->filepos, bytes_to_write);
450    
451     params->handle_sigbus = 0;
452     /* OK, SIGBUS **after** this point is very bad! */
453     } else {
454 nmav 1.4 char buf[30];
455 nmav 1.1 /* sigbus! */
456     log_error_doc(req);
457     /* sending an error here is inappropriate
458     * if we are here, the file is mmapped, and thus,
459     * a content-length has been sent. If we send fewer bytes
460     * the client knows there has been a problem.
461     * We run the risk of accidentally sending the right number
462     * of bytes (or a few too many) and the client
463     * won't be the wiser.
464     */
465     req->status = DEAD;
466 nmav 1.17 get_commonlog_time(buf);
467 nmav 1.4 fprintf(stderr, "%sGot SIGBUS in write(2)!\n", buf);
468 nmav 1.1 return 0;
469     }
470    
471     if (bytes_written < 0) {
472     if (errno == EWOULDBLOCK || errno == EAGAIN)
473     return -1;
474     /* request blocked at the pipe level, but keep going */
475     else {
476     if (errno != EPIPE) {
477     log_error_doc(req);
478     /* Can generate lots of log entries, */
479     perror("write");
480     /* OK to disable if your logs get too big */
481     }
482     req->status = DEAD;
483     return 0;
484     }
485     }
486     req->filepos += bytes_written;
487    
488 nmav 1.5 if (req->filepos == req->range_stop) { /* EOF */
489 nmav 1.1 return 0;
490     } else
491     return 1; /* more to do */
492     }
493    
494     /*
495     * Name: get_dir
496     * Description: Called from process_get if the request is a directory.
497     * statbuf must describe directory on input, since we may need its
498     * device, inode, and mtime.
499     * statbuf is updated, since we may need to check mtimes of a cache.
500     * returns:
501     * -1 error
502     * 0 cgi (either gunzip or auto-generated)
503     * >0 file descriptor of file
504     */
505    
506     int get_dir(request * req, struct stat *statbuf)
507     {
508    
509 nmav 1.17 char *directory_index;
510 nmav 1.1 int data_fd;
511    
512 nmav 1.17 directory_index =
513     find_and_open_directory_index(req->pathname, 0, &data_fd);
514 nmav 1.10
515 nmav 1.1 if (directory_index) { /* look for index.html first?? */
516     if (data_fd != -1) { /* user's index file */
517 nmav 1.17 int ret;
518    
519 nmav 1.12 /* Check if we can execute the file
520     */
521    
522 nmav 1.17 strcat(req->request_uri, directory_index);
523    
524     ret = is_executable_cgi(req, directory_index);
525     if (ret != 0) { /* it is a CGI */
526     close(data_fd); /* we don't need it */
527     if (ret == -1) {
528 nmav 1.19 send_r_not_found(req);
529 nmav 1.17 return -1;
530     }
531     return init_cgi(req);
532     }
533 nmav 1.12
534 nmav 1.17 /* Not a cgi */
535 nmav 1.12
536 nmav 1.1 fstat(data_fd, statbuf);
537     return data_fd;
538     }
539     if (errno == EACCES) {
540     send_r_forbidden(req);
541     return -1;
542     } else if (errno != ENOENT) {
543     /* if there is an error *other* than EACCES or ENOENT */
544     send_r_not_found(req);
545     return -1;
546     }
547     }
548    
549     /* only here if index.html, index.html.gz don't exist */
550     if (dirmaker != NULL) { /* don't look for index.html... maybe automake? */
551     req->response_status = R_REQUEST_OK;
552     SQUASH_KA(req);
553    
554     /* the indexer should take care of all headers */
555     if (!req->simple) {
556 nmav 1.17 req_write(req, HTTP_VERSION " 200 OK\r\n");
557 nmav 1.1 print_http_headers(req);
558     print_last_modified(req);
559 nmav 1.11 req_write(req, "Content-Type: " TEXT_HTML CRLF CRLF);
560 nmav 1.1 req_flush(req);
561     }
562     if (req->method == M_HEAD)
563     return 0;
564    
565 nmav 1.7 req->is_cgi = INDEXER_CGI;
566 nmav 1.1 return init_cgi(req);
567     /* in this case, 0 means success */
568     } else if (cachedir) {
569     return get_cachedir_file(req, statbuf);
570     } else { /* neither index.html nor autogenerate are allowed */
571     send_r_forbidden(req);
572     return -1; /* nothing worked */
573     }
574     }
575    
576     int get_cachedir_file(request * req, struct stat *statbuf)
577     {
578    
579     char pathname_with_index[MAX_PATH_LENGTH];
580     int data_fd;
581     time_t real_dir_mtime;
582    
583     real_dir_mtime = statbuf->st_mtime;
584     sprintf(pathname_with_index, "%s/dir.%d.%ld",
585     cachedir, (int) statbuf->st_dev, statbuf->st_ino);
586     data_fd = open(pathname_with_index, O_RDONLY);
587    
588     if (data_fd != -1) { /* index cache */
589    
590     fstat(data_fd, statbuf);
591     if (statbuf->st_mtime > real_dir_mtime) {
592     statbuf->st_mtime = real_dir_mtime; /* lie */
593 nmav 1.10 strcpy(req->request_uri, find_default_directory_index()); /* for mimetype */
594 nmav 1.1 return data_fd;
595     }
596     close(data_fd);
597     unlink(pathname_with_index); /* cache is stale, delete it */
598     }
599     if (index_directory(req, pathname_with_index) == -1)
600     return -1;
601    
602     data_fd = open(pathname_with_index, O_RDONLY); /* Last chance */
603     if (data_fd != -1) {
604 nmav 1.10 strcpy(req->request_uri, find_default_directory_index()); /* for mimetype */
605 nmav 1.1 fstat(data_fd, statbuf);
606     statbuf->st_mtime = real_dir_mtime; /* lie */
607     return data_fd;
608     }
609    
610     boa_perror(req, "re-opening dircache");
611     return -1; /* Nothing worked. */
612    
613     }
614    
615     /*
616     * Name: index_directory
617     * Description: Called from get_cachedir_file if a directory html
618     * has to be generated on the fly
619     * returns -1 for problem, else 0
620     * This version is the fastest, ugliest, and most accurate yet.
621     * It solves the "stale size or type" problem by not ever giving
622     * the size or type. This also speeds it up since no per-file
623     * stat() is required.
624     */
625    
626     int index_directory(request * req, char *dest_filename)
627     {
628     DIR *request_dir;
629     FILE *fdstream;
630     struct dirent *dirbuf;
631     int bytes = 0;
632     char *escname = NULL;
633    
634     if (chdir(req->pathname) == -1) {
635     if (errno == EACCES || errno == EPERM) {
636     send_r_forbidden(req);
637     } else {
638     log_error_doc(req);
639     perror("chdir");
640     send_r_bad_request(req);
641     }
642     return -1;
643     }
644    
645     request_dir = opendir(".");
646     if (request_dir == NULL) {
647     int errno_save = errno;
648     send_r_error(req);
649     log_error_time();
650     fprintf(stderr, "directory \"%s\": ", req->pathname);
651     errno = errno_save;
652     perror("opendir");
653     return -1;
654     }
655    
656     fdstream = fopen(dest_filename, "w");
657     if (fdstream == NULL) {
658     boa_perror(req, "dircache fopen");
659     closedir(request_dir);
660     return -1;
661     }
662    
663     bytes += fprintf(fdstream,
664     "<HTML><HEAD>\n<TITLE>Index of %s</TITLE>\n</HEAD>\n\n",
665     req->request_uri);
666     bytes +=
667     fprintf(fdstream, "<BODY>\n\n<H2>Index of %s</H2>\n\n<PRE>\n",
668     req->request_uri);
669    
670     while ((dirbuf = readdir(request_dir))) {
671     if (!strcmp(dirbuf->d_name, "."))
672     continue;
673    
674     if (!strcmp(dirbuf->d_name, "..")) {
675     bytes += fprintf(fdstream,
676     " [DIR] <A HREF=\"../\">Parent Directory</A>\n");
677     continue;
678     }
679    
680     if ((escname = escape_string(dirbuf->d_name, NULL)) != NULL) {
681     bytes += fprintf(fdstream, " <A HREF=\"%s\">%s</A>\n",
682     escname, dirbuf->d_name);
683     free(escname);
684     escname = NULL;
685     }
686     }
687     closedir(request_dir);
688     bytes += fprintf(fdstream, "</PRE>\n\n</BODY>\n</HTML>\n");
689    
690     fclose(fdstream);
691    
692     chdir(server_root);
693    
694     req->filesize = bytes; /* for logging transfer size */
695     return 0; /* success */
696     }

webmaster@linux.gr
ViewVC Help
Powered by ViewVC 1.1.26