1 // This file is part of the Civetweb project, http://code.google.com/p/civetweb
2 // It implements an online chat server. For more details,
3 // see the documentation on the project web site.
4 // To test the application,
5 // 1. type "make" in the directory where this file lives
6 // 2. point your browser to http://127.0.0.1:8081
18 #define MAX_USER_LEN 20
19 #define MAX_MESSAGE_LEN 100
20 #define MAX_MESSAGES 5
21 #define MAX_SESSIONS 2
22 #define SESSION_TTL 120
24 static const char *authorize_url
= "/authorize";
25 static const char *login_url
= "/login.html";
26 static const char *ajax_reply_start
=
29 "Content-Type: application/x-javascript\r\n"
32 // Describes single message sent to a chat. If user is empty (0 length),
33 // the message is then originated from the server itself.
35 long id
; // Message ID
36 char user
[MAX_USER_LEN
]; // User that have sent the message
37 char text
[MAX_MESSAGE_LEN
]; // Message text
38 time_t timestamp
; // Message timestamp, UTC
41 // Describes web session.
43 char session_id
[33]; // Session ID, must be unique
44 char random
[20]; // Random data used for extra user validation
45 char user
[MAX_USER_LEN
]; // Authenticated user
46 time_t expire
; // Expiration timestamp, UTC
49 static struct message messages
[MAX_MESSAGES
]; // Ringbuffer for messages
50 static struct session sessions
[MAX_SESSIONS
]; // Current sessions
51 static long last_message_id
;
53 // Protects messages, sessions, last_message_id
54 static pthread_rwlock_t rwlock
= PTHREAD_RWLOCK_INITIALIZER
;
56 // Get session object for the connection. Caller must hold the lock.
57 static struct session
*get_session(const struct mg_connection
*conn
)
60 const char *cookie
= mg_get_header(conn
, "Cookie");
62 time_t now
= time(NULL
);
63 mg_get_cookie(cookie
, "session", session_id
, sizeof(session_id
));
64 for (i
= 0; i
< MAX_SESSIONS
; i
++) {
65 if (sessions
[i
].expire
!= 0 &&
66 sessions
[i
].expire
> now
&&
67 strcmp(sessions
[i
].session_id
, session_id
) == 0) {
71 return i
== MAX_SESSIONS
? NULL
: &sessions
[i
];
74 static void get_qsvar(const struct mg_request_info
*request_info
,
75 const char *name
, char *dst
, size_t dst_len
)
77 const char *qs
= request_info
->query_string
;
78 mg_get_var(qs
, strlen(qs
== NULL
? "" : qs
), name
, dst
, dst_len
);
81 // Get a get of messages with IDs greater than last_id and transform them
82 // into a JSON string. Return that string to the caller. The string is
83 // dynamically allocated, caller must free it. If there are no messages,
85 static char *messages_to_json(long last_id
)
87 const struct message
*message
;
89 char buf
[sizeof(messages
)]; // Large enough to hold all messages
91 // Read-lock the ringbuffer. Loop over all messages, making a JSON string.
92 pthread_rwlock_rdlock(&rwlock
);
94 max_msgs
= sizeof(messages
) / sizeof(messages
[0]);
95 // If client is too far behind, return all messages.
96 if (last_message_id
- last_id
> max_msgs
) {
97 last_id
= last_message_id
- max_msgs
;
99 for (; last_id
< last_message_id
; last_id
++) {
100 message
= &messages
[last_id
% max_msgs
];
101 if (message
->timestamp
== 0) {
104 // buf is allocated on stack and hopefully is large enough to hold all
105 // messages (it may be too small if the ringbuffer is full and all
106 // messages are large. in this case asserts will trigger).
107 len
+= snprintf(buf
+ len
, sizeof(buf
) - len
,
108 "{user: '%s', text: '%s', timestamp: %lu, id: %ld},",
109 message
->user
, message
->text
, message
->timestamp
, message
->id
);
111 assert((size_t) len
< sizeof(buf
));
113 pthread_rwlock_unlock(&rwlock
);
115 return len
== 0 ? NULL
: strdup(buf
);
118 // If "callback" param is present in query string, this is JSONP call.
119 // Return 1 in this case, or 0 if "callback" is not specified.
120 // Wrap an output in Javascript function call.
121 static int handle_jsonp(struct mg_connection
*conn
,
122 const struct mg_request_info
*request_info
)
126 get_qsvar(request_info
, "callback", cb
, sizeof(cb
));
128 mg_printf(conn
, "%s(", cb
);
131 return cb
[0] == '\0' ? 0 : 1;
134 // A handler for the /ajax/get_messages endpoint.
135 // Return a list of messages with ID greater than requested.
136 static void ajax_get_messages(struct mg_connection
*conn
,
137 const struct mg_request_info
*request_info
)
139 char last_id
[32], *json
;
142 mg_printf(conn
, "%s", ajax_reply_start
);
143 is_jsonp
= handle_jsonp(conn
, request_info
);
145 get_qsvar(request_info
, "last_id", last_id
, sizeof(last_id
));
146 if ((json
= messages_to_json(strtoul(last_id
, NULL
, 10))) != NULL
) {
147 mg_printf(conn
, "[%s]", json
);
152 mg_printf(conn
, "%s", ")");
156 // Allocate new message. Caller must hold the lock.
157 static struct message
*new_message(void)
159 static int size
= sizeof(messages
) / sizeof(messages
[0]);
160 struct message
*message
= &messages
[last_message_id
% size
];
161 message
->id
= last_message_id
++;
162 message
->timestamp
= time(0);
166 static void my_strlcpy(char *dst
, const char *src
, size_t len
)
168 strncpy(dst
, src
, len
);
172 // A handler for the /ajax/send_message endpoint.
173 static void ajax_send_message(struct mg_connection
*conn
,
174 const struct mg_request_info
*request_info
)
176 struct message
*message
;
177 struct session
*session
;
178 char text
[sizeof(message
->text
) - 1];
181 mg_printf(conn
, "%s", ajax_reply_start
);
182 is_jsonp
= handle_jsonp(conn
, request_info
);
184 get_qsvar(request_info
, "text", text
, sizeof(text
));
185 if (text
[0] != '\0') {
186 // We have a message to store. Write-lock the ringbuffer,
187 // grab the next message and copy data into it.
188 pthread_rwlock_wrlock(&rwlock
);
189 message
= new_message();
190 // TODO(lsm): JSON-encode all text strings
191 session
= get_session(conn
);
192 assert(session
!= NULL
);
193 my_strlcpy(message
->text
, text
, sizeof(text
));
194 my_strlcpy(message
->user
, session
->user
, sizeof(message
->user
));
195 pthread_rwlock_unlock(&rwlock
);
198 mg_printf(conn
, "%s", text
[0] == '\0' ? "false" : "true");
201 mg_printf(conn
, "%s", ")");
205 // Redirect user to the login form. In the cookie, store the original URL
206 // we came from, so that after the authorization we could redirect back.
207 static void redirect_to_login(struct mg_connection
*conn
,
208 const struct mg_request_info
*request_info
)
210 mg_printf(conn
, "HTTP/1.1 302 Found\r\n"
211 "Set-Cookie: original_url=%s\r\n"
212 "Location: %s\r\n\r\n",
213 request_info
->uri
, login_url
);
216 // Return 1 if username/password is allowed, 0 otherwise.
217 static int check_password(const char *user
, const char *password
)
219 // In production environment we should ask an authentication system
220 // to authenticate the user.
221 // Here however we do trivial check that user and password are not empty
222 return (user
[0] && password
[0]);
225 // Allocate new session object
226 static struct session
*new_session(void)
229 time_t now
= time(NULL
);
230 pthread_rwlock_wrlock(&rwlock
);
231 for (i
= 0; i
< MAX_SESSIONS
; i
++) {
232 if (sessions
[i
].expire
== 0 || sessions
[i
].expire
< now
) {
233 sessions
[i
].expire
= time(0) + SESSION_TTL
;
237 pthread_rwlock_unlock(&rwlock
);
238 return i
== MAX_SESSIONS
? NULL
: &sessions
[i
];
241 // Generate session ID. buf must be 33 bytes in size.
242 // Note that it is easy to steal session cookies by sniffing traffic.
243 // This is why all communication must be SSL-ed.
244 static void generate_session_id(char *buf
, const char *random
,
247 mg_md5(buf
, random
, user
, NULL
);
250 static void send_server_message(const char *fmt
, ...)
253 struct message
*message
;
255 pthread_rwlock_wrlock(&rwlock
);
256 message
= new_message();
257 message
->user
[0] = '\0'; // Empty user indicates server message
259 vsnprintf(message
->text
, sizeof(message
->text
), fmt
, ap
);
262 pthread_rwlock_unlock(&rwlock
);
265 // A handler for the /authorize endpoint.
266 // Login page form sends user name and password to this endpoint.
267 static void authorize(struct mg_connection
*conn
,
268 const struct mg_request_info
*request_info
)
270 char user
[MAX_USER_LEN
], password
[MAX_USER_LEN
];
271 struct session
*session
;
273 // Fetch user name and password.
274 get_qsvar(request_info
, "user", user
, sizeof(user
));
275 get_qsvar(request_info
, "password", password
, sizeof(password
));
277 if (check_password(user
, password
) && (session
= new_session()) != NULL
) {
278 // Authentication success:
279 // 1. create new session
280 // 2. set session ID token in the cookie
281 // 3. remove original_url from the cookie - not needed anymore
282 // 4. redirect client back to the original URL
284 // The most secure way is to stay HTTPS all the time. However, just to
285 // show the technique, we redirect to HTTP after the successful
286 // authentication. The danger of doing this is that session cookie can
287 // be stolen and an attacker may impersonate the user.
288 // Secure application must use HTTPS all the time.
289 my_strlcpy(session
->user
, user
, sizeof(session
->user
));
290 snprintf(session
->random
, sizeof(session
->random
), "%d", rand());
291 generate_session_id(session
->session_id
, session
->random
, session
->user
);
292 send_server_message("<%s> joined", session
->user
);
293 mg_printf(conn
, "HTTP/1.1 302 Found\r\n"
294 "Set-Cookie: session=%s; max-age=3600; http-only\r\n" // Session ID
295 "Set-Cookie: user=%s\r\n" // Set user, needed by Javascript code
296 "Set-Cookie: original_url=/; max-age=0\r\n" // Delete original_url
297 "Location: /\r\n\r\n",
298 session
->session_id
, session
->user
);
300 // Authentication failure, redirect to login.
301 redirect_to_login(conn
, request_info
);
305 // Return 1 if request is authorized, 0 otherwise.
306 static int is_authorized(const struct mg_connection
*conn
,
307 const struct mg_request_info
*request_info
)
309 struct session
*session
;
313 // Always authorize accesses to login page and to authorize URI
314 if (!strcmp(request_info
->uri
, login_url
) ||
315 !strcmp(request_info
->uri
, authorize_url
)) {
319 pthread_rwlock_rdlock(&rwlock
);
320 if ((session
= get_session(conn
)) != NULL
) {
321 generate_session_id(valid_id
, session
->random
, session
->user
);
322 if (strcmp(valid_id
, session
->session_id
) == 0) {
323 session
->expire
= time(0) + SESSION_TTL
;
327 pthread_rwlock_unlock(&rwlock
);
332 static void redirect_to_ssl(struct mg_connection
*conn
,
333 const struct mg_request_info
*request_info
)
335 const char *p
, *host
= mg_get_header(conn
, "Host");
336 if (host
!= NULL
&& (p
= strchr(host
, ':')) != NULL
) {
337 mg_printf(conn
, "HTTP/1.1 302 Found\r\n"
338 "Location: https://%.*s:8082/%s:8082\r\n\r\n",
339 (int) (p
- host
), host
, request_info
->uri
);
341 mg_printf(conn
, "%s", "HTTP/1.1 500 Error\r\n\r\nHost: header is not set");
345 static int begin_request_handler(struct mg_connection
*conn
)
347 const struct mg_request_info
*request_info
= mg_get_request_info(conn
);
350 if (!request_info
->is_ssl
) {
351 redirect_to_ssl(conn
, request_info
);
352 } else if (!is_authorized(conn
, request_info
)) {
353 redirect_to_login(conn
, request_info
);
354 } else if (strcmp(request_info
->uri
, authorize_url
) == 0) {
355 authorize(conn
, request_info
);
356 } else if (strcmp(request_info
->uri
, "/ajax/get_messages") == 0) {
357 ajax_get_messages(conn
, request_info
);
358 } else if (strcmp(request_info
->uri
, "/ajax/send_message") == 0) {
359 ajax_send_message(conn
, request_info
);
361 // No suitable handler found, mark as not processed. Civetweb will
362 // try to serve the request.
368 static const char *options
[] = {
369 "document_root", "html",
370 "listening_ports", "8081,8082s",
371 "ssl_certificate", "ssl_cert.pem",
378 struct mg_callbacks callbacks
;
379 struct mg_context
*ctx
;
381 // Initialize random number generator. It will be used later on for
382 // the session identifier creation.
383 srand((unsigned) time(0));
385 // Setup and start Civetweb
386 memset(&callbacks
, 0, sizeof(callbacks
));
387 callbacks
.begin_request
= begin_request_handler
;
388 if ((ctx
= mg_start(&callbacks
, NULL
, options
)) == NULL
) {
389 printf("%s\n", "Cannot start chat server, fatal exit");
393 // Wait until enter is pressed, then exit
394 printf("Chat server started on ports %s, press enter to quit.\n",
395 mg_get_option(ctx
, "listening_ports"));
398 printf("%s\n", "Chat server stopped.");