2002-04-30 22:22:54 +00:00
|
|
|
// The contents of this file are subject to the Mozilla Public License
|
|
|
|
// Version 1.0 (the "License"); you may not use this file except in
|
|
|
|
// compliance with the License. You may obtain a copy of the License at
|
|
|
|
// http://www.mozilla.org/MPL/
|
|
|
|
//
|
|
|
|
// Software distributed under the License is distributed on an "AS IS"
|
|
|
|
// basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
|
|
|
|
// License for the specific language governing rights and limitations
|
|
|
|
// under the License.
|
|
|
|
//
|
|
|
|
// The Original Code is the Berkeley Open Infrastructure for Network Computing.
|
|
|
|
//
|
|
|
|
// The Initial Developer of the Original Code is the SETI@home project.
|
|
|
|
// Portions created by the SETI@home project are Copyright (C) 2002
|
|
|
|
// University of California at Berkeley. All Rights Reserved.
|
|
|
|
//
|
|
|
|
// Contributor(s):
|
|
|
|
//
|
|
|
|
|
2002-06-06 18:50:12 +00:00
|
|
|
#include "windows_cpp.h"
|
|
|
|
|
2002-04-30 22:22:54 +00:00
|
|
|
#include <stdio.h>
|
2002-06-06 18:50:12 +00:00
|
|
|
|
|
|
|
#ifdef _WIN32
|
|
|
|
#include "winsock.h"
|
2002-12-18 20:17:35 +00:00
|
|
|
#include "Win_net.h"
|
2002-07-15 23:21:20 +00:00
|
|
|
#endif
|
|
|
|
|
|
|
|
#if HAVE_SYS_TIME_H
|
2002-04-30 22:22:54 +00:00
|
|
|
#include <sys/time.h>
|
2002-07-15 23:21:20 +00:00
|
|
|
#endif
|
|
|
|
#if HAVE_SYS_SOCKET_H
|
2002-04-30 22:22:54 +00:00
|
|
|
#include <sys/socket.h>
|
2002-07-15 23:21:20 +00:00
|
|
|
#endif
|
|
|
|
#if HAVE_SYS_SELECT_H
|
2002-04-30 22:22:54 +00:00
|
|
|
#include <sys/select.h>
|
2002-07-15 23:21:20 +00:00
|
|
|
#endif
|
|
|
|
#if HAVE_NETINET_IN_H
|
2002-04-30 22:22:54 +00:00
|
|
|
#include <netinet/in.h>
|
2002-07-15 23:21:20 +00:00
|
|
|
#endif
|
|
|
|
#if HAVE_NETINET_TCP_H
|
2002-04-30 22:22:54 +00:00
|
|
|
#include <netinet/tcp.h>
|
2002-07-15 23:21:20 +00:00
|
|
|
#endif
|
|
|
|
#if HAVE_NETDB_H
|
2002-04-30 22:22:54 +00:00
|
|
|
#include <netdb.h>
|
2002-07-15 23:21:20 +00:00
|
|
|
#endif
|
|
|
|
#if HAVE_UNISTD_H
|
2002-06-06 18:50:12 +00:00
|
|
|
#include <unistd.h>
|
|
|
|
#endif
|
2002-07-15 23:21:20 +00:00
|
|
|
#if HAVE_FCNTL_H
|
|
|
|
#include <fcntl.h>
|
|
|
|
#endif
|
2002-06-06 18:50:12 +00:00
|
|
|
|
|
|
|
#include <sys/types.h>
|
2002-04-30 22:22:54 +00:00
|
|
|
#include <errno.h>
|
|
|
|
#include <stdlib.h>
|
2002-06-06 18:50:12 +00:00
|
|
|
#include <time.h>
|
2002-05-08 22:21:27 +00:00
|
|
|
#include <string.h>
|
2002-04-30 22:22:54 +00:00
|
|
|
|
|
|
|
#include "error_numbers.h"
|
|
|
|
#include "log_flags.h"
|
|
|
|
#include "net_xfer.h"
|
|
|
|
|
2002-11-13 18:38:09 +00:00
|
|
|
// If socklen_t isn't defined, define it here as size_t
|
2002-05-25 15:03:53 +00:00
|
|
|
#if !defined(socklen_t)
|
|
|
|
#define socklen_t size_t
|
|
|
|
#endif
|
|
|
|
|
2002-12-18 23:56:25 +00:00
|
|
|
int NET_XFER::get_ip_addr( char *hostname, int &ip_addr ) {
|
2002-04-30 22:22:54 +00:00
|
|
|
hostent* hep;
|
|
|
|
|
2002-12-18 20:17:35 +00:00
|
|
|
#ifdef _WIN32
|
2002-12-18 23:56:25 +00:00
|
|
|
if(NetOpen()) return -1;
|
2002-12-18 20:17:35 +00:00
|
|
|
#endif
|
2002-12-09 09:36:40 +00:00
|
|
|
hep = gethostbyname(hostname);
|
2002-04-30 22:22:54 +00:00
|
|
|
if (!hep) {
|
|
|
|
fprintf(stderr, "can't resolve hostname %s\n", hostname);
|
2002-12-18 20:17:35 +00:00
|
|
|
#ifdef _WIN32
|
2002-12-18 23:56:25 +00:00
|
|
|
NetClose();
|
2002-12-18 20:17:35 +00:00
|
|
|
#endif
|
2002-04-30 22:22:54 +00:00
|
|
|
return ERR_GETHOSTBYNAME;
|
|
|
|
}
|
2002-12-18 23:56:25 +00:00
|
|
|
ip_addr = *(int*)hep->h_addr_list[0];
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Attempt to open a nonblocking socket to a server
|
|
|
|
//
|
|
|
|
int NET_XFER::open_server() {
|
|
|
|
sockaddr_in addr;
|
|
|
|
int fd=0, ipaddr, retval=0;
|
|
|
|
|
|
|
|
retval = get_ip_addr(hostname, ipaddr);
|
|
|
|
if (retval) return retval;
|
|
|
|
|
2002-04-30 22:22:54 +00:00
|
|
|
fd = ::socket(AF_INET, SOCK_STREAM, 0);
|
2002-12-18 20:17:35 +00:00
|
|
|
if (fd < 0) {
|
|
|
|
#ifdef _WIN32
|
2002-12-18 23:56:25 +00:00
|
|
|
NetClose();
|
2002-12-18 20:17:35 +00:00
|
|
|
#endif
|
2002-12-18 23:56:25 +00:00
|
|
|
return -1;
|
2002-12-18 20:17:35 +00:00
|
|
|
}
|
2002-04-30 22:22:54 +00:00
|
|
|
|
2002-06-06 18:50:12 +00:00
|
|
|
#ifdef _WIN32
|
2002-08-22 20:19:18 +00:00
|
|
|
unsigned long one = 1;
|
|
|
|
ioctlsocket(fd, FIONBIO, &one);
|
2002-06-06 18:50:12 +00:00
|
|
|
#else
|
|
|
|
int flags;
|
2002-04-30 22:22:54 +00:00
|
|
|
flags = fcntl(fd, F_GETFL, 0);
|
|
|
|
if (flags < 0) return -1;
|
2002-08-22 20:19:18 +00:00
|
|
|
else if (fcntl(fd, F_SETFL, flags|O_NONBLOCK) < 0 ) return -1;
|
2002-06-06 18:50:12 +00:00
|
|
|
#endif
|
2002-04-30 22:22:54 +00:00
|
|
|
|
|
|
|
addr.sin_family = AF_INET;
|
|
|
|
addr.sin_port = htons(port);
|
|
|
|
addr.sin_addr.s_addr = ((long)ipaddr);
|
|
|
|
retval = connect(fd, (sockaddr*)&addr, sizeof(addr));
|
|
|
|
if (retval) {
|
2002-06-06 18:50:12 +00:00
|
|
|
#ifdef _WIN32
|
2002-06-21 00:22:59 +00:00
|
|
|
errno = WSAGetLastError();
|
|
|
|
if (errno != WSAEINPROGRESS && errno != WSAEWOULDBLOCK) {
|
2002-06-06 18:50:12 +00:00
|
|
|
closesocket(fd);
|
2003-02-06 19:08:18 +00:00
|
|
|
NetClose();
|
2002-06-06 18:50:12 +00:00
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
#else
|
2002-04-30 22:22:54 +00:00
|
|
|
if (errno != EINPROGRESS) {
|
|
|
|
close(fd);
|
2002-06-21 18:31:32 +00:00
|
|
|
perror("connect");
|
2002-04-30 22:22:54 +00:00
|
|
|
return -1;
|
|
|
|
}
|
2002-06-06 18:50:12 +00:00
|
|
|
#endif
|
2002-04-30 22:22:54 +00:00
|
|
|
} else {
|
|
|
|
is_connected = true;
|
|
|
|
}
|
|
|
|
socket = fd;
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2002-08-22 20:19:18 +00:00
|
|
|
void NET_XFER::close_socket() {
|
2002-08-02 18:31:20 +00:00
|
|
|
#ifdef _WIN32
|
2002-12-18 20:17:35 +00:00
|
|
|
NetClose();
|
2002-08-02 18:31:20 +00:00
|
|
|
if (socket) closesocket(socket);
|
|
|
|
#else
|
|
|
|
if (socket) close(socket);
|
|
|
|
#endif
|
|
|
|
}
|
2002-04-30 22:22:54 +00:00
|
|
|
|
|
|
|
void NET_XFER::init(char* host, int p, int b) {
|
2002-07-15 23:21:20 +00:00
|
|
|
socket = -1;
|
2002-04-30 22:22:54 +00:00
|
|
|
is_connected = false;
|
|
|
|
want_download = false;
|
|
|
|
want_upload = false;
|
|
|
|
do_file_io = false;
|
|
|
|
io_done = false;
|
2002-07-15 23:21:20 +00:00
|
|
|
file = NULL;
|
2002-04-30 22:22:54 +00:00
|
|
|
io_ready = false;
|
|
|
|
error = 0;
|
2002-07-15 23:21:20 +00:00
|
|
|
strcpy(hostname, host);
|
|
|
|
port = p;
|
2002-04-30 22:22:54 +00:00
|
|
|
blocksize = b;
|
|
|
|
}
|
|
|
|
|
2002-07-15 23:21:20 +00:00
|
|
|
// Insert a NET_XFER object into the set
|
|
|
|
//
|
2002-04-30 22:22:54 +00:00
|
|
|
int NET_XFER_SET::insert(NET_XFER* nxp) {
|
|
|
|
int retval = nxp->open_server();
|
|
|
|
if (retval) return retval;
|
|
|
|
net_xfers.push_back(nxp);
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2002-07-15 23:21:20 +00:00
|
|
|
// Remove a NET_XFER object from the set
|
|
|
|
//
|
2002-04-30 22:22:54 +00:00
|
|
|
int NET_XFER_SET::remove(NET_XFER* nxp) {
|
|
|
|
vector<NET_XFER*>::iterator iter;
|
|
|
|
|
2002-08-02 18:31:20 +00:00
|
|
|
nxp->close_socket();
|
2002-04-30 22:22:54 +00:00
|
|
|
|
|
|
|
iter = net_xfers.begin();
|
|
|
|
while (iter != net_xfers.end()) {
|
|
|
|
if (*iter == nxp) {
|
|
|
|
net_xfers.erase(iter);
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
iter++;
|
|
|
|
}
|
2002-08-20 00:30:13 +00:00
|
|
|
fprintf(stdout, "NET_XFER_SET::remove(): not found\n");
|
2002-04-30 22:22:54 +00:00
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
// transfer data to/from a list of active streams
|
|
|
|
// transfer at most max_bytes bytes.
|
2002-07-15 23:21:20 +00:00
|
|
|
// TODO: implement other bandwidth constraints (ul/dl ratio, time of day)
|
2002-08-22 20:19:18 +00:00
|
|
|
//
|
2002-04-30 22:22:54 +00:00
|
|
|
int NET_XFER_SET::poll(int max_bytes, int& bytes_transferred) {
|
|
|
|
int n, retval;
|
2003-02-04 22:15:30 +00:00
|
|
|
struct timeval timeout;
|
2002-04-30 22:22:54 +00:00
|
|
|
|
|
|
|
bytes_transferred = 0;
|
|
|
|
while (1) {
|
2003-02-06 19:08:18 +00:00
|
|
|
timeout.tv_sec = timeout.tv_usec = 0;
|
|
|
|
#ifndef _WIN32
|
|
|
|
timeout.tv_sec = 1;
|
|
|
|
#endif
|
2003-02-04 22:15:30 +00:00
|
|
|
retval = do_select(max_bytes, n, timeout);
|
2003-02-06 19:08:18 +00:00
|
|
|
if (retval) return retval;
|
2002-04-30 22:22:54 +00:00
|
|
|
if (n == 0) break;
|
|
|
|
max_bytes -= n;
|
|
|
|
bytes_transferred += n;
|
|
|
|
if (max_bytes < 0) break;
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// do a select and do I/O on as many sockets as possible.
|
|
|
|
//
|
2003-02-04 22:15:30 +00:00
|
|
|
int NET_XFER_SET::do_select(int max_bytes, int& bytes_transferred, struct timeval timeout) {
|
2003-02-12 19:21:44 +00:00
|
|
|
struct timeval zeros;
|
2002-04-30 22:22:54 +00:00
|
|
|
int n, fd, retval;
|
|
|
|
socklen_t i;
|
|
|
|
NET_XFER *nxp;
|
2002-05-25 15:03:53 +00:00
|
|
|
#if GETSOCKOPT_SIZE_T
|
|
|
|
size_t intsize = sizeof(int);
|
2002-05-30 08:33:30 +00:00
|
|
|
#elif GETSOCKOPT_SOCKLEN_T
|
|
|
|
socklen_t intsize = sizeof(int);
|
2002-05-25 15:03:53 +00:00
|
|
|
#else
|
2002-06-01 20:26:21 +00:00
|
|
|
//int intsize = sizeof(int);
|
|
|
|
socklen_t intsize = sizeof(int);
|
2002-05-25 15:03:53 +00:00
|
|
|
#endif
|
2002-04-30 22:22:54 +00:00
|
|
|
|
|
|
|
bytes_transferred = 0;
|
|
|
|
|
|
|
|
fd_set read_fds, write_fds, error_fds;
|
2003-02-12 19:21:44 +00:00
|
|
|
memset(&zeros, 0, sizeof(zeros));
|
2003-02-04 22:15:30 +00:00
|
|
|
|
2002-04-30 22:22:54 +00:00
|
|
|
FD_ZERO(&read_fds);
|
|
|
|
FD_ZERO(&write_fds);
|
|
|
|
FD_ZERO(&error_fds);
|
|
|
|
|
|
|
|
// do a select on all active streams
|
|
|
|
//
|
|
|
|
for (i=0; i<net_xfers.size(); i++) {
|
2002-08-30 21:41:03 +00:00
|
|
|
nxp = net_xfers[i];
|
|
|
|
if (!nxp->is_connected) {
|
|
|
|
FD_SET(net_xfers[i]->socket, &write_fds);
|
|
|
|
} else if (net_xfers[i]->want_download) {
|
|
|
|
FD_SET(net_xfers[i]->socket, &read_fds);
|
|
|
|
} else if (net_xfers[i]->want_upload) {
|
|
|
|
FD_SET(net_xfers[i]->socket, &write_fds);
|
|
|
|
}
|
|
|
|
FD_SET(net_xfers[i]->socket, &error_fds);
|
2002-04-30 22:22:54 +00:00
|
|
|
}
|
2003-02-12 19:21:44 +00:00
|
|
|
n = select(FD_SETSIZE, &read_fds, &write_fds, &error_fds, &zeros);
|
2002-04-30 22:22:54 +00:00
|
|
|
if (log_flags.net_xfer_debug) printf("select returned %d\n", n);
|
|
|
|
if (n == 0) return 0;
|
|
|
|
if (n < 0) return ERR_SELECT;
|
|
|
|
|
|
|
|
// if got a descriptor, find the first one in round-robin order
|
|
|
|
// and do I/O on it
|
|
|
|
for (i=0; i<net_xfers.size(); i++) {
|
2002-08-30 21:41:03 +00:00
|
|
|
nxp = net_xfers[i];
|
2002-04-30 22:22:54 +00:00
|
|
|
fd = nxp->socket;
|
2002-08-30 21:41:03 +00:00
|
|
|
if (FD_ISSET(fd, &read_fds) || FD_ISSET(fd, &write_fds)) {
|
2002-04-30 22:22:54 +00:00
|
|
|
if (!nxp->is_connected) {
|
2002-06-06 18:50:12 +00:00
|
|
|
#ifdef _WIN32
|
|
|
|
getsockopt(fd, SOL_SOCKET, SO_ERROR, (char *)&n, (int *)&intsize);
|
2002-07-22 21:25:35 +00:00
|
|
|
#elif __APPLE__
|
2002-08-30 21:41:03 +00:00
|
|
|
getsockopt(fd, SOL_SOCKET, SO_ERROR, &n, (int *)&intsize);
|
2002-06-06 18:50:12 +00:00
|
|
|
#else
|
2003-02-12 19:21:44 +00:00
|
|
|
getsockopt(fd, SOL_SOCKET, SO_ERROR, (void*)&n, &intsize);
|
2002-06-06 18:50:12 +00:00
|
|
|
#endif
|
2002-04-30 22:22:54 +00:00
|
|
|
if (n) {
|
|
|
|
if (log_flags.net_xfer_debug) {
|
|
|
|
printf("socket %d connect failed\n", fd);
|
|
|
|
}
|
|
|
|
nxp->error = ERR_CONNECT;
|
|
|
|
nxp->io_done = true;
|
|
|
|
} else {
|
|
|
|
if (log_flags.net_xfer_debug) {
|
|
|
|
printf("socket %d is connected\n", fd);
|
|
|
|
}
|
|
|
|
nxp->is_connected = true;
|
|
|
|
bytes_transferred += 1;
|
|
|
|
}
|
|
|
|
} else if (nxp->do_file_io) {
|
|
|
|
if (max_bytes > 0) {
|
|
|
|
retval = nxp->do_xfer(n);
|
|
|
|
max_bytes -= n;
|
|
|
|
bytes_transferred += n;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
nxp->io_ready = true;
|
|
|
|
}
|
2002-08-30 21:41:03 +00:00
|
|
|
} else if (FD_ISSET(fd, &error_fds)) {
|
2002-04-30 22:22:54 +00:00
|
|
|
if (log_flags.net_xfer_debug) printf("got error on socket %d\n", fd);
|
|
|
|
nxp = lookup_fd(fd);
|
2002-08-30 21:41:03 +00:00
|
|
|
nxp->got_error();
|
|
|
|
}
|
2002-04-30 22:22:54 +00:00
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2002-07-15 23:21:20 +00:00
|
|
|
// Return the NET_XFER object whose socket matches fd
|
|
|
|
//
|
2002-04-30 22:22:54 +00:00
|
|
|
NET_XFER* NET_XFER_SET::lookup_fd(int fd) {
|
|
|
|
for (unsigned int i=0; i<net_xfers.size(); i++) {
|
|
|
|
if (net_xfers[i]->socket == fd) {
|
|
|
|
return net_xfers[i];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// transfer up to a block of data; return #bytes transferred
|
|
|
|
//
|
|
|
|
int NET_XFER::do_xfer(int& nbytes_transferred) {
|
|
|
|
int n, m, nleft, offset;
|
|
|
|
char* buf = (char*)malloc(blocksize);
|
|
|
|
|
|
|
|
nbytes_transferred = 0;
|
|
|
|
|
|
|
|
if (!buf) return ERR_MALLOC;
|
|
|
|
if (want_download) {
|
2002-06-06 18:50:12 +00:00
|
|
|
#ifdef _WIN32
|
2002-08-30 21:41:03 +00:00
|
|
|
n = recv(socket, buf, blocksize, 0);
|
2002-06-06 18:50:12 +00:00
|
|
|
#else
|
2002-08-30 21:41:03 +00:00
|
|
|
n = read(socket, buf, blocksize);
|
2002-06-06 18:50:12 +00:00
|
|
|
#endif
|
2002-08-30 21:41:03 +00:00
|
|
|
if (log_flags.net_xfer_debug) {
|
|
|
|
printf("read %d bytes from socket %d\n", n, socket);
|
|
|
|
}
|
|
|
|
if (n == 0) {
|
2002-08-12 21:54:19 +00:00
|
|
|
io_done = true;
|
2002-08-30 21:41:03 +00:00
|
|
|
want_download = false;
|
2002-08-12 21:54:19 +00:00
|
|
|
goto done;
|
2002-08-30 21:41:03 +00:00
|
|
|
} else if (n < 0) {
|
|
|
|
io_done = true;
|
|
|
|
error = ERR_READ;
|
|
|
|
goto done;
|
|
|
|
} else {
|
|
|
|
nbytes_transferred += n;
|
|
|
|
m = fwrite(buf, 1, n, file);
|
|
|
|
if (n != m) {
|
|
|
|
fprintf(stdout, "Error: incomplete disk write\n");
|
|
|
|
io_done = true;
|
|
|
|
error = ERR_FWRITE;
|
|
|
|
goto done;
|
|
|
|
}
|
|
|
|
}
|
2002-04-30 22:22:54 +00:00
|
|
|
} else if (want_upload) {
|
2002-08-30 21:41:03 +00:00
|
|
|
m = fread(buf, 1, blocksize, file);
|
2002-04-30 22:22:54 +00:00
|
|
|
if (m == 0) {
|
|
|
|
want_upload = false;
|
|
|
|
io_done = true;
|
|
|
|
goto done;
|
|
|
|
} else if (m < 0) {
|
2002-08-30 21:41:03 +00:00
|
|
|
io_done = true;
|
|
|
|
error = ERR_FREAD;
|
|
|
|
goto done;
|
|
|
|
}
|
|
|
|
nleft = m;
|
|
|
|
offset = 0;
|
|
|
|
while (nleft) {
|
2002-06-06 18:50:12 +00:00
|
|
|
#ifdef _WIN32
|
2002-08-30 21:41:03 +00:00
|
|
|
n = send(socket, buf+offset, nleft, 0);
|
2002-06-06 18:50:12 +00:00
|
|
|
#else
|
2002-08-30 21:41:03 +00:00
|
|
|
n = write(socket, buf+offset, nleft);
|
2002-06-06 18:50:12 +00:00
|
|
|
#endif
|
2002-04-30 22:22:54 +00:00
|
|
|
if (log_flags.net_xfer_debug) {
|
|
|
|
printf("wrote %d bytes to socket %d\n", n, socket);
|
|
|
|
}
|
2002-08-30 21:41:03 +00:00
|
|
|
if (n < 0) {
|
2002-08-25 03:24:55 +00:00
|
|
|
error = ERR_WRITE;
|
|
|
|
io_done = true;
|
|
|
|
goto done;
|
|
|
|
} else if (n < nleft) {
|
2002-08-25 03:15:47 +00:00
|
|
|
fseek( file, n+nbytes_transferred-blocksize, SEEK_CUR );
|
|
|
|
nbytes_transferred += n;
|
|
|
|
goto done;
|
|
|
|
}
|
2002-08-30 21:41:03 +00:00
|
|
|
nleft -= n;
|
|
|
|
offset += n;
|
2002-04-30 22:22:54 +00:00
|
|
|
nbytes_transferred += n;
|
2002-08-30 21:41:03 +00:00
|
|
|
}
|
2002-04-30 22:22:54 +00:00
|
|
|
}
|
|
|
|
done:
|
|
|
|
free(buf);
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
void NET_XFER::got_error() {
|
|
|
|
error = ERR_IO;
|
|
|
|
io_done = true;
|
2003-02-12 19:21:44 +00:00
|
|
|
if (log_flags.net_xfer_debug) {
|
|
|
|
printf("IO error on socket %d\n", socket);
|
|
|
|
}
|
2002-04-30 22:22:54 +00:00
|
|
|
}
|