add fclose to shadow login
[matches/MCTX3420.git] / server / login.c
1 /**
2  * @file login.c
3  * @brief Implementation of Login related functionality
4  */
5
6
7 #include "common.h"
8 #include "options.h"
9 #include <ctype.h>
10 #include <unistd.h>
11
12 // LDAP stuff
13 #define LDAP_DEPRECATED 1 // Required to use ldap_simple_bind_s
14 #include <ldap.h>
15
16 // MySQL stuff
17 #undef _GNU_SOURCE // HACK to silence compiler warning on redefinition in my_global.h
18 #include <my_global.h>
19 #include <mysql.h>
20
21
22
23
24
25 /**
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
35  */
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)
38 {
39         MYSQL * con = mysql_init(NULL);
40         if (con == NULL)
41         {
42                 Log(LOGERR, "mysql_init failed - %s", mysql_error(con));
43                 return USER_UNAUTH;
44         }
45
46         if (mysql_real_connect(con, db_host, db_user, db_pass, NULL, 0, NULL, 0) == NULL)
47         {
48                 Log(LOGERR, "mysql_real_connect failed - %s", mysql_error(con));
49                 mysql_close(con);
50                 return USER_UNAUTH;
51         }
52
53         char buffer[BUFSIZ];
54
55         // Select the database
56         sprintf(buffer, "USE %s;", db_name);
57         if (mysql_query(con, buffer))
58         {
59                 Log(LOGERR, "mysql_query failed - %s", mysql_error(con));
60                 mysql_close(con);
61                 return USER_UNAUTH;
62         }
63
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))
67         {
68                 Log(LOGERR, "mysql_query failed - %s", mysql_error(con));
69                 mysql_close(con);
70                 return USER_UNAUTH;
71         }
72         
73         // Process the result
74         MYSQL_RES * result = mysql_store_result(con);
75         if (result == NULL)
76         {
77                 Log(LOGERR, "mysql_store_result failed - %s", mysql_error(con));
78                 mysql_close(con);
79                 return USER_UNAUTH;
80         }
81
82         int num_fields = mysql_num_fields(result);
83         if (num_fields != 1)
84         {
85                 Log(LOGERR, "The database may be corrupt; %d fields found, expected %d", num_fields, 1);
86                 mysql_close(con);
87                 return USER_UNAUTH;
88         }
89
90         UserType user_type = USER_UNAUTH;
91         MYSQL_ROW row;
92
93         // Get first row
94         if ((row = mysql_fetch_row(result)))
95         {
96                 if (strcmp(crypt(pass, row[0]), row[0]) == 0)
97                 {
98                         user_type = USER_NORMAL;
99                 }
100
101                 // There should only be one row. Through a hissy fit if we see any more.
102                 if ((row = mysql_fetch_row(result)))
103                 {
104                         Log(LOGERR, "Too many rows found.");
105                         user_type = USER_UNAUTH;
106                 }
107         }
108         else
109         {
110                 Log(LOGERR, "No user matching %s", user);
111         }
112
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)
116         {
117                 user_type = USER_ADMIN;
118         }
119         mysql_free_result(result);
120         mysql_close(con);
121         return user_type;
122 }
123
124 /**
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
131  */
132 UserType Login_Shadow(const char * user, const char * pass, const char * shadow)
133 {
134         if (strlen(user) + strlen(pass) >= BUFSIZ-1)
135         {
136                 Log(LOGERR, "User/Password too long!\n");
137                 return USER_UNAUTH;
138         }
139
140         FILE * f = fopen(shadow, "r");
141         if (f == NULL)
142         {
143                 Log(LOGERR,"Can't open %s - %s\n", shadow, strerror(errno));
144                 return USER_UNAUTH;
145         }
146
147         char buffer[BUFSIZ];
148         int passwd_index = -1;
149
150         while (fgets(buffer, BUFSIZ, f) != NULL) // NOTE: Restrict username+password strings to BUFSIZ... what could possibly go wrong?
151         {
152
153                 //Log(LOGDEBUG,"Scanning %d: %s", strlen(buffer), buffer);
154         
155                 for (int i = 0; i < BUFSIZ-1 && buffer[i] != '\0'; ++i)
156                 {
157                         if (buffer[i] == ':')
158                         {
159                                 buffer[i] = '\0';
160                                 passwd_index = i+1;
161                                 break;
162                         }
163                 }
164
165                 if (strcmp(user,buffer) == 0)
166                 {
167                         //Log(LOGDEBUG,"User matches! %s\n", buffer);
168                         break;
169                 } 
170                 passwd_index = -1;
171         }
172
173         fclose(f);
174
175         if (passwd_index <= 0)
176         {
177                 //Log(LOGDEBUG,"No user found matching %s\n", user);
178                 return USER_UNAUTH;
179         }
180
181         int gid_index = -1;
182         for (int i = passwd_index; i < BUFSIZ-1 && buffer[i] != '\0'; ++i)
183         {
184                 if (buffer[i] == ':')
185                 {
186                         gid_index = i+1;
187                         buffer[i] = '\0';
188                 }
189                 if (buffer[i] == '\n')
190                         buffer[i] = '\0';
191         }
192         char * end = buffer+gid_index;
193         UserType user_type = USER_NORMAL;
194         if (gid_index > passwd_index && gid_index < BUFSIZ-1)
195         {
196                 int gid = strtol(buffer+gid_index, &end,10);
197                 Log(LOGDEBUG, "Usertype %d %s", gid, buffer+gid_index);
198                 if (*end == '\0' && gid == 0)
199                 {
200                         Log(LOGDEBUG, "Admin");
201                         user_type = USER_ADMIN;
202                 }
203         }
204         
205         // Determine the salt
206         char salt[BUFSIZ];
207         int s = 0; int count = 0;
208         for (int i = passwd_index; i < BUFSIZ-1; ++i)
209         {
210                 salt[s++] = buffer[i];
211                 if (salt[s] == '$' && ++count >= 3)
212                         break;
213         }
214
215 //      Log(LOGDEBUG,"Salted Entry: %s\n", buffer+passwd_index);
216 //      Log(LOGDEBUG,"Salted Attempt: %s\n", crypt(pass, salt));
217         
218         if (strcmp(crypt(pass, salt), buffer+passwd_index) == 0)
219         {
220                 return user_type;
221         }
222         return USER_UNAUTH;
223 }
224
225 /**
226  * Attempt to bind to a LDAP uri
227  * @param uri - The uri
228  * @param dn - The DN
229  * @param pass - The password
230  * @returns An error code according to libldap; LDAP_SUCCESS if everything worked
231  */
232 int Login_LDAP_Bind(const char * uri, const char * dn, const char * pass)
233 {
234         Log(LOGDEBUG, "Bind to %s with dn %s and pass %s", uri, dn, pass);
235
236         // Initialise LDAP; prepares to connect to the server
237         LDAP * ld = NULL;
238         int err = ldap_initialize(&ld, uri);
239         if (err != LDAP_SUCCESS || ld == NULL)
240         {
241                 Log(LOGERR,"ldap_initialize failed - %s (ld = %p)", ldap_err2string(err), ld);
242                 return err;
243         }
244
245         // Set the LDAP version...
246         int version = LDAP_VERSION3;
247         err = ldap_set_option(ld, LDAP_OPT_PROTOCOL_VERSION, &version); // specify the version
248         if (err != LDAP_SUCCESS)
249         {
250                 Log(LOGERR,"ldap_set_option failed - %s", ldap_err2string(err));
251                 return err;
252         }
253
254         // Attempt to bind using the supplied credentials.
255         // NOTE: ldap_simple_bind_s is "deprecated" in <ldap.h>, but not listed as such in the man pages :S
256         err = ldap_simple_bind_s(ld, dn, pass);
257         if (err != LDAP_SUCCESS)
258         {
259                 Log(LOGERR, "ldap_simple_bind_s failed - %s", ldap_err2string(err));
260         }
261         else
262         {
263                 Log(LOGDEBUG, "Successfully bound to %s with dn %s", uri, dn);
264         }
265
266         int err2 = ldap_unbind_s(ld);
267         if (err2 != LDAP_SUCCESS)
268         {
269                 Log(LOGERR, "ldap_unbind_s failed - %s", ldap_err2string(err2));
270                 err = err2;
271         }
272         return err;
273 }
274
275 /**
276  * Logout
277  * @param context - The context. The key will be cleared.
278  * @param params - Parameter string, UNUSED
279  */
280 void Logout_Handler(FCGIContext * context, char * params)
281 {
282         FCGI_ReleaseControl(context);
283         FCGI_SendControlCookie(context, false); //Unset the cookie
284         FCGI_AcceptJSON(context, "Logged out");
285 }
286
287
288 /**
289  * Handle a Login Request
290  * @param context - The context
291  * @param params - Parameter string, should contain username and password.
292  *                                 NOTE: Care should be taken when using params, as it is
293  *                                 completely unescaped. Do not log or use it without
294  *                 suitable escaping.
295  */
296 void Login_Handler(FCGIContext * context, char * params)
297 {
298         char * user; // The username supplied through CGI
299         char * pass; // The password supplied through CGI
300
301         FCGIValue values[] = {
302                 {"user", &user, FCGI_REQUIRED(FCGI_STRING_T)},
303                 {"pass", &pass, FCGI_REQUIRED(FCGI_STRING_T)},
304         };
305
306         //enum to avoid the use of magic numbers
307         typedef enum {
308                 USER,
309                 PASS,
310                 LOGOUT
311         } LoginParams;
312
313         // Fill values appropriately
314         if (!FCGI_ParseRequest(context, params, values, sizeof(values)/sizeof(FCGIValue)))
315         {
316                 // Error occured; FCGI_RejectJSON already called
317                 return;
318         }
319
320         // Trim leading whitespace
321         int i = 0;
322         for (i = 0; isspace(user[0]) && user[0] != '\0'; ++i, ++user);
323
324         // Truncate string at first non alphanumeric character
325         for (i = 0; isalnum(user[i]) && user[i] != '\0'; ++i);
326         user[i] = '\0';
327
328         
329         UserType user_type = USER_UNAUTH;
330         
331         switch (g_options.auth_method)
332         {
333
334                 case AUTH_LDAP:
335                 {
336                         if (*pass == '\0')
337                         {
338                                 FCGI_RejectJSON(context, "No password supplied.");
339                                 return;
340                         }
341
342                         //TODO: Generate the DN in some sane way
343                         char dn[BUFSIZ];
344
345
346                 
347                         // On a simple LDAP server:
348                         //int len = sprintf(dn, "uid=%s,%s", user, g_options.auth_options);
349         
350                         // At UWA (hooray)
351                         char * user_group = "Students";
352                         if (user[0] == '0')
353                                 user_group = "Staff";
354                         int len = sprintf(dn, "cn=%s,ou=%s,%s", user, user_group, g_options.auth_options);
355                 
356
357                         if (len >= BUFSIZ)
358                         {
359                                 FCGI_RejectJSON(context, "DN too long! Recompile with increased BUFSIZ");
360                                 return;
361                         }
362                 
363                         if (Login_LDAP_Bind(g_options.auth_uri, dn, pass) == LDAP_SUCCESS)
364                         {
365                                 if (user[0] == '0')
366                                         user_type = USER_ADMIN;
367                                 else
368                                         user_type = USER_NORMAL;
369                         }
370                         break;
371                 }
372                 case AUTH_SHADOW:
373                 {
374                         user_type = Login_Shadow(user, pass, g_options.auth_uri);
375                         break;
376                 }
377                 case AUTH_MYSQL:
378                 {
379                         //WARNING: C string manipulation code approaching!
380                         // Non reentrent; uses strsep and modifies g_options.auth_options
381                         // If problems happen, try strdup first ...
382                         static char * db_opts[] = {"root", "", "users", "uc_users"};
383                         static bool db_init_opts = false;
384                         if (!db_init_opts)
385                         {
386                                 db_init_opts = true;
387                                 db_opts[0] = (char*)g_options.auth_options;
388                                 for (int i = 1; i < sizeof(db_opts)/sizeof(char*); ++i)
389                                 {
390                                         char * def = db_opts[i];
391                                         db_opts[i] = db_opts[i-1];
392
393                                         strsep(db_opts+i, ",");
394                                         if (db_opts[i] == NULL)
395                                         {
396                                                 db_opts[i] = def;
397                                                 break;
398                                         }
399                                 }                       
400                                 //Log(LOGDEBUG, "MySQL: user %s pass %s name %s table %s", db_opts[0], db_opts[1], db_opts[2], db_opts[3]);     
401                         }
402
403                         user_type = Login_MySQL(user, pass, g_options.auth_uri, db_opts[0],db_opts[1], db_opts[2], db_opts[3]);
404                         break;
405                 }
406                 default:
407                 {
408                         Log(LOGWARN, "No authentication!");
409                         user_type = USER_ADMIN;
410                         break;
411                 }
412         }
413                 
414         // error check  
415         
416         if (user_type == USER_UNAUTH)
417         {
418                 Log(LOGDEBUG, "Authentication failure for %s", user);
419                 FCGI_RejectJSONEx(context, STATUS_UNAUTHORIZED, "Authentication failure.");
420         }
421         else
422         {
423                 // Try and gain control over the system
424                 if (FCGI_LockControl(context, user, user_type))
425                 {
426                         FCGI_EscapeText(context->user_name); //Don't break javascript pls
427                         // Give the user a cookie
428                         FCGI_SendControlCookie(context, true); //Send the control key
429                         FCGI_AcceptJSON(context, "Logged in");
430                         Log(LOGDEBUG, "Successful authentication for %s", user);
431                 }
432                 else
433                 {
434                         Log(LOGDEBUG, "%s successfully authenticated but system was in use by %s", user, context->user_name);
435                         FCGI_RejectJSON(context, "Someone else is already logged in (and you are not an admin)");
436                 }
437         }
438 }

UCC git Repository :: git.ucc.asn.au