|
@@ -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);
|