Browse Source

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

Sergey Lyubka 15 years ago
parent
commit
b33755c962
1 changed files with 108 additions and 33 deletions
  1. 108 33
      examples/chat.c

+ 108 - 33
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 <stdio.h>
@@ -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);