Commit b33755c9 authored by Sergey Lyubka's avatar Sergey Lyubka

sessions implemented (simplistically). security expert review would be nice to have.

parent 4b0424d0
/* /*
* This file is part of the Mongoose project, http://code.google.com/p/mongoose * This file is part of the Mongoose project, http://code.google.com/p/mongoose
* It implements an online chat server. For more details, * It implements an online chat server. For more details,
* see the documentation on project page. * see the documentation on the project web site.
* To start the server, * To test the application,
* a) type "make" in the directory where this file lives * 1. type "make" in the directory where this file lives
* b) point your browser to http://127.0.0.1:8081 * 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 * NOTE(lsm): this file follows Google style, not BSD style as the rest of
* Mongoose code. * Mongoose code.
*
* $Id: chat.c 513 2010-05-03 11:06:08Z valenok $
*/ */
#include <stdio.h> #include <stdio.h>
...@@ -25,13 +23,13 @@ ...@@ -25,13 +23,13 @@
#define MAX_MESSAGE_LEN 100 #define MAX_MESSAGE_LEN 100
#define MAX_MESSAGES 5 #define MAX_MESSAGES 5
#define MAX_SESSIONS 2 #define MAX_SESSIONS 2
#define SESSION_TTL 120
static const char *login_url = "/login.html"; static const char *login_url = "/login.html";
static const char *authorize_url = "/authorize"; static const char *authorize_url = "/authorize";
static const char *web_root = "./html"; static const char *web_root = "./html";
static const char *http_ports = "8081,8082s"; static const char *http_ports = "8081,8082s";
static const char *ssl_certificate = "ssl_cert.pem"; static const char *ssl_certificate = "ssl_cert.pem";
static const char *ajax_reply_start = static const char *ajax_reply_start =
"HTTP/1.1 200 OK\r\n" "HTTP/1.1 200 OK\r\n"
"Cache: no-cache\r\n" "Cache: no-cache\r\n"
...@@ -41,17 +39,18 @@ static const char *ajax_reply_start = ...@@ -41,17 +39,18 @@ static const char *ajax_reply_start =
// Describes single message sent to a chat. If user is empty (0 length), // Describes single message sent to a chat. If user is empty (0 length),
// the message is then originated from the server itself. // the message is then originated from the server itself.
struct message { struct message {
long id; long id; // Message ID
char user[MAX_USER_LEN]; char user[MAX_USER_LEN]; // User that have sent the message
char text[MAX_MESSAGE_LEN]; char text[MAX_MESSAGE_LEN]; // Message text
time_t utc_timestamp; time_t timestamp; // Message timestamp, UTC
}; };
// Describes web session. // Describes web session.
struct session { struct session {
char session_id[33]; char session_id[33]; // Session ID, must be unique
char authenticated_user[MAX_USER_LEN]; char random[20]; // Random data used for extra user validation
time_t expiration_timestamp_utc; char user[MAX_USER_LEN]; // Authenticated user
time_t expire; // Expiration timestamp, UTC
}; };
static struct message messages[MAX_MESSAGES]; // Ringbuffer for messages static struct message messages[MAX_MESSAGES]; // Ringbuffer for messages
...@@ -80,7 +79,7 @@ static char *messages_to_json(long last_id) { ...@@ -80,7 +79,7 @@ static char *messages_to_json(long last_id) {
} }
for (; last_id < last_message_id; last_id++) { for (; last_id < last_message_id; last_id++) {
message = &messages[last_id % max_msgs]; message = &messages[last_id % max_msgs];
if (message->utc_timestamp == 0) { if (message->timestamp == 0) {
break; break;
} }
// buf is allocated on stack and hopefully is large enough to hold all // 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) { ...@@ -88,7 +87,7 @@ static char *messages_to_json(long last_id) {
// messages are large. in this case asserts will trigger). // messages are large. in this case asserts will trigger).
len += snprintf(buf + len, sizeof(buf) - len, len += snprintf(buf + len, sizeof(buf) - len,
"{user: '%s', text: '%s', timestamp: %lu, id: %lu},", "{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(len > 0);
assert((size_t) len < sizeof(buf)); assert((size_t) len < sizeof(buf));
} }
...@@ -153,7 +152,7 @@ static void ajax_send_message(struct mg_connection *conn, ...@@ -153,7 +152,7 @@ static void ajax_send_message(struct mg_connection *conn,
// TODO(lsm): JSON-encode all text strings // TODO(lsm): JSON-encode all text strings
strncpy(message->text, text, sizeof(text)); strncpy(message->text, text, sizeof(text));
strncpy(message->user, "joe", sizeof(message->user)); strncpy(message->user, "joe", sizeof(message->user));
message->utc_timestamp = time(0); message->timestamp = time(0);
message->id = last_message_id++; message->id = last_message_id++;
pthread_rwlock_unlock(&rwlock); pthread_rwlock_unlock(&rwlock);
} }
...@@ -169,43 +168,90 @@ static void ajax_send_message(struct mg_connection *conn, ...@@ -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. // we came from, so that after the authorization we could redirect back.
static void redirect_to_login(struct mg_connection *conn, static void redirect_to_login(struct mg_connection *conn,
const struct mg_request_info *request_info) { 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" mg_printf(conn, "HTTP/1.1 302 Found\r\n"
"Set-Cookie: original_url=%s\r\n" "Set-Cookie: original_url=%s://%s%s\r\n"
"Location: %s\r\n\r\n", request_info->uri, login_url); "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. // Return 1 if username/password is allowed, 0 otherwise.
static int check_password(const char *user, const char *password) { static int check_password(const char *user, const char *password) {
// In production environment we should ask an authentication system // In production environment we should ask an authentication system
// to authenticate the user. // to authenticate the user.
// Here however we do trivial check: if username == password, allow. // Here however we do trivial check that user and password are not empty
return (strcmp(user, password) == 0 ? 1 : 0); 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. // A handler for the /authorize endpoint.
// Login page form sends user name and password to this endpoint. // Login page form sends user name and password to this endpoint.
static void authorize(struct mg_connection *conn, static void authorize(struct mg_connection *conn,
const struct mg_request_info *request_info) { 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. // Fetch user name and password.
mg_get_qsvar(request_info, "user", user, sizeof(user)); mg_get_qsvar(request_info, "user", user, sizeof(user));
mg_get_qsvar(request_info, "password", password, sizeof(password)); mg_get_qsvar(request_info, "password", password, sizeof(password));
mg_get_cookie(conn, "original_url", original_url, sizeof(original_url)); 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: // Authentication success:
// 1. create new session // 1. create new session
// 2. set session ID token in the cookie // 2. set session ID token in the cookie
// 3. remove original_url from the cookie - not needed anymore // 3. remove original_url from the cookie - not needed anymore
// 4. redirect client back to the original URL // 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" 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: session=%s; max-age=3600; http-only\r\n" // Session ID
"Set-Cookie: original_url=/; max_age=0\r\n" // Delete original_url "Set-Cookie: user=%s\r\n" // Set user, needed by Javascript code
"Location: %s\r\n\r\n", original_url[0] == '\0' ? "/" : original_url); "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 { } else {
// Authentication failure, redirect to login again. // Authentication failure, redirect to login.
redirect_to_login(conn, request_info); redirect_to_login(conn, request_info);
} }
} }
...@@ -213,8 +259,33 @@ static void authorize(struct mg_connection *conn, ...@@ -213,8 +259,33 @@ static void authorize(struct mg_connection *conn,
// Return 1 if request is authorized, 0 otherwise. // Return 1 if request is authorized, 0 otherwise.
static int is_authorized(const struct mg_connection *conn, static int is_authorized(const struct mg_connection *conn,
const struct mg_request_info *request_info) { const struct mg_request_info *request_info) {
// TODO(lsm): implement this part: fetch session ID from the cookie. char valid_id[33], received_id[33];
return 0; 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. // 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, ...@@ -246,18 +317,22 @@ static enum mg_error_t process_request(struct mg_connection *conn,
return processed; return processed;
} }
int main(int argc, char *argv[]) { int main(void) {
struct mg_context *ctx; 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, "root", web_root);
mg_set_option(ctx, "ssl_cert", ssl_certificate); // Must be set before ports mg_set_option(ctx, "ssl_cert", ssl_certificate); // Must be set before ports
mg_set_option(ctx, "ports", http_ports); mg_set_option(ctx, "ports", http_ports);
mg_set_option(ctx, "dir_list", "no"); // Disable directory listing mg_set_option(ctx, "dir_list", "no"); // Disable directory listing
mg_set_callback(ctx, MG_EVENT_NEW_REQUEST, &process_request); 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); printf("Chat server started on ports %s, press enter to quit.\n", http_ports);
getchar(); getchar();
mg_stop(ctx); mg_stop(ctx);
......
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