3 * @brief Implementation of Login related functionality
13 #define LDAP_DEPRECATED 1 // Required to use ldap_simple_bind_s
17 #undef _GNU_SOURCE // HACK to silence compiler warning on redefinition in my_global.h
18 #include <my_global.h>
26 * Attempt to login by searching a MySQL database
27 * @param user - Username
28 * @param pass - Password
29 * @param db_host - Host running the DataBase
30 * @param db_user - User to search the database as
31 * @param db_pass - Password for the database user
32 * @param db_name - Name of the database to use
33 * @param db_table - Table to search in
34 * @returns Privelage level of the user or USER_UNAUTH for failure to authenticate
36 UserType Login_MySQL(const char * user, const char * pass,
37 const char * db_host, const char * db_user, const char * db_pass, const char * db_name, const char * db_table)
39 MYSQL * con = mysql_init(NULL);
42 Log(LOGERR, "mysql_init failed - %s", mysql_error(con));
46 if (mysql_real_connect(con, db_host, db_user, db_pass, NULL, 0, NULL, 0) == NULL)
48 Log(LOGERR, "mysql_real_connect failed - %s", mysql_error(con));
55 // Select the database
56 sprintf(buffer, "USE %s;", db_name);
57 if (mysql_query(con, buffer))
59 Log(LOGERR, "mysql_query failed - %s", mysql_error(con));
64 // Search for the user
65 sprintf(buffer, "SELECT password FROM %s WHERE user_name = \"%s\";", db_table, user);
66 if (mysql_query(con, buffer))
68 Log(LOGERR, "mysql_query failed - %s", mysql_error(con));
74 MYSQL_RES * result = mysql_store_result(con);
77 Log(LOGERR, "mysql_store_result failed - %s", mysql_error(con));
82 int num_fields = mysql_num_fields(result);
85 Log(LOGERR, "The database may be corrupt; %d fields found, expected %d", num_fields, 1);
90 UserType user_type = USER_UNAUTH;
94 if ((row = mysql_fetch_row(result)))
96 if (strcmp(crypt(pass, row[0]), row[0]) == 0)
98 user_type = USER_NORMAL;
101 // There should only be one row. Through a hissy fit if we see any more.
102 if ((row = mysql_fetch_row(result)))
104 Log(LOGERR, "Too many rows found.");
105 user_type = USER_UNAUTH;
110 Log(LOGERR, "No user matching %s", user);
113 //TODO: Handle administrator users somehow better than this
114 // UserCake stores the permission level in a seperate table to the username/password, which is annoying
115 if (user_type != USER_UNAUTH && strcmp(user, "admin") == 0)
117 user_type = USER_ADMIN;
119 mysql_free_result(result);
125 * Attempt to login using a file formatted like /etc/shadow
126 * This is here... because all better options have been exhausted
127 * @param user - The username
128 * @param pass - The password
129 * @param shadow - The file to use
130 * @returns Privelage level of the user or USER_UNAUTH for failure to authenticate
132 UserType Login_Shadow(const char * user, const char * pass, const char * shadow)
134 if (strlen(user) + strlen(pass) >= BUFSIZ-1)
136 Log(LOGERR, "User/Password too long!\n");
140 FILE * f = fopen(shadow, "r");
143 Log(LOGERR,"Can't open %s - %s\n", shadow, strerror(errno));
148 int passwd_index = -1;
150 while (fgets(buffer, BUFSIZ, f) != NULL) // NOTE: Restrict username+password strings to BUFSIZ... what could possibly go wrong?
153 //Log(LOGDEBUG,"Scanning %d: %s", strlen(buffer), buffer);
155 for (int i = 0; i < BUFSIZ-1 && buffer[i] != '\0'; ++i)
157 if (buffer[i] == ':')
165 if (strcmp(user,buffer) == 0)
167 //Log(LOGDEBUG,"User matches! %s\n", buffer);
173 if (passwd_index <= 0)
175 //Log(LOGDEBUG,"No user found matching %s\n", user);
180 for (int i = passwd_index; i < BUFSIZ-1 && buffer[i] != '\0'; ++i)
182 if (buffer[i] == ':')
187 if (buffer[i] == '\n')
190 char * end = buffer+gid_index;
191 UserType user_type = USER_NORMAL;
192 if (gid_index > passwd_index && gid_index < BUFSIZ-1)
194 int gid = strtol(buffer+gid_index, &end,10);
195 Log(LOGDEBUG, "Usertype %d %s", gid, buffer+gid_index);
196 if (*end == '\0' && gid == 0)
198 Log(LOGDEBUG, "Admin");
199 user_type = USER_ADMIN;
203 // Determine the salt
205 int s = 0; int count = 0;
206 for (int i = passwd_index; i < BUFSIZ-1; ++i)
208 salt[s++] = buffer[i];
209 if (salt[s] == '$' && ++count >= 3)
213 // Log(LOGDEBUG,"Salted Entry: %s\n", buffer+passwd_index);
214 // Log(LOGDEBUG,"Salted Attempt: %s\n", crypt(pass, salt));
216 if (strcmp(crypt(pass, salt), buffer+passwd_index) == 0)
224 * Attempt to bind to a LDAP uri
225 * @param uri - The uri
227 * @param pass - The password
228 * @returns An error code according to libldap; LDAP_SUCCESS if everything worked
230 int Login_LDAP_Bind(const char * uri, const char * dn, const char * pass)
232 Log(LOGDEBUG, "Bind to %s with dn %s and pass %s", uri, dn, pass);
234 // Initialise LDAP; prepares to connect to the server
236 int err = ldap_initialize(&ld, uri);
237 if (err != LDAP_SUCCESS || ld == NULL)
239 Log(LOGERR,"ldap_initialize failed - %s (ld = %p)", ldap_err2string(err), ld);
243 // Set the LDAP version...
244 int version = LDAP_VERSION3;
245 err = ldap_set_option(ld, LDAP_OPT_PROTOCOL_VERSION, &version); // specify the version
246 if (err != LDAP_SUCCESS)
248 Log(LOGERR,"ldap_set_option failed - %s", ldap_err2string(err));
252 // Attempt to bind using the supplied credentials.
253 // NOTE: ldap_simple_bind_s is "deprecated" in <ldap.h>, but not listed as such in the man pages :S
254 err = ldap_simple_bind_s(ld, dn, pass);
255 if (err != LDAP_SUCCESS)
257 Log(LOGERR, "ldap_simple_bind_s failed - %s", ldap_err2string(err));
261 Log(LOGDEBUG, "Successfully bound to %s with dn %s", uri, dn);
264 int err2 = ldap_unbind_s(ld);
265 if (err2 != LDAP_SUCCESS)
267 Log(LOGERR, "ldap_unbind_s failed - %s", ldap_err2string(err2));
275 * @param context - The context. The key will be cleared.
276 * @param params - Parameter string, UNUSED
278 void Logout_Handler(FCGIContext * context, char * params)
280 FCGI_ReleaseControl(context);
281 FCGI_SendControlCookie(context, false); //Unset the cookie
282 FCGI_AcceptJSON(context, "Logged out");
287 * Handle a Login Request
288 * @param context - The context
289 * @param params - Parameter string, should contain username and password.
290 * NOTE: Care should be taken when using params, as it is
291 * completely unescaped. Do not log or use it without
294 void Login_Handler(FCGIContext * context, char * params)
296 char * user; // The username supplied through CGI
297 char * pass; // The password supplied through CGI
299 FCGIValue values[] = {
300 {"user", &user, FCGI_REQUIRED(FCGI_STRING_T)},
301 {"pass", &pass, FCGI_REQUIRED(FCGI_STRING_T)},
304 //enum to avoid the use of magic numbers
311 // Fill values appropriately
312 if (!FCGI_ParseRequest(context, params, values, sizeof(values)/sizeof(FCGIValue)))
314 // Error occured; FCGI_RejectJSON already called
318 // Trim leading whitespace
320 for (i = 0; isspace(user[0]) && user[0] != '\0'; ++i, ++user);
322 // Truncate string at first non alphanumeric character
323 for (i = 0; isalnum(user[i]) && user[i] != '\0'; ++i);
327 UserType user_type = USER_UNAUTH;
329 switch (g_options.auth_method)
336 FCGI_RejectJSON(context, "No password supplied.");
340 //TODO: Generate the DN in some sane way
345 // On a simple LDAP server:
346 //int len = sprintf(dn, "uid=%s,%s", user, g_options.auth_options);
349 char * user_group = "Students";
351 user_group = "Staff";
352 int len = sprintf(dn, "cn=%s,ou=%s,%s", user, user_group, g_options.auth_options);
357 FCGI_RejectJSON(context, "DN too long! Recompile with increased BUFSIZ");
361 if (Login_LDAP_Bind(g_options.auth_uri, dn, pass) == LDAP_SUCCESS)
364 user_type = USER_ADMIN;
366 user_type = USER_NORMAL;
372 user_type = Login_Shadow(user, pass, g_options.auth_uri);
377 //WARNING: C string manipulation code approaching!
378 // Non reentrent; uses strsep and modifies g_options.auth_options
379 // If problems happen, try strdup first ...
380 static char * db_opts[] = {"root", "", "users", "uc_users"};
381 static bool db_init_opts = false;
385 db_opts[0] = (char*)g_options.auth_options;
386 for (int i = 1; i < sizeof(db_opts)/sizeof(char*); ++i)
388 char * def = db_opts[i];
389 db_opts[i] = db_opts[i-1];
391 strsep(db_opts+i, ",");
392 if (db_opts[i] == NULL)
398 //Log(LOGDEBUG, "MySQL: user %s pass %s name %s table %s", db_opts[0], db_opts[1], db_opts[2], db_opts[3]);
401 user_type = Login_MySQL(user, pass, g_options.auth_uri, db_opts[0],db_opts[1], db_opts[2], db_opts[3]);
406 Log(LOGWARN, "No authentication!");
407 user_type = USER_ADMIN;
414 if (user_type == USER_UNAUTH)
416 Log(LOGDEBUG, "Authentication failure for %s", user);
417 FCGI_RejectJSONEx(context, STATUS_UNAUTHORIZED, "Authentication failure.");
421 // Try and gain control over the system
422 if (FCGI_LockControl(context, user, user_type))
424 FCGI_EscapeText(context->user_name); //Don't break javascript pls
425 // Give the user a cookie
426 FCGI_SendControlCookie(context, true); //Send the control key
427 FCGI_AcceptJSON(context, "Logged in");
428 Log(LOGDEBUG, "Successful authentication for %s", user);
432 Log(LOGDEBUG, "%s successfully authenticated but system was in use by %s", user, context->user_name);
433 FCGI_RejectJSON(context, "Someone else is already logged in (and you are not an admin)");