Annotation of libwww/Library/src/HTSocket.c, revision 2.16
2.1 frystyk 1: /* HTSocket.c
2: ** MANAGES READ AND WRITE TO AND FROM THE NETWORK
3: **
4: ** (c) COPYRIGHT MIT 1995.
5: ** Please first read the full copyright statement in the file COPYRIGH.
6: **
7: **
8: ** HISTORY:
9: ** 6 June 95 HFN Spawned off from HTFormat
10: */
11:
12: /* Library Include files */
13: #include "tcp.h"
14: #include "HTUtils.h"
15: #include "HTString.h"
2.7 frystyk 16: #include "HTReqMan.h"
2.4 frystyk 17: #include "HTProt.h"
2.1 frystyk 18: #include "HTTCP.h"
19: #include "HTStream.h"
2.6 frystyk 20: #include "HTAlert.h"
2.1 frystyk 21: #include "HTFormat.h"
2.8 frystyk 22: #include "HTNetMan.h"
2.1 frystyk 23: #include "HTError.h"
24: #include "HTSocket.h" /* Implemented here */
25:
2.2 frystyk 26: struct _HTInputSocket {
2.7 frystyk 27: char buffer[INPUT_BUFFER_SIZE];
28: char * write; /* Last byte written */
29: char * read; /* Last byte read */
30: SOCKFD sockfd;
2.2 frystyk 31: };
32:
2.1 frystyk 33: struct _HTStream {
34: CONST HTStreamClass * isa;
35: };
36:
37: /* ------------------------------------------------------------------------- */
38: /* SOCKET INPUT BUFFERING */
39: /* ------------------------------------------------------------------------- */
40: /*
41: ** This code is used because one cannot in general open a
42: ** file descriptor for a socket.
43: **
44: ** The input file is read using the macro which can read from
45: ** a socket or a file, but this should not be used for files
46: ** as fopen() etc is more portable of course.
47: **
48: ** The input buffer size, if large will give greater efficiency and
49: ** release the server faster, and if small will save space on PCs etc.
50: */
51:
52:
53: /* Set up the buffering
54: **
55: ** These routines are public because they are in fact needed by
56: ** many parsers, and on PCs and Macs we should not duplicate
57: ** the static buffer area.
58: */
2.12 frystyk 59: PUBLIC HTInputSocket * HTInputSocket_new (SOCKFD file_number)
2.1 frystyk 60: {
61: HTInputSocket *isoc = (HTInputSocket *)calloc(1, sizeof(*isoc));
62: if (!isoc) outofmem(__FILE__, "HTInputSocket_new");
2.7 frystyk 63: isoc->sockfd = file_number;
64: isoc->write = isoc->read = isoc->buffer;
2.1 frystyk 65: return isoc;
66: }
67:
2.12 frystyk 68: PUBLIC void HTInputSocket_free (HTInputSocket * me)
69: {
70: if (me) free(me);
71: }
72:
73: /* ------------------------------------------------------------------------- */
74:
2.15 frystyk 75: #if 0
76:
2.1 frystyk 77: /* This should return HT_INTERRUPTED if interrupted BUT the connection
78: MUST not be closed */
2.16 ! frystyk 79: PRIVATE int HTInputSocket_getCharacter (HTInputSocket* isoc)
2.1 frystyk 80: {
81: int ch;
82: do {
2.7 frystyk 83: if (isoc->write >= isoc->read) {
84: int status = NETREAD(isoc->sockfd, isoc->buffer,INPUT_BUFFER_SIZE);
2.1 frystyk 85: if (status <= 0) {
86: if (status == 0)
87: return EOF;
88: if (PROT_TRACE)
2.14 frystyk 89: TTYPrint(TDEST, "Read Socket. READ ERROR %d\n", socerrno);
2.1 frystyk 90: return EOF; /* -1 is returned by UCX at end of HTTP link */
91: }
2.7 frystyk 92: isoc->write = isoc->buffer;
93: isoc->read = isoc->buffer + status;
2.1 frystyk 94: }
2.7 frystyk 95: ch = (unsigned char) *isoc->write++;
2.1 frystyk 96: } while (ch == 13); /* Ignore ASCII carriage return */
97:
98: return FROMASCII(ch);
99: }
100:
2.16 ! frystyk 101: PRIVATE char * HTInputSocket_getBlock (HTInputSocket* isoc,
! 102: int * len)
2.1 frystyk 103: {
2.7 frystyk 104: if (isoc->write >= isoc->read) {
105: int status = NETREAD(isoc->sockfd,
106: isoc->buffer,
2.1 frystyk 107: ((*len < INPUT_BUFFER_SIZE) ?
108: *len : INPUT_BUFFER_SIZE));
109: if (status <= 0) {
2.7 frystyk 110: isoc->read = isoc->buffer;
2.1 frystyk 111: if (status < 0) {
112: if (PROT_TRACE)
2.14 frystyk 113: TTYPrint(TDEST, "Read Socket. READ ERROR %d\n", socerrno);
2.1 frystyk 114: }
115: *len = 0;
116: return NULL;
117: }
118: else {
119: *len = status;
2.7 frystyk 120: return isoc->buffer;
2.1 frystyk 121: }
122: }
123: else {
2.7 frystyk 124: char * ret = isoc->write;
125: *len = isoc->read - isoc->write;
126: isoc->write = isoc->read;
2.1 frystyk 127: return ret;
128: }
129: }
130:
131:
2.16 ! frystyk 132: PRIVATE int fill_in_buffer (HTInputSocket * isoc)
2.1 frystyk 133: {
134: if (isoc) {
135: int status;
136:
2.7 frystyk 137: isoc->write = isoc->buffer;
138: status = NETREAD(isoc->sockfd,
139: isoc->buffer,
2.1 frystyk 140: INPUT_BUFFER_SIZE);
141: if (status <= 0) {
2.7 frystyk 142: isoc->read = isoc->buffer;
2.1 frystyk 143: if (status < 0) {
144: if (PROT_TRACE)
2.14 frystyk 145: TTYPrint(TDEST, "Read Socket. READ ERROR %d\n", socerrno);
2.1 frystyk 146: }
147: }
148: else
2.7 frystyk 149: isoc->read = isoc->buffer + status;
2.1 frystyk 150: return status;
151: }
152: return -1;
153: }
154:
155:
2.16 ! frystyk 156: PRIVATE void ascii_cat (char ** linep,
! 157: char * start,
! 158: char * end)
2.1 frystyk 159: {
160: if (linep && start && end && start <= end) {
161: char *ptr;
162:
163: if (*linep) {
164: int len = strlen(*linep);
165: *linep = (char*)realloc(*linep, len + end-start + 1);
166: ptr = *linep + len;
167: }
168: else {
169: ptr = *linep = (char*)malloc(end-start + 1);
170: }
171:
172: while (start < end) {
173: *ptr = FROMASCII(*start);
174: ptr++;
175: start++;
176: }
177: *ptr = 0;
178: }
179: }
180:
181:
2.16 ! frystyk 182: PRIVATE char * get_some_line (HTInputSocket * isoc,
! 183: BOOL unfold)
2.1 frystyk 184: {
185: if (!isoc)
186: return NULL;
187: else {
188: BOOL check_unfold = NO;
189: int prev_cr = 0;
2.7 frystyk 190: char *start = isoc->write;
191: char *cur = isoc->write;
2.1 frystyk 192: char * line = NULL;
193:
194: for(;;) {
195: /*
196: ** Get more if needed to complete line
197: */
2.7 frystyk 198: if (cur >= isoc->read) { /* Need more data */
2.1 frystyk 199: ascii_cat(&line, start, cur);
200: if (fill_in_buffer(isoc) <= 0)
201: return line;
2.7 frystyk 202: start = cur = isoc->write;
2.1 frystyk 203: } /* if need more data */
204:
205: /*
206: ** Find a line feed if there is one
207: */
2.7 frystyk 208: for(; cur < isoc->read; cur++) {
2.1 frystyk 209: char c = FROMASCII(*cur);
210: if (!c) {
211: if (line) free(line); /* Leak fixed AL 6 Feb 94 */
212: return NULL; /* Panic! read a 0! */
213: }
214: if (check_unfold && c != ' ' && c != '\t') {
2.7 frystyk 215: return line; /* Note: didn't update isoc->write */
2.1 frystyk 216: }
217: else {
218: check_unfold = NO;
219: }
220:
221: if (c=='\r') {
222: prev_cr = 1;
223: }
224: else {
225: if (c=='\n') { /* Found a line feed */
226: ascii_cat(&line, start, cur-prev_cr);
2.7 frystyk 227: start = isoc->write = cur+1;
2.1 frystyk 228:
229: if (line && (int) strlen(line) > 0 && unfold) {
230: check_unfold = YES;
231: }
232: else {
233: return line;
234: }
235: } /* if NL */
236: /* else just a regular character */
237: prev_cr = 0;
238: } /* if not CR */
239: } /* while characters in buffer remain */
240: } /* until line read or end-of-file */
241: } /* valid parameters to function */
242: }
243:
244: /* The returned string must be freed by the caller */
2.16 ! frystyk 245: PRIVATE char * HTInputSocket_getLine (HTInputSocket * isoc)
2.1 frystyk 246: {
247: return get_some_line(isoc, NO);
248: }
249:
250: /* The returned string must be freed by the caller */
2.16 ! frystyk 251: PRIVATE char * HTInputSocket_getUnfoldedLine (HTInputSocket * isoc)
2.1 frystyk 252: {
253: return get_some_line(isoc, YES);
254: }
255:
2.15 frystyk 256: #endif
2.1 frystyk 257:
258: /* Push data from a socket down a stream
259: ** -------------------------------------
260: **
261: ** This routine is responsible for creating and PRESENTING any
262: ** graphic (or other) objects described by the file.
263: **
264: ** The file number given is assumed to be a TELNET stream ie containing
265: ** CRLF at the end of lines which need to be stripped to LF for unix
266: ** when the format is textual.
267: **
268: ** RETURNS the number of bytes transferred.
269: **
270: */
2.16 ! frystyk 271: PRIVATE int HTCopy (
! 272: SOCKFD file_number,
! 273: HTStream* sink)
2.1 frystyk 274: {
275: HTStreamClass targetClass;
276: HTInputSocket * isoc;
277: int cnt = 0;
278:
279: /* Push the data down the stream
280: **
281: */
282: targetClass = *(sink->isa); /* Copy pointers to procedures */
283: isoc = HTInputSocket_new(file_number);
284:
285: /* Push binary from socket down sink
286: **
287: ** This operation could be put into a main event loop
288: */
289: for(;;) {
290: int status = NETREAD(
2.7 frystyk 291: file_number, isoc->buffer, INPUT_BUFFER_SIZE);
2.1 frystyk 292: if (status <= 0) {
293: if (status == 0) break;
2.14 frystyk 294: if (WWWTRACE) TTYPrint(TDEST,
2.1 frystyk 295: "Socket Copy. Read error, read returns %d with errno=%d\n",
296: status, socerrno);
2.15 frystyk 297: break;
2.1 frystyk 298: }
299:
300: #ifdef NOT_ASCII
301: {
302: char * p;
2.7 frystyk 303: for(p = isoc->buffer; p < isoc->buffer+status; p++) {
2.1 frystyk 304: *p = FROMASCII(*p);
305: }
306: }
307: #endif
308:
2.7 frystyk 309: (*targetClass.put_block)(sink, isoc->buffer, status);
2.1 frystyk 310: cnt += status;
311: } /* next bufferload */
312:
313: HTInputSocket_free(isoc);
314:
315: return cnt;
316: }
317:
2.13 frystyk 318: #if 0
2.1 frystyk 319:
320: /* Push data from a socket down a stream STRIPPING CR
321: ** --------------------------------------------------
322: **
323: ** This routine is responsible for creating and PRESENTING any
324: ** graphic (or other) objects described by the socket.
325: **
326: ** The file number given is assumed to be a TELNET stream ie containing
327: ** CRLF at the end of lines which need to be stripped to LF for unix
328: ** when the format is textual.
329: **
330: ** Character handling is now of type int, Henrik, May 09-94
331: */
2.16 ! frystyk 332: PRIVATE void HTCopyNoCR (
! 333: SOCKFD file_number,
! 334: HTStream* sink)
2.1 frystyk 335: {
336: HTStreamClass targetClass;
337: HTInputSocket * isoc;
338: int ch;
339:
340: /* Push the data, ignoring CRLF, down the stream
341: **
342: */
343: targetClass = *(sink->isa); /* Copy pointers to procedures */
344:
345: /* Push text from telnet socket down sink
346: **
347: ** @@@@@ To push strings could be faster? (especially is we
348: ** cheat and don't ignore CR! :-}
349: */
350: isoc = HTInputSocket_new(file_number);
351: while ((ch = HTInputSocket_getCharacter(isoc)) >= 0)
352: (*targetClass.put_character)(sink, ch);
353: HTInputSocket_free(isoc);
354: }
355:
2.15 frystyk 356: #endif
2.1 frystyk 357:
358: /* Parse a socket given format and file number
359: **
360: ** This routine is responsible for creating and PRESENTING any
361: ** graphic (or other) objects described by the file.
362: **
363: ** The file number given is assumed to be a TELNET stream ie containing
364: ** CRLF at the end of lines which need to be stripped to LF for unix
365: ** when the format is textual.
366: **
367: ** Returns <0 on error, HT_LOADED on success.
368: */
369:
370: /* The parameter to this function and HTParsefile should be HTRequest */
371:
2.16 ! frystyk 372: PUBLIC int HTParseSocket (
! 373: HTFormat rep_in,
! 374: SOCKFD file_number,
! 375: HTRequest * request)
2.1 frystyk 376: {
377: HTStream * stream;
378: HTStreamClass targetClass;
379:
380: if (request->error_stack) {
2.14 frystyk 381: if (WWWTRACE) TTYPrint(TDEST, "ParseSocket. Called whith non-empty error stack, so I return right away!\n");
2.1 frystyk 382: return -1;
383: }
384:
385: /* Set up stream stack */
386: if ((stream = HTStreamStack(rep_in, request->output_format,
387: request->output_stream,
388: request, YES)) == NULL)
389: return -1;
390:
391: /* Push the data, ignoring CRLF if necessary, down the stream
392: **
393: **
394: ** @@ Bug: This decision ought to be made based on "encoding"
395: ** rather than on format. @@@ When we handle encoding.
396: ** The current method smells anyway.
397: */
398: targetClass = *(stream->isa); /* Copy pointers to procedures */
2.15 frystyk 399: #if 0
2.1 frystyk 400: if (rep_in == WWW_BINARY || rep_in == WWW_UNKNOWN
401: || (HTAnchor_encoding(request->anchor) != HTAtom_for("8bit") &&
402: HTAnchor_encoding(request->anchor) != HTAtom_for("7bit"))
403: || strstr(HTAtom_name(rep_in), "image/")
404: || strstr(HTAtom_name(rep_in), "video/")) { /* @@@@@@ */
405: HTCopy(file_number, stream);
406: } else
407: HTCopyNoCR(file_number, stream);
2.15 frystyk 408: #endif
409: HTCopy(file_number, stream);
2.1 frystyk 410: (*targetClass._free)(stream);
411: return HT_LOADED;
412: }
413:
414:
2.12 frystyk 415: #if 0
2.1 frystyk 416: /* Parse a file given format and file pointer
417: **
418: ** This routine is responsible for creating and PRESENTING any
419: ** graphic (or other) objects described by the file.
420: **
421: ** The file number given is assumed to be a TELNET stream ie containing
422: ** CRLF at the end of lines which need to be stripped to \n for unix
423: ** when the format is textual.
424: **
425: */
2.16 ! frystyk 426: PRIVATE int HTParseFile (
! 427: HTFormat rep_in,
! 428: FILE * fp,
! 429: HTRequest * request)
2.1 frystyk 430: {
431: HTStream * stream;
432: HTStreamClass targetClass;
433:
434: if (request->error_stack) {
2.14 frystyk 435: if (WWWTRACE) TTYPrint(TDEST, "ParseFile... Called whith non-empty error stack, so I return right away!\n");
2.1 frystyk 436: return -1;
437: }
438:
439: /* Set up stream stack */
440: if ((stream = HTStreamStack(rep_in, request->output_format,
441: request->output_stream, request, YES)) == NULL)
442: return -1;
443:
444: /* Push the data down the stream
445: **
446: **
447: ** @@ Bug: This decision ought to be made based on "encoding"
448: ** rather than on content-type. @@@ When we handle encoding.
449: ** The current method smells anyway.
450: */
451: targetClass = *(stream->isa); /* Copy pointers to procedures */
452: HTFileCopy(fp, stream);
453: (*targetClass._free)(stream);
454:
455: return HT_LOADED;
456: }
2.12 frystyk 457: #endif
2.1 frystyk 458:
459: /* ------------------------------------------------------------------------- */
460: /* MULTI THREADED IMPLEMENTATIONS */
461: /* ------------------------------------------------------------------------- */
462:
463: /* Push data from a socket down a stream
464: ** -------------------------------------
465: **
466: ** This routine is responsible for creating and PRESENTING any
467: ** graphic (or other) objects described by the file. As this function
468: ** max reads a chunk of data on size INPUT_BUFFER_SIZE, it can be used
469: ** with both blocking or non-blocking sockets. It will always return to
470: ** the event loop, however if we are using blocking I/O then we get a full
471: ** buffer read, otherwise we get what's available.
472: **
473: ** Returns HT_LOADED if finished reading
2.3 frystyk 474: ** HT_OK if OK, but more to read
2.1 frystyk 475: ** HT_ERROR if error,
476: ** HT_WOULD_BLOCK if read would block
477: */
2.10 frystyk 478: PUBLIC int HTSocketRead (HTRequest * request, HTNet * net)
2.1 frystyk 479: {
2.7 frystyk 480: HTInputSocket *isoc = net->isoc;
2.10 frystyk 481: HTStream *target = net->target;
2.7 frystyk 482: int b_read = isoc->read - isoc->buffer;
2.1 frystyk 483: int status;
2.7 frystyk 484: if (!isoc || isoc->sockfd==INVSOC) {
2.14 frystyk 485: if (PROT_TRACE) TTYPrint(TDEST, "Read Socket. Bad argument\n");
2.1 frystyk 486: return HT_ERROR;
487: }
488:
2.3 frystyk 489: /* Read from socket if we got rid of all the data previously read */
2.4 frystyk 490: do {
2.7 frystyk 491: if (isoc->write >= isoc->read) {
492: if ((b_read = NETREAD(isoc->sockfd, isoc->buffer,
2.4 frystyk 493: INPUT_BUFFER_SIZE)) < 0) {
2.1 frystyk 494: #ifdef EAGAIN
2.4 frystyk 495: if (socerrno==EAGAIN || socerrno==EWOULDBLOCK) /* POSIX */
2.1 frystyk 496: #else
2.11 frystyk 497: if (socerrno==EWOULDBLOCK) /* BSD */
498: #endif
499: {
500: if (PROT_TRACE)
2.14 frystyk 501: TTYPrint(TDEST, "Read Socket. WOULD BLOCK soc %d\n",
2.11 frystyk 502: isoc->sockfd);
503: HTEvent_Register(isoc->sockfd, request, (SockOps) FD_READ,
504: net->cbf, net->priority);
505: return HT_WOULD_BLOCK;
506: } else { /* We have a real error */
507: if (PROT_TRACE)
2.14 frystyk 508: TTYPrint(TDEST, "Read Socket. READ ERROR %d\n",
2.11 frystyk 509: socerrno);
510: return HT_ERROR;
511: }
2.4 frystyk 512: } else if (!b_read) {
2.16 ! frystyk 513: HTAlertCallback *cbf = HTAlert_find(HT_PROG_DONE);
2.3 frystyk 514: if (PROT_TRACE)
2.16 ! frystyk 515: TTYPrint(TDEST,"Read Socket. Finished loading socket %d\n",
! 516: isoc->sockfd);
! 517: if(cbf)(*cbf)(request,HT_PROG_DONE,HT_MSG_NULL,NULL,NULL,NULL);
2.7 frystyk 518: HTEvent_UnRegister(isoc->sockfd, FD_READ);
2.4 frystyk 519: return HT_LOADED;
2.1 frystyk 520: }
521:
2.4 frystyk 522: /* Remember how much we have read from the input socket */
2.7 frystyk 523: isoc->write = isoc->buffer;
524: isoc->read = isoc->buffer + b_read;
2.1 frystyk 525:
526: #ifdef NOT_ASCII
2.4 frystyk 527: {
2.7 frystyk 528: char *p = isoc->buffer;
529: while (p < isoc->read) {
2.4 frystyk 530: *p = FROMASCII(*p);
531: p++;
532: }
2.1 frystyk 533: }
534: #endif
2.3 frystyk 535: if (PROT_TRACE)
2.14 frystyk 536: TTYPrint(TDEST, "Read Socket. %d bytes read from socket %d\n",
2.7 frystyk 537: b_read, isoc->sockfd);
2.8 frystyk 538: net->bytes_read += b_read;
2.16 ! frystyk 539:
! 540: {
! 541: HTAlertCallback *cbf = HTAlert_find(HT_PROG_READ);
! 542: if (cbf)
! 543: (*cbf)(request, HT_PROG_READ, HT_MSG_NULL,NULL,NULL,NULL);
! 544: }
2.4 frystyk 545: }
546:
547: /* Now push the data down the stream */
2.7 frystyk 548: if ((status = (*target->isa->put_block)(target, isoc->buffer,
2.4 frystyk 549: b_read)) != HT_OK) {
550: if (status==HT_WOULD_BLOCK) {
551: if (PROT_TRACE)
2.14 frystyk 552: TTYPrint(TDEST, "Read Socket. Target WOULD BLOCK\n");
2.7 frystyk 553: HTEvent_UnRegister(isoc->sockfd, FD_READ);
2.4 frystyk 554: return HT_WOULD_BLOCK;
2.5 frystyk 555: } else if (status>0) { /* Stream specific return code */
556: if (PROT_TRACE)
2.14 frystyk 557: TTYPrint(TDEST, "Read Socket. Target returns %d\n", status);
2.10 frystyk 558: isoc->write = isoc->buffer + b_read;
2.5 frystyk 559: return status;
560: } else { /* We have a real error */
2.4 frystyk 561: if (PROT_TRACE)
2.14 frystyk 562: TTYPrint(TDEST, "Read Socket. Target ERROR\n");
2.4 frystyk 563: return status;
564: }
2.1 frystyk 565: }
2.7 frystyk 566: isoc->write = isoc->buffer + b_read;
567: } while (net->preemtive);
2.10 frystyk 568: HTEvent_Register(isoc->sockfd, request, (SockOps) FD_READ,
569: net->cbf, net->priority);
2.1 frystyk 570: return HT_WOULD_BLOCK;
571: }
2.2 frystyk 572:
573:
574:
575: /* Push data from an ANSI file descriptor down a stream
576: ** ----------------------------------------------------
577: **
578: ** This routine is responsible for creating and PRESENTING any
579: ** graphic (or other) objects described by the file.
580: **
581: ** Bugs: When we can wait on a file then this should also check interrupts!
582: **
583: ** Returns HT_LOADED if finished reading
584: ** HT_ERROR if error,
585: */
2.10 frystyk 586: PUBLIC int HTFileRead (HTRequest * request, HTNet * net, FILE * fp)
2.2 frystyk 587: {
2.10 frystyk 588: HTInputSocket *isoc = net->isoc;
589: HTStream *target = net->target;
2.2 frystyk 590: int b_read;
591: int status;
592: if (!fp) {
2.14 frystyk 593: if (PROT_TRACE) TTYPrint(TDEST, "Read File... Bad argument\n");
2.2 frystyk 594: return HT_ERROR;
595: }
596:
597: while(1) {
2.7 frystyk 598: if ((b_read = fread(isoc->buffer, 1, INPUT_BUFFER_SIZE, fp))==0){
2.2 frystyk 599: if (ferror(fp)) {
600: if (PROT_TRACE)
2.14 frystyk 601: TTYPrint(TDEST, "Read File... READ ERROR\n");
2.2 frystyk 602: } else
603: return HT_LOADED;
604: }
2.7 frystyk 605: isoc->write = isoc->buffer;
606: isoc->read = isoc->buffer + b_read;
2.2 frystyk 607: if (PROT_TRACE)
2.14 frystyk 608: TTYPrint(TDEST, "Read File... %d bytes read from file %p\n",
2.2 frystyk 609: b_read, fp);
610:
611: /* Now push the data down the stream (we use blocking I/O) */
2.7 frystyk 612: if ((status = (*target->isa->put_block)(target, isoc->buffer,
2.2 frystyk 613: b_read)) != HT_OK) {
614: if (PROT_TRACE)
2.14 frystyk 615: TTYPrint(TDEST, "Read File... Target ERROR\n");
2.2 frystyk 616: return status;
617: }
2.7 frystyk 618: isoc->write = isoc->buffer + b_read;
2.2 frystyk 619: }
620: }
621:
622:
Webmaster