Commit 7a129c17 authored by Sergey Lyubka's avatar Sergey Lyubka

Added http client with unit tests

parent 400084ea
......@@ -318,7 +318,7 @@ enum connection_flags {
CONN_HEADERS_SENT = 8, // User callback has sent HTTP headers
CONN_BUFFER = 16, // CGI only. Holds data send until CGI prints
// all HTTP headers
CONN_CONNECTED = 32, // HTTP client has connected
CONN_CONNECTING = 32, // HTTP client is doing non-blocking connect()
CONN_LONG_RUNNING = 64 // Long-running URI handlers
};
......@@ -1154,6 +1154,7 @@ static void open_cgi_endpoint(struct connection *conn, const char *prog) {
conn->endpoint_type = EP_CGI;
conn->endpoint.cgi_sock = fds[0];
spool(&conn->remote_iobuf, cgi_status, sizeof(cgi_status) - 1);
conn->mg_conn.status_code = 200;
conn->flags |= CONN_BUFFER;
} else {
closesocket(fds[0]);
......@@ -1193,19 +1194,24 @@ static void read_from_cgi(struct connection *conn) {
}
}
memcpy(io->buf + 9, status, 3);
conn->mg_conn.status_code = atoi(status);
conn->flags &= ~CONN_BUFFER;
}
}
}
static void forward_post_data(struct connection *conn) {
struct iobuf *io = &conn->local_iobuf;
int n = send(conn->endpoint.cgi_sock, io->buf, io->len, 0);
if (n > 0) {
static void discard_leading_iobuf_bytes(struct iobuf *io, int n) {
if (n >= 0 && n <= io->len) {
memmove(io->buf, io->buf + n, io->len - n);
io->len -= n;
}
}
static void forward_post_data(struct connection *conn) {
struct iobuf *io = &conn->local_iobuf;
int n = send(conn->endpoint.cgi_sock, io->buf, io->len, 0);
discard_leading_iobuf_bytes(io, n);
}
#endif // !NO_CGI
// 'sa' must be an initialized address to bind to
......@@ -1331,9 +1337,10 @@ static struct connection *accept_new_connection(struct mg_server *server) {
}
static void close_conn(struct connection *conn) {
DBG(("%p %d %d", conn, conn->flags, conn->endpoint_type));
LINKED_LIST_REMOVE(&conn->link);
closesocket(conn->client_sock);
close_local_endpoint(conn);
DBG(("%p %d %d", conn, conn->flags, conn->endpoint_type));
free(conn->request); // It's OK to free(NULL), ditto below
free(conn->path_info);
free(conn->remote_iobuf.buf);
......@@ -1840,8 +1847,7 @@ static int deliver_websocket_frame(struct connection *conn) {
if (conn->endpoint.uh->handler(&conn->mg_conn)) {
conn->flags |= CONN_SPOOL_DONE;
}
memmove(buf, buf + frame_len, buf_len - frame_len);
conn->local_iobuf.len -= frame_len;
discard_leading_iobuf_bytes(&conn->local_iobuf, frame_len);
}
return buffered;
......@@ -1938,8 +1944,7 @@ static void write_to_socket(struct connection *conn) {
if (is_error(n)) {
conn->flags |= CONN_CLOSE;
} else if (n > 0) {
memmove(io->buf, io->buf + n, io->len - n);
io->len -= n;
discard_leading_iobuf_bytes(io, n);
conn->num_bytes_sent += n;
}
......@@ -2641,8 +2646,7 @@ static void forward_put_data(struct connection *conn) {
struct iobuf *io = &conn->local_iobuf;
int n = write(conn->endpoint.fd, io->buf, io->len);
if (n > 0) {
memmove(io->buf, io->buf + n, io->len - n);
io->len -= n;
discard_leading_iobuf_bytes(io, n);
conn->cl -= n;
if (conn->cl <= 0) {
close_local_endpoint(conn);
......@@ -3312,7 +3316,6 @@ static void handle_lsp_request(struct connection *conn, const char *path,
#endif // USE_LUA
static void open_local_endpoint(struct connection *conn) {
const char *cl_hdr = mg_get_header(&conn->mg_conn, "Content-Length");
#ifndef NO_FILESYSTEM
static const char lua_pat[] = LUA_SCRIPT_PATTERN;
file_stat_t st;
......@@ -3322,8 +3325,6 @@ static void open_local_endpoint(struct connection *conn) {
const char *dir_lst = conn->server->config_options[ENABLE_DIRECTORY_LISTING];
#endif
conn->mg_conn.content_len = cl_hdr == NULL ? 0 : (int) to64(cl_hdr);
// Call URI handler if one is registered for this URI
conn->endpoint.uh = find_uri_handler(conn->server, conn->mg_conn.uri);
if (conn->endpoint.uh != NULL) {
......@@ -3425,7 +3426,7 @@ static int is_valid_uri(const char *uri) {
return uri[0] == '/' || (uri[0] == '*' && uri[1] == '\0');
}
static void process_request(struct connection *conn) {
static void try_http_parse_and_set_content_length(struct connection *conn) {
struct iobuf *io = &conn->local_iobuf;
if (conn->request_len == 0 &&
......@@ -3435,14 +3436,24 @@ static void process_request(struct connection *conn) {
// become invalid.
conn->request = (char *) malloc(conn->request_len);
memcpy(conn->request, io->buf, conn->request_len);
DBG(("%p ==> [%.*s]", conn, conn->request_len, conn->request));
memmove(io->buf, io->buf + conn->request_len, io->len - conn->request_len);
io->len -= conn->request_len;
DBG(("%p [%.*s]", conn, conn->request_len, conn->request));
discard_leading_iobuf_bytes(io, conn->request_len);
conn->request_len = parse_http_message(conn->request, conn->request_len,
&conn->mg_conn);
if (conn->request_len > 0) {
const char *cl_hdr = mg_get_header(&conn->mg_conn, "Content-Length");
conn->cl = cl_hdr == NULL ? 0 : to64(cl_hdr);
conn->mg_conn.content_len = (long int) conn->cl;
}
}
}
DBG(("%p %d %d [%.*s]", conn, conn->request_len, io->len, io->len, io->buf));
static void process_request(struct connection *conn) {
struct iobuf *io = &conn->local_iobuf;
try_http_parse_and_set_content_length(conn);
DBG(("%p %d %d %d [%.*s]", conn, conn->request_len, io->len, conn->flags,
io->len, io->buf));
if (conn->request_len < 0 ||
(conn->request_len > 0 && !is_valid_uri(conn->mg_conn.uri))) {
send_http_error(conn, 400, NULL);
......@@ -3475,48 +3486,96 @@ static void process_request(struct connection *conn) {
#endif
}
static void call_http_client_handler(struct connection *conn, int code) {
conn->mg_conn.status_code = code;
// For responses without Content-Lengh, use the whole buffer
if (conn->cl == 0 && code == MG_DOWNLOAD_SUCCESS) {
conn->mg_conn.content_len = conn->local_iobuf.len;
}
conn->mg_conn.content = conn->local_iobuf.buf;
if (conn->handler(&conn->mg_conn) || code == MG_CONNECT_FAILURE ||
code == MG_DOWNLOAD_FAILURE) {
conn->flags |= CONN_CLOSE;
}
discard_leading_iobuf_bytes(&conn->local_iobuf, conn->mg_conn.content_len);
conn->flags = conn->mg_conn.status_code = 0;
conn->cl = conn->num_bytes_sent = conn->request_len = 0;
free(conn->request);
conn->request = NULL;
}
static void process_response(struct connection *conn) {
struct iobuf *io = &conn->local_iobuf;
try_http_parse_and_set_content_length(conn);
DBG(("%p %d %d [%.*s]", conn, conn->request_len, io->len,
io->len > 40 ? 40 : io->len, io->buf));
if (conn->request_len < 0 ||
(conn->request_len == 0 && io->len > MAX_REQUEST_SIZE)) {
call_http_client_handler(conn, MG_DOWNLOAD_FAILURE);
}
if (io->len >= conn->cl) {
call_http_client_handler(conn, MG_DOWNLOAD_SUCCESS);
}
}
static void callback_http_client_on_connect(struct connection *conn) {
int ok;
socklen_t len = sizeof(ok);
if (getsockopt(conn->client_sock, SOL_SOCKET, SO_ERROR, (char *) &ok,
&len) == 0 && ok == 0) {
conn->mg_conn.status_code = MG_CONNECT_SUCCESS;
}
conn->flags &= ~CONN_CONNECTING;
if (conn->handler(&conn->mg_conn) || ok != 0) {
conn->flags |= CONN_CLOSE;
}
}
static void read_from_socket(struct connection *conn) {
char buf[IOBUF_SIZE];
int ok, n = 0;
socklen_t len = sizeof(ok);
int n = 0;
if (conn->endpoint_type == EP_CLIENT) {
conn->mg_conn.wsbits = 1;
if (!(conn->flags & CONN_CONNECTED) &&
getsockopt(conn->client_sock, SOL_SOCKET, SO_ERROR,
(char *) &ok, &len) < 0) {
conn->mg_conn.wsbits = 0;
}
conn->handler(&conn->mg_conn);
conn->flags |= CONN_CLOSE | CONN_CONNECTED;
} else {
if (conn->ssl != NULL) {
if (conn->endpoint_type == EP_CLIENT && conn->flags & CONN_CONNECTING) {
callback_http_client_on_connect(conn);
return;
}
if (conn->ssl != NULL) {
#ifdef USE_SSL
if (conn->flags & CONN_SSL_HANDS_SHAKEN) {
n = SSL_read(conn->ssl, buf, sizeof(buf));
} else {
if (SSL_accept(conn->ssl) == 1) {
conn->flags |= CONN_SSL_HANDS_SHAKEN;
}
return;
}
#endif
if (conn->flags & CONN_SSL_HANDS_SHAKEN) {
n = SSL_read(conn->ssl, buf, sizeof(buf));
} else {
n = recv(conn->client_sock, buf, sizeof(buf), 0);
if (SSL_accept(conn->ssl) == 1) {
conn->flags |= CONN_SSL_HANDS_SHAKEN;
}
return;
}
#endif
} else {
n = recv(conn->client_sock, buf, sizeof(buf), 0);
}
DBG(("%p %d", conn, n));
if (is_error(n)) {
conn->flags |= CONN_CLOSE;
} else if (n > 0) {
spool(&conn->local_iobuf, buf, n);
DBG(("%p %d %d (1)", conn, n, conn->flags));
if (is_error(n)) {
if (conn->endpoint_type == EP_CLIENT && conn->local_iobuf.len > 0) {
call_http_client_handler(conn, MG_DOWNLOAD_SUCCESS);
}
conn->flags |= CONN_CLOSE;
} else if (n > 0) {
spool(&conn->local_iobuf, buf, n);
if (conn->endpoint_type == EP_CLIENT) {
process_response(conn);
} else {
process_request(conn);
}
}
DBG(("%p %d %d (2)", conn, n, conn->flags));
}
int mg_connect(struct mg_server *server, const char *host,
int port, mg_handler_t handler, void *param) {
int mg_connect(struct mg_server *server, const char *host, int port,
mg_handler_t handler, void *param) {
sock_t sock = INVALID_SOCKET;
struct sockaddr_in sin;
struct hostent *he = NULL;
......@@ -3544,10 +3603,16 @@ int mg_connect(struct mg_server *server, const char *host,
conn->handler = handler;
conn->mg_conn.server_param = server->server_data;
conn->mg_conn.connection_param = param;
conn->birth_time = conn->last_activity_time = time(NULL);
conn->flags = CONN_CONNECTING;
conn->mg_conn.status_code = MG_CONNECT_FAILURE;
LINKED_LIST_ADD_TO_FRONT(&server->active_connections, &conn->link);
DBG(("%p %s:%d", conn, host, port));
if (connect_ret_val == 0) {
conn->flags = CONN_CONNECTED;
conn->mg_conn.status_code = MG_CONNECT_SUCCESS;
conn->flags &= ~CONN_CONNECTING;
conn->mg_conn.content = conn->local_iobuf.buf;
handler(&conn->mg_conn);
}
......@@ -3593,17 +3658,11 @@ static void log_access(const struct connection *conn, const char *path) {
}
#endif
static void gobble_prior_post_data(struct iobuf *io, int len) {
if (len > 0 && len <= io->len) {
memmove(io->buf, io->buf + len, io->len - len);
io->len -= len;
}
}
static void close_local_endpoint(struct connection *conn) {
// Must be done before free()
int keep_alive = should_keep_alive(&conn->mg_conn) &&
(conn->endpoint_type == EP_FILE || conn->endpoint_type == EP_USER);
DBG(("%p %d %d %d", conn, conn->endpoint_type, keep_alive, conn->flags));
switch (conn->endpoint_type) {
case EP_PUT: close(conn->endpoint.fd); break;
......@@ -3613,17 +3672,16 @@ static void close_local_endpoint(struct connection *conn) {
}
#ifndef NO_LOGGING
if (conn->mg_conn.status_code != 400) {
if (conn->mg_conn.status_code > 0 && conn->endpoint_type != EP_CLIENT &&
conn->mg_conn.status_code != 400) {
log_access(conn, conn->server->config_options[ACCESS_LOG_FILE]);
}
#endif
if (conn->endpoint_type == EP_USER) {
gobble_prior_post_data(&conn->local_iobuf, conn->mg_conn.content_len);
}
// Gobble possible POST data sent to the URI handler
discard_leading_iobuf_bytes(&conn->local_iobuf, conn->mg_conn.content_len);
conn->endpoint_type = EP_NONE;
conn->flags = 0;
conn->flags = conn->mg_conn.status_code = 0;
conn->cl = conn->num_bytes_sent = conn->request_len = 0;
free(conn->request);
conn->request = NULL;
......@@ -3690,6 +3748,9 @@ unsigned int mg_poll_server(struct mg_server *server, int milliseconds) {
LINKED_LIST_FOREACH(&server->active_connections, lp, tmp) {
conn = LINKED_LIST_ENTRY(lp, struct connection, link);
add_to_set(conn->client_sock, &read_set, &max_fd);
if (conn->endpoint_type == EP_CLIENT && (conn->flags & CONN_CONNECTING)) {
add_to_set(conn->client_sock, &write_set, &max_fd);
}
if (conn->endpoint_type == EP_FILE) {
transfer_file_data(conn);
} else if (conn->endpoint_type == EP_CGI) {
......@@ -3730,10 +3791,14 @@ unsigned int mg_poll_server(struct mg_server *server, int milliseconds) {
read_from_cgi(conn);
}
#endif
if (FD_ISSET(conn->client_sock, &write_set) &&
!(conn->flags & CONN_BUFFER)) {
conn->last_activity_time = current_time;
write_to_socket(conn);
if (FD_ISSET(conn->client_sock, &write_set)) {
if (conn->endpoint_type == EP_CLIENT &&
(conn->flags & CONN_CONNECTING)) {
read_from_socket(conn);
} else if (!(conn->flags & CONN_BUFFER)) {
conn->last_activity_time = current_time;
write_to_socket(conn);
}
}
}
}
......@@ -3767,7 +3832,7 @@ void mg_destroy_server(struct mg_server **server) {
closesocket((*server)->ctl[0]);
closesocket((*server)->ctl[1]);
LINKED_LIST_FOREACH(&(*server)->active_connections, lp, tmp) {
free(LINKED_LIST_ENTRY(lp, struct connection, link));
close_conn(LINKED_LIST_ENTRY(lp, struct connection, link));
}
LINKED_LIST_FOREACH(&(*server)->uri_handlers, lp, tmp) {
free(LINKED_LIST_ENTRY(lp, struct uri_handler, link)->uri);
......@@ -3980,7 +4045,7 @@ const char *mg_set_option(struct mg_server *server, const char *name,
free(server->config_options[ind]);
}
server->config_options[ind] = mg_strdup(value);
DBG(("%s => %s", name, value));
DBG(("%s [%s]", name, value));
if (ind == LISTENING_PORT) {
if (server->listening_sock != INVALID_SOCKET) {
......
......@@ -46,11 +46,11 @@ struct mg_connection {
} http_headers[30];
char *content; // POST (or websocket message) data, or NULL
int content_len; // content length
long int content_len; // content length
int is_websocket; // Connection is a websocket connection
int status_code; // HTTP status code for HTTP error handler
unsigned char wsbits; // First byte of the websocket frame
int wsbits; // First byte of the websocket frame
void *server_param; // Parameter passed to mg_add_uri_handler()
void *connection_param; // Placeholder for connection-specific data
};
......@@ -101,6 +101,14 @@ char *mg_md5(char buf[33], ...);
int mg_authorize_digest(struct mg_connection *c, FILE *fp);
void mg_send_digest_auth_request(struct mg_connection *conn);
// HTTP client interface
enum {
MG_CONNECT_SUCCESS, MG_CONNECT_FAILURE,
MG_DOWNLOAD_SUCCESS, MG_DOWNLOAD_FAILURE
};
int mg_connect(struct mg_server *server, const char *host, int port,
mg_handler_t handler, void *param);
#ifdef __cplusplus
}
#endif // __cplusplus
......
// Unit test for the mongoose web server.
// g++ -W -Wall -pedantic unit_test.c -o t && ./t
// g++ -W -Wall -pedantic -g unit_test.c && ./a.out
#define USE_WEBSOCKET
......@@ -28,6 +28,7 @@
static int static_num_tests = 0;
#if 0
// Connects to host:port, and sends formatted request to it. Returns
// malloc-ed reply and reply length, or NULL on error. Reply contains
// everything including headers, not just the message body.
......@@ -73,6 +74,7 @@ static char *wget(const char *host, int port, int *len, const char *fmt, ...) {
return reply;
}
#endif
static char *read_file(const char *path, int *size) {
FILE *fp;
......@@ -361,78 +363,42 @@ static const char *test_next_option(void) {
static int cb1(struct mg_connection *conn) {
// We're not sending HTTP headers here, to make testing easier
//mg_printf(conn, "HTTP/1.0 200 OK\r\n\r\n%s %s %s",
mg_printf(conn, "%s %s %s",
conn->server_param == NULL ? "?" : (char *) conn->server_param,
conn->connection_param == NULL ? "?" : "!", conn->remote_ip);
return 1;
}
static const char *test_regular_file(void) {
static const char *fname = "mongoose.c";
int reply_len, file_len;
char *reply, *file_data;
file_stat_t st;
ASSERT(stat(fname, &st) == 0);
ASSERT(st.st_size > 0);
ASSERT((file_data = read_file(fname, &file_len)) != NULL);
ASSERT(file_len == st.st_size);
reply = wget("127.0.0.1", atoi(HTTP_PORT), &reply_len,
"GET /%s.c HTTP/1.0\r\n\r\n", fname);
ASSERT(reply != NULL);
ASSERT(reply_len > 0);
// TODO(lsm): test headers and content
free(reply);
free(file_data);
return NULL;
}
static const char *test_server_param(void) {
int reply_len;
char *reply;
reply = wget("127.0.0.1", atoi(HTTP_PORT), &reply_len, "%s",
"GET /cb1 HTTP/1.0\r\n\r\n");
ASSERT(reply != NULL);
ASSERT(reply_len == 15);
// cb1() does not send HTTP headers
ASSERT(memcmp(reply, "foo ? 127.0.0.1", 5) == 0);
//printf("%d [%.*s]\n", reply_len, reply_len, reply);
free(reply);
return NULL;
}
static int error_handler(struct mg_connection *conn) {
mg_printf(conn, "error: %d", conn->status_code);
mg_printf(conn, "HTTP/1.0 404 NF\r\n\r\nERR: %d", conn->status_code);
return 1;
}
static const char *test_error_handler(void) {
int reply_len;
char *reply;
reply = wget("127.0.0.1", atoi(HTTP_PORT), &reply_len, "%s",
"GET /non_exist HTTP/1.0\r\n\r\n");
ASSERT(reply != NULL);
ASSERT(reply_len == 10);
ASSERT(memcmp(reply, "error: 404", 10) == 0);
free(reply);
return NULL;
static int ts1(struct mg_connection *conn) {
if (conn->status_code == MG_CONNECT_SUCCESS) {
mg_printf(conn, "%s", "GET /cb1 HTTP/1.0\r\n\r\n");
return 0;
} else if (conn->status_code == MG_DOWNLOAD_SUCCESS) {
sprintf((char *) conn->connection_param, "%.*s",
(int) conn->content_len, conn->content);
}
return 1;
}
static void *server_thread(void *param) {
int i;
for (i = 0; i < 10; i++) mg_poll_server((struct mg_server *) param, 1);
return NULL;
static int ts2(struct mg_connection *conn) {
if (conn->status_code == MG_CONNECT_SUCCESS) {
mg_printf(conn, "%s", "GET /non_exist HTTP/1.0\r\n\r\n");
return 0;
} else if (conn->status_code == MG_DOWNLOAD_SUCCESS) {
sprintf((char *) conn->connection_param, "%s %.*s",
conn->uri, (int) conn->content_len, conn->content);
}
return 1;
}
static const char *test_server(void) {
char buf1[100] = "", buf2[100] = "";
struct mg_server *server = mg_create_server((void *) "foo");
ASSERT(server != NULL);
......@@ -440,34 +406,78 @@ static const char *test_server(void) {
ASSERT(mg_set_option(server, "document_root", ".") == NULL);
mg_add_uri_handler(server, "/cb1", cb1);
mg_set_http_error_handler(server, error_handler);
mg_start_thread(server_thread, server);
RUN_TEST(test_regular_file);
RUN_TEST(test_server_param);
RUN_TEST(test_error_handler);
// TODO(lsm): come up with a better way of thread sync
sleep(1);
ASSERT(mg_connect(server, "127.0.0.1", atoi(HTTP_PORT), ts1, buf1) == 1);
ASSERT(mg_connect(server, "127.0.0.1", atoi(HTTP_PORT), ts2, buf2) == 1);
ASSERT(strcmp(static_config_options[URL_REWRITES * 2], "url_rewrites") == 0);
{ int i; for (i = 0; i < 50; i++) mg_poll_server(server, 0); }
ASSERT(strcmp(buf1, "foo ? 127.0.0.1") == 0);
ASSERT(strcmp(buf2, "404 ERR: 404") == 0);
ASSERT(strcmp(static_config_options[URL_REWRITES * 2], "url_rewrites") == 0);
mg_destroy_server(&server);
ASSERT(server == NULL);
return NULL;
}
// This http client sends two requests using one connection
static int cb2(struct mg_connection *conn) {
(void) conn;
return 0;
const char *file1 = "mongoose.h", *file2 = "mongoose.c", *file_name = file2;
char *buf = (char *) conn->connection_param, *file_data;
int file_len, ret_val = 1;
switch (conn->status_code) {
case MG_CONNECT_SUCCESS:
mg_printf(conn, "GET /%s HTTP/1.1\r\n\r\n", file1);
strcat(buf, "a");
ret_val = 0;
break;
case MG_CONNECT_FAILURE: strcat(buf, "b"); break;
case MG_DOWNLOAD_FAILURE: strcat(buf, "c"); break;
case MG_DOWNLOAD_SUCCESS:
if (!strcmp(buf, "a")) {
// Send second request
mg_printf(conn, "GET /%s HTTP/1.1\r\n\r\n", file2);
file_name = file1;
}
if ((file_data = read_file(file_name, &file_len)) != NULL) {
if (file_len == conn->content_len &&
memcmp(file_data, conn->content, file_len) == 0) {
strcat(buf, "d");
} else {
strcat(buf, "f");
}
free(file_data);
}
ret_val = !strcmp(buf, "ad") ? 0 : 1;
break;
default: strcat(buf, "e"); break;
}
return ret_val;
}
static int cb3(struct mg_connection *conn) {
sprintf((char *) conn->connection_param, "%d", conn->status_code);
return 1;
}
static const char *test_mg_connect(void) {
char buf[1000];
struct mg_server *server = mg_create_server(buf);
//mg_connect(server, "127.0.0.1", atoi(HTTP_PORT), cb2);
char buf2[40] = "", buf3[40] = "";
struct mg_server *server = mg_create_server(NULL);
ASSERT(mg_set_option(server, "listening_port", LISTENING_ADDR) == NULL);
ASSERT(mg_set_option(server, "document_root", ".") == NULL);
mg_add_uri_handler(server, "/cb1", cb2);
ASSERT(mg_connect(server, "", 0, NULL, NULL) == 0);
ASSERT(mg_connect(server, "127.0.0.1", atoi(HTTP_PORT), cb2, buf2) == 1);
ASSERT(mg_connect(server, "127.0.0.1", 1, cb3, buf3) == 1);
{ int i; for (i = 0; i < 50; i++) mg_poll_server(server, 0); }
ASSERT(strcmp(buf2, "add") == 0);
ASSERT(strcmp(buf3, "1") == 0);
mg_destroy_server(&server);
return NULL;
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment