From b33755c9627805de67cf5a82802d2e475d7909d5 Mon Sep 17 00:00:00 2001 From: Sergey Lyubka Date: Wed, 5 May 2010 00:36:23 +0100 Subject: [PATCH] sessions implemented (simplistically). security expert review would be nice to have. --- examples/chat.c | 141 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 108 insertions(+), 33 deletions(-) diff --git a/examples/chat.c b/examples/chat.c index bb2ee387..edf8705f 100644 --- a/examples/chat.c +++ b/examples/chat.c @@ -1,15 +1,13 @@ /* * This file is part of the Mongoose project, http://code.google.com/p/mongoose * It implements an online chat server. For more details, - * see the documentation on project page. - * To start the server, - * a) type "make" in the directory where this file lives - * b) point your browser to http://127.0.0.1:8081 + * see the documentation on the project web site. + * To test the application, + * 1. type "make" in the directory where this file lives + * 2. point your browser to http://127.0.0.1:8081 * * NOTE(lsm): this file follows Google style, not BSD style as the rest of * Mongoose code. - * - * $Id: chat.c 513 2010-05-03 11:06:08Z valenok $ */ #include @@ -25,13 +23,13 @@ #define MAX_MESSAGE_LEN 100 #define MAX_MESSAGES 5 #define MAX_SESSIONS 2 +#define SESSION_TTL 120 static const char *login_url = "/login.html"; static const char *authorize_url = "/authorize"; static const char *web_root = "./html"; static const char *http_ports = "8081,8082s"; static const char *ssl_certificate = "ssl_cert.pem"; - static const char *ajax_reply_start = "HTTP/1.1 200 OK\r\n" "Cache: no-cache\r\n" @@ -41,17 +39,18 @@ static const char *ajax_reply_start = // Describes single message sent to a chat. If user is empty (0 length), // the message is then originated from the server itself. struct message { - long id; - char user[MAX_USER_LEN]; - char text[MAX_MESSAGE_LEN]; - time_t utc_timestamp; + long id; // Message ID + char user[MAX_USER_LEN]; // User that have sent the message + char text[MAX_MESSAGE_LEN]; // Message text + time_t timestamp; // Message timestamp, UTC }; // Describes web session. struct session { - char session_id[33]; - char authenticated_user[MAX_USER_LEN]; - time_t expiration_timestamp_utc; + char session_id[33]; // Session ID, must be unique + char random[20]; // Random data used for extra user validation + char user[MAX_USER_LEN]; // Authenticated user + time_t expire; // Expiration timestamp, UTC }; static struct message messages[MAX_MESSAGES]; // Ringbuffer for messages @@ -80,7 +79,7 @@ static char *messages_to_json(long last_id) { } for (; last_id < last_message_id; last_id++) { message = &messages[last_id % max_msgs]; - if (message->utc_timestamp == 0) { + if (message->timestamp == 0) { break; } // buf is allocated on stack and hopefully is large enough to hold all @@ -88,7 +87,7 @@ static char *messages_to_json(long last_id) { // messages are large. in this case asserts will trigger). len += snprintf(buf + len, sizeof(buf) - len, "{user: '%s', text: '%s', timestamp: %lu, id: %lu},", - message->user, message->text, message->utc_timestamp, message->id); + message->user, message->text, message->timestamp, message->id); assert(len > 0); assert((size_t) len < sizeof(buf)); } @@ -153,7 +152,7 @@ static void ajax_send_message(struct mg_connection *conn, // TODO(lsm): JSON-encode all text strings strncpy(message->text, text, sizeof(text)); strncpy(message->user, "joe", sizeof(message->user)); - message->utc_timestamp = time(0); + message->timestamp = time(0); message->id = last_message_id++; pthread_rwlock_unlock(&rwlock); } @@ -169,43 +168,90 @@ static void ajax_send_message(struct mg_connection *conn, // we came from, so that after the authorization we could redirect back. static void redirect_to_login(struct mg_connection *conn, const struct mg_request_info *request_info) { + const char *host; + + host = mg_get_header(conn, "Host"); mg_printf(conn, "HTTP/1.1 302 Found\r\n" - "Set-Cookie: original_url=%s\r\n" - "Location: %s\r\n\r\n", request_info->uri, login_url); + "Set-Cookie: original_url=%s://%s%s\r\n" + "Location: %s\r\n\r\n", + request_info->is_ssl ? "https" : "http", + host ? host : "127.0.0.1", + request_info->uri, + login_url); } // Return 1 if username/password is allowed, 0 otherwise. static int check_password(const char *user, const char *password) { // In production environment we should ask an authentication system // to authenticate the user. - // Here however we do trivial check: if username == password, allow. - return (strcmp(user, password) == 0 ? 1 : 0); + // Here however we do trivial check that user and password are not empty + return (user[0] && password[0]); +} + +// Allocate new session object +static struct session *new_session(void) { + int i; + time_t now = time(NULL); + pthread_rwlock_wrlock(&rwlock); + for (i = 0; i < MAX_SESSIONS; i++) { + if (sessions[i].expire == 0 || sessions[i].expire < now) { + sessions[i].expire = time(0) + SESSION_TTL; + break; + } + } + pthread_rwlock_unlock(&rwlock); + return i == MAX_SESSIONS ? NULL : &sessions[i]; +} + +// Generate session ID. buf must be 33 bytes in size. +static void generate_session_id(char *buf, const char *random, + const char *user, const struct mg_request_info *request_info) { + char remote_ip[20], remote_port[20]; + snprintf(remote_ip, sizeof(remote_ip), "%ld", request_info->remote_ip); + snprintf(remote_port, sizeof(remote_port), "%d", request_info->remote_port); + mg_md5(buf, random, user, remote_port, remote_ip, NULL); } // A handler for the /authorize endpoint. // Login page form sends user name and password to this endpoint. static void authorize(struct mg_connection *conn, const struct mg_request_info *request_info) { - char user[20], password[20], original_url[200]; + char user[MAX_USER_LEN], password[MAX_USER_LEN], original_url[200]; + struct session *session; // Fetch user name and password. mg_get_qsvar(request_info, "user", user, sizeof(user)); mg_get_qsvar(request_info, "password", password, sizeof(password)); mg_get_cookie(conn, "original_url", original_url, sizeof(original_url)); - if (user[0] && password[0] && check_password(user, password)) { + if (check_password(user, password) && (session = new_session()) != NULL) { // Authentication success: // 1. create new session // 2. set session ID token in the cookie // 3. remove original_url from the cookie - not needed anymore // 4. redirect client back to the original URL - // TODO(lsm): implement sessions. + // + // The most secure way is to stay HTTPS all the time. However, just to + // show the technique, we redirect to HTTP after the successful + // authentication. The danger of doing this is that session cookie can + // be stolen and an attacker may impersonate the user. + // Secure application must use HTTPS all the time. + strlcpy(session->user, user, sizeof(session->user)); + snprintf(session->random, sizeof(session->random), "%d", rand()); + generate_session_id(session->session_id, session->random, + session->user, request_info); + printf("New session, user: %s, id: %s, redirecting to %s\n", + session->user, session->session_id, original_url); mg_printf(conn, "HTTP/1.1 302 Found\r\n" - "Set-Cookie: sid=1234; max-age=2h; http-only\r\n" // Set session ID - "Set-Cookie: original_url=/; max_age=0\r\n" // Delete original_url - "Location: %s\r\n\r\n", original_url[0] == '\0' ? "/" : original_url); + "Set-Cookie: session=%s; max-age=3600; http-only\r\n" // Session ID + "Set-Cookie: user=%s\r\n" // Set user, needed by Javascript code + "Set-Cookie: original_url=/; max-age=0\r\n" // Delete original_url + "Location: %s\r\n\r\n", + session->session_id, + session->user, + original_url[0] == '\0' ? "/" : original_url); } else { - // Authentication failure, redirect to login again. + // Authentication failure, redirect to login. redirect_to_login(conn, request_info); } } @@ -213,8 +259,33 @@ static void authorize(struct mg_connection *conn, // Return 1 if request is authorized, 0 otherwise. static int is_authorized(const struct mg_connection *conn, const struct mg_request_info *request_info) { - // TODO(lsm): implement this part: fetch session ID from the cookie. - return 0; + char valid_id[33], received_id[33]; + int i, authorized = 0; + + mg_get_cookie(conn, "session", received_id, sizeof(received_id)); + if (received_id[0] != '\0') { + pthread_rwlock_rdlock(&rwlock); + for (i = 0; i < MAX_SESSIONS; i++) { + if (sessions[i].expire != 0 && + sessions[i].expire > time(NULL) && + strcmp(sessions[i].session_id, received_id) == 0) { + break; + } + } + if (i < MAX_SESSIONS) { + generate_session_id(valid_id, sessions[i].random, + sessions[i].user, request_info); + if (strcmp(valid_id, received_id) == 0) { + sessions[i].expire = time(0) + SESSION_TTL; + authorized = 1; + } + } + pthread_rwlock_unlock(&rwlock); + } + printf("session: %s, uri: %s, authorized: %s, cookie: %s\n", + received_id, request_info->uri, authorized ? "yes" : "no", mg_get_header(conn, "Cookie")); + + return authorized; } // Return 1 if authorization is required for requested URL, 0 otherwise. @@ -246,18 +317,22 @@ static enum mg_error_t process_request(struct mg_connection *conn, return processed; } -int main(int argc, char *argv[]) { +int main(void) { struct mg_context *ctx; - ctx = mg_start(); + // Initialize random number generator. It will be used later on for + // the session identifier creation. + srand((unsigned) time(0)); + // Start and setup Mongoose + ctx = mg_start(); mg_set_option(ctx, "root", web_root); mg_set_option(ctx, "ssl_cert", ssl_certificate); // Must be set before ports mg_set_option(ctx, "ports", http_ports); mg_set_option(ctx, "dir_list", "no"); // Disable directory listing - mg_set_callback(ctx, MG_EVENT_NEW_REQUEST, &process_request); + // Wait until enter is pressed, then exit printf("Chat server started on ports %s, press enter to quit.\n", http_ports); getchar(); mg_stop(ctx);