1 /*
2 ** Copyright (c) 2007 D. Richard Hipp
3 **
4 ** This program is free software; you can redistribute it and/or
5 ** modify it under the terms of the Simplified BSD License (also
6 ** known as the "2-Clause License" or "FreeBSD License".)
7 
8 ** This program is distributed in the hope that it will be useful,
9 ** but without any warranty; without even the implied warranty of
10 ** merchantability or fitness for a particular purpose.
11 **
12 ** Author contact information:
13 **   drh@hwaci.com
14 **   http://www.hwaci.com/drh/
15 **
16 *******************************************************************************
17 **
18 ** This file contains code for generating the login and logout screens.
19 **
20 ** Notes:
21 **
22 ** There are four special-case user-ids:  "anonymous", "nobody",
23 ** "developer" and "reader".
24 **
25 ** The capabilities of the nobody user are available to anyone,
26 ** regardless of whether or not they are logged in.  The capabilities
27 ** of anonymous are only available after logging in, but the login
28 ** screen displays the password for the anonymous login, so this
29 ** should not prevent a human user from doing so.  The capabilities
30 ** of developer and reader are inherited by any user that has the
31 ** "v" and "u" capabilities, respectively.
32 **
33 ** The nobody user has capabilities that you want spiders to have.
34 ** The anonymous user has capabilities that you want people without
35 ** logins to have.
36 **
37 ** Of course, a sophisticated spider could easily circumvent the
38 ** anonymous login requirement and walk the website.  But that is
39 ** not really the point.  The anonymous login keeps search-engine
40 ** crawlers and site download tools like wget from walking change
41 ** logs and downloading diffs of very version of the archive that
42 ** has ever existed, and things like that.
43 */
44 #include "config.h"
45 #include "login.h"
46 #if defined(_WIN32)
47 #  include <windows.h>           /* for Sleep */
48 #  if defined(__MINGW32__) || defined(_MSC_VER)
49 #    define sleep Sleep            /* windows does not have sleep, but Sleep */
50 #  endif
51 #endif
52 #include <time.h>
53 
54 
55 /*
56 ** Return the login-group name.  Or return 0 if this repository is
57 ** not a member of a login-group.
58 */
login_group_name(void)59 const char *login_group_name(void){
60   static const char *zGroup = 0;
61   static int once = 1;
62   if( once ){
63     zGroup = db_get("login-group-name", 0);
64     once = 0;
65   }
66   return zGroup;
67 }
68 
69 /*
70 ** Return a path appropriate for setting a cookie.
71 **
72 ** The path is g.zTop for single-repo cookies.  It is "/" for
73 ** cookies of a login-group.
74 */
login_cookie_path(void)75 const char *login_cookie_path(void){
76   if( login_group_name()==0 ){
77     return g.zTop;
78   }else{
79     return "/";
80   }
81 }
82 
83 /*
84 ** Return the name of the login cookie.
85 **
86 ** The login cookie name is always of the form:  fossil-XXXXXXXXXXXXXXXX
87 ** where the Xs are the first 16 characters of the login-group-code or
88 ** of the project-code if we are not a member of any login-group.
89 */
login_cookie_name(void)90 char *login_cookie_name(void){
91   static char *zCookieName = 0;
92   if( zCookieName==0 ){
93     zCookieName = db_text(0,
94        "SELECT 'fossil-' || substr(value,1,16)"
95        "  FROM config"
96        " WHERE name IN ('project-code','login-group-code')"
97        " ORDER BY name /*sort*/"
98     );
99   }
100   return zCookieName;
101 }
102 
103 /*
104 ** Redirect to the page specified by the "g" query parameter.
105 ** Or if there is no "g" query parameter, redirect to the homepage.
106 */
redirect_to_g(void)107 static void redirect_to_g(void){
108   const char *zGoto = P("g");
109   if( zGoto ){
110     cgi_redirectf("%R/%s",zGoto);
111   }else{
112     fossil_redirect_home();
113   }
114 }
115 
116 /*
117 ** Return an abbreviated project code.  The abbreviation is the first
118 ** 16 characters of the project code.
119 **
120 ** Memory is obtained from malloc.
121 */
abbreviated_project_code(const char * zFullCode)122 static char *abbreviated_project_code(const char *zFullCode){
123   return mprintf("%.16s", zFullCode);
124 }
125 
126 
127 /*
128 ** Check to see if the anonymous login is valid.  If it is valid, return
129 ** the userid of the anonymous user.
130 **
131 ** The zCS parameter is the "captcha seed" used for a specific
132 ** anonymous login request.
133 */
login_is_valid_anonymous(const char * zUsername,const char * zPassword,const char * zCS)134 int login_is_valid_anonymous(
135   const char *zUsername,  /* The username.  Must be "anonymous" */
136   const char *zPassword,  /* The supplied password */
137   const char *zCS         /* The captcha seed value */
138 ){
139   const char *zPw;        /* The correct password shown in the captcha */
140   int uid;                /* The user ID of anonymous */
141 
142   if( zUsername==0 ) return 0;
143   else if( zPassword==0 ) return 0;
144   else if( zCS==0 ) return 0;
145   else if( fossil_strcmp(zUsername,"anonymous")!=0 ) return 0;
146   zPw = captcha_decode((unsigned int)atoi(zCS));
147   if( fossil_stricmp(zPw, zPassword)!=0 ) return 0;
148   uid = db_int(0, "SELECT uid FROM user WHERE login='anonymous'"
149                   " AND length(pw)>0 AND length(cap)>0");
150   return uid;
151 }
152 
153 /*
154 ** Make sure the accesslog table exists.  Create it if it does not
155 */
create_accesslog_table(void)156 void create_accesslog_table(void){
157   db_multi_exec(
158     "CREATE TABLE IF NOT EXISTS repository.accesslog("
159     "  uname TEXT,"
160     "  ipaddr TEXT,"
161     "  success BOOLEAN,"
162     "  mtime TIMESTAMP"
163     ");"
164   );
165 }
166 
167 /*
168 ** Make a record of a login attempt, if login record keeping is enabled.
169 */
record_login_attempt(const char * zUsername,const char * zIpAddr,int bSuccess)170 static void record_login_attempt(
171   const char *zUsername,     /* Name of user logging in */
172   const char *zIpAddr,       /* IP address from which they logged in */
173   int bSuccess               /* True if the attempt was a success */
174 ){
175   if( db_get_boolean("access-log", 0) ){
176     create_accesslog_table();
177     db_multi_exec(
178       "INSERT INTO accesslog(uname,ipaddr,success,mtime)"
179       "VALUES(%Q,%Q,%d,julianday('now'));",
180       zUsername, zIpAddr, bSuccess
181     );
182   }
183   if( bSuccess ){
184     alert_user_contact(zUsername);
185   }
186 }
187 
188 /*
189 ** Searches for the user ID matching the given name and password.
190 ** On success it returns a positive value. On error it returns 0.
191 ** On serious (DB-level) error it will probably exit.
192 **
193 ** zUsername uses double indirection because we may re-point *zUsername
194 ** at a C string allocated with fossil_strdup() if you pass an email
195 ** address instead and we find that address in the user table's info
196 ** field, which is expected to contain a string of the form "Human Name
197 ** <human@example.com>".  In that case, *zUsername will point to that
198 ** user's actual login name on return, causing a leak unless the caller
199 ** is diligent enough to check whether its pointer was re-pointed.
200 **
201 ** zPassword may be either the plain-text form or the encrypted
202 ** form of the user's password.
203 */
login_search_uid(const char ** pzUsername,const char * zPasswd)204 int login_search_uid(const char **pzUsername, const char *zPasswd){
205   char *zSha1Pw = sha1_shared_secret(zPasswd, *pzUsername, 0);
206   int uid = db_int(0,
207     "SELECT uid FROM user"
208     " WHERE login=%Q"
209     "   AND length(cap)>0 AND length(pw)>0"
210     "   AND login NOT IN ('anonymous','nobody','developer','reader')"
211     "   AND (pw=%Q OR (length(pw)<>40 AND pw=%Q))"
212     "   AND (info NOT LIKE '%%expires 20%%'"
213     "      OR substr(info,instr(lower(info),'expires')+8,10)>datetime('now'))",
214     *pzUsername, zSha1Pw, zPasswd
215   );
216 
217   /* If we did not find a login on the first attempt, and the username
218   ** looks like an email address, then perhaps the user entered their
219   ** email address instead of their login.  Try again to match the user
220   ** against email addresses contained in the "info" field.
221   */
222   if( uid==0 && strchr(*pzUsername,'@')!=0 ){
223     Stmt q;
224     db_prepare(&q,
225       "SELECT login FROM user"
226       " WHERE find_emailaddr(info)=%Q"
227       "   AND instr(login,'@')==0",
228       *pzUsername
229     );
230     while( db_step(&q)==SQLITE_ROW ){
231       const char *zLogin = db_column_text(&q,0);
232       if( (uid = login_search_uid(&zLogin, zPasswd) ) != 0 ){
233         *pzUsername = fossil_strdup(zLogin);
234         break;
235       }
236     }
237     db_finalize(&q);
238   }
239   free(zSha1Pw);
240   return uid;
241 }
242 
243 /*
244 ** Generates a login cookie value for a non-anonymous user.
245 **
246 ** The zHash parameter must be a random value which must be
247 ** subsequently stored in user.cookie for later validation.
248 **
249 ** The returned memory should be free()d after use.
250 */
login_gen_user_cookie_value(const char * zUsername,const char * zHash)251 char *login_gen_user_cookie_value(const char *zUsername, const char *zHash){
252   char *zProjCode = db_get("project-code",NULL);
253   char *zCode = abbreviated_project_code(zProjCode);
254   free(zProjCode);
255   assert((zUsername && *zUsername) && "Invalid user data.");
256   return mprintf("%s/%z/%s", zHash, zCode, zUsername);
257 }
258 
259 /*
260 ** Generates a login cookie for NON-ANONYMOUS users.  Note that this
261 ** function "could" figure out the uid by itself but it currently
262 ** doesn't because the code which calls this already has the uid.
263 **
264 ** This function also updates the user.cookie, user.ipaddr,
265 ** and user.cexpire fields for the given user.
266 **
267 ** If zDest is not NULL then the generated cookie is copied to
268 ** *zDdest and ownership is transfered to the caller (who should
269 ** eventually pass it to free()).
270 **
271 ** If bSessionCookie is true, the cookie will be a session cookie,
272 ** else a persistent cookie. If it's a session cookie, the
273 ** [user].[cexpire] and [user].[cookie] entries will be modified as if
274 ** it were a persistent cookie because doing so is necessary for
275 ** fossil's own "is this cookie still valid?" checks to work.
276 */
login_set_user_cookie(const char * zUsername,int uid,char ** zDest,int bSessionCookie)277 void login_set_user_cookie(
278   const char *zUsername,  /* User's name */
279   int uid,                /* User's ID */
280   char **zDest,           /* Optional: store generated cookie value. */
281   int bSessionCookie      /* True for session-only cookie */
282 ){
283   const char *zCookieName = login_cookie_name();
284   const char *zExpire = db_get("cookie-expire","8766");
285   const int expires = atoi(zExpire)*3600;
286   char *zHash = 0;
287   char *zCookie;
288   const char *zIpAddr = PD("REMOTE_ADDR","nil"); /* IP address of user */
289 
290   assert((zUsername && *zUsername) && (uid > 0) && "Invalid user data.");
291   zHash = db_text(0,
292       "SELECT cookie FROM user"
293       " WHERE uid=%d"
294       "   AND cexpire>julianday('now')"
295       "   AND length(cookie)>30",
296       uid);
297   if( zHash==0 ) zHash = db_text(0, "SELECT hex(randomblob(25))");
298   zCookie = login_gen_user_cookie_value(zUsername, zHash);
299   cgi_set_cookie(zCookieName, zCookie, login_cookie_path(),
300                  bSessionCookie ? 0 : expires);
301   record_login_attempt(zUsername, zIpAddr, 1);
302   db_unprotect(PROTECT_USER);
303   db_multi_exec("UPDATE user SET cookie=%Q,"
304                 "  cexpire=julianday('now')+%d/86400.0 WHERE uid=%d",
305                 zHash, expires, uid);
306   db_protect_pop();
307   fossil_free(zHash);
308   if( zDest ){
309     *zDest = zCookie;
310   }else{
311     free(zCookie);
312   }
313 }
314 
315 /* Sets a cookie for an anonymous user login, which looks like this:
316 **
317 **    HASH/TIME/anonymous
318 **
319 ** Where HASH is the sha1sum of TIME/SECRET, in which SECRET is captcha-secret.
320 **
321 ** If zCookieDest is not NULL then the generated cookie is assigned to
322 ** *zCookieDest and the caller must eventually free() it.
323 **
324 ** If bSessionCookie is true, the cookie will be a session cookie.
325 */
login_set_anon_cookie(const char * zIpAddr,char ** zCookieDest,int bSessionCookie)326 void login_set_anon_cookie(const char *zIpAddr, char **zCookieDest,
327                            int bSessionCookie ){
328   const char *zNow;            /* Current time (julian day number) */
329   char *zCookie;               /* The login cookie */
330   const char *zCookieName;     /* Name of the login cookie */
331   Blob b;                      /* Blob used during cookie construction */
332   int expires = bSessionCookie ? 0 : 6*3600;
333   zCookieName = login_cookie_name();
334   zNow = db_text("0", "SELECT julianday('now')");
335   assert( zCookieName && zNow );
336   blob_init(&b, zNow, -1);
337   blob_appendf(&b, "/%s", db_get("captcha-secret",""));
338   sha1sum_blob(&b, &b);
339   zCookie = mprintf("%s/%s/anonymous", blob_buffer(&b), zNow);
340   blob_reset(&b);
341   cgi_set_cookie(zCookieName, zCookie, login_cookie_path(), expires);
342   if( zCookieDest ){
343     *zCookieDest = zCookie;
344   }else{
345     free(zCookie);
346   }
347 }
348 
349 /*
350 ** "Unsets" the login cookie (insofar as cookies can be unset) and
351 ** clears the current user's (g.userUid) login information from the
352 ** user table. Sets: user.cookie, user.ipaddr, user.cexpire.
353 **
354 ** We could/should arguably clear out g.userUid and g.perm here, but
355 ** we don't currently do not.
356 **
357 ** This is a no-op if g.userUid is 0.
358 */
login_clear_login_data()359 void login_clear_login_data(){
360   if(!g.userUid){
361     return;
362   }else{
363     const char *cookie = login_cookie_name();
364     /* To logout, change the cookie value to an empty string */
365     cgi_set_cookie(cookie, "",
366                    login_cookie_path(), -86400);
367     db_unprotect(PROTECT_USER);
368     db_multi_exec("UPDATE user SET cookie=NULL, ipaddr=NULL, "
369                   "  cexpire=0 WHERE uid=%d"
370                   "  AND login NOT IN ('anonymous','nobody',"
371                   "  'developer','reader')", g.userUid);
372     db_protect_pop();
373     cgi_replace_parameter(cookie, NULL);
374     cgi_replace_parameter("anon", NULL);
375   }
376 }
377 
378 /*
379 ** Return true if the prefix of zStr matches zPattern.  Return false if
380 ** they are different.
381 **
382 ** A lowercase character in zPattern will match either upper or lower
383 ** case in zStr.  But an uppercase in zPattern will only match an
384 ** uppercase in zStr.
385 */
prefix_match(const char * zPattern,const char * zStr)386 static int prefix_match(const char *zPattern, const char *zStr){
387   int i;
388   char c;
389   for(i=0; (c = zPattern[i])!=0; i++){
390     if( zStr[i]!=c && fossil_tolower(zStr[i])!=c ) return 0;
391   }
392   return 1;
393 }
394 
395 /*
396 ** Look at the HTTP_USER_AGENT parameter and try to determine if the user agent
397 ** is a manually operated browser or a bot.  When in doubt, assume a bot.
398 ** Return true if we believe the agent is a real person.
399 */
isHuman(const char * zAgent)400 static int isHuman(const char *zAgent){
401   int i;
402   if( zAgent==0 ) return 0;  /* If no UserAgent, then probably a bot */
403   for(i=0; zAgent[i]; i++){
404     if( prefix_match("bot", zAgent+i) ) return 0;
405     if( prefix_match("spider", zAgent+i) ) return 0;
406     if( prefix_match("crawl", zAgent+i) ) return 0;
407     /* If a URI appears in the User-Agent, it is probably a bot */
408     if( strncmp("http", zAgent+i,4)==0 ) return 0;
409   }
410   if( strncmp(zAgent, "Mozilla/", 8)==0 ){
411     if( atoi(&zAgent[8])<4 ) return 0;  /* Many bots advertise as Mozilla/3 */
412 
413     /* 2016-05-30:  A pernicious spider that likes to walk Fossil timelines has
414     ** been detected on the SQLite website.  The spider changes its user-agent
415     ** string frequently, but it always seems to include the following text:
416     */
417     if( sqlite3_strglob("*Safari/537.36Mozilla/5.0*", zAgent)==0 ) return 0;
418 
419     if( sqlite3_strglob("*Firefox/[1-9]*", zAgent)==0 ) return 1;
420     if( sqlite3_strglob("*Chrome/[1-9]*", zAgent)==0 ) return 1;
421     if( sqlite3_strglob("*(compatible;?MSIE?[1789]*", zAgent)==0 ) return 1;
422     if( sqlite3_strglob("*Trident/[1-9]*;?rv:[1-9]*", zAgent)==0 ){
423       return 1; /* IE11+ */
424     }
425     if( sqlite3_strglob("*AppleWebKit/[1-9]*(KHTML*", zAgent)==0 ) return 1;
426     if( sqlite3_strglob("*PaleMoon/[1-9]*", zAgent)==0 ) return 1;
427     return 0;
428   }
429   if( strncmp(zAgent, "Opera/", 6)==0 ) return 1;
430   if( strncmp(zAgent, "Safari/", 7)==0 ) return 1;
431   if( strncmp(zAgent, "Lynx/", 5)==0 ) return 1;
432   if( strncmp(zAgent, "NetSurf/", 8)==0 ) return 1;
433   return 0;
434 }
435 
436 /*
437 ** Make a guess at whether or not the requestor is a mobile device or
438 ** a desktop device (narrow screen vs. wide screen) based the HTTP_USER_AGENT
439 ** parameter.  Return true for mobile and false for desktop.
440 **
441 ** Caution:  This is only a guess.
442 **
443 ** Algorithm derived from https://developer.mozilla.org/en-US/docs/Web/
444 ** HTTP/Browser_detection_using_the_user_agent#mobile_device_detection on
445 ** 2021-03-01
446 */
user_agent_is_likely_mobile(void)447 int user_agent_is_likely_mobile(void){
448   const char *zAgent = P("HTTP_USER_AGENT");
449   if( zAgent==0 ) return 0;
450   if( strstr(zAgent,"Mobi")!=0 ) return 1;
451   return 0;
452 }
453 
454 /*
455 ** COMMAND: test-ishuman
456 **
457 ** Read lines of text from standard input.  Interpret each line of text
458 ** as a User-Agent string from an HTTP header.  Label each line as HUMAN
459 ** or ROBOT.
460 */
test_ishuman(void)461 void test_ishuman(void){
462   char zLine[3000];
463   while( fgets(zLine, sizeof(zLine), stdin) ){
464     fossil_print("%s %s", isHuman(zLine) ? "HUMAN" : "ROBOT", zLine);
465   }
466 }
467 
468 /*
469 ** SQL function for constant time comparison of two values.
470 ** Sets result to 0 if two values are equal.
471 */
constant_time_cmp_function(sqlite3_context * context,int argc,sqlite3_value ** argv)472 static void constant_time_cmp_function(
473  sqlite3_context *context,
474  int argc,
475  sqlite3_value **argv
476 ){
477   const unsigned char *buf1, *buf2;
478   int len, i;
479   unsigned char rc = 0;
480 
481   assert( argc==2 );
482   len = sqlite3_value_bytes(argv[0]);
483   if( len==0 || len!=sqlite3_value_bytes(argv[1]) ){
484     rc = 1;
485   }else{
486     buf1 = sqlite3_value_text(argv[0]);
487     buf2 = sqlite3_value_text(argv[1]);
488     for( i=0; i<len; i++ ){
489       rc = rc | (buf1[i] ^ buf2[i]);
490     }
491   }
492   sqlite3_result_int(context, rc);
493 }
494 
495 /*
496 ** Return true if the current page was reached by a redirect from the /login
497 ** page.
498 */
referred_from_login(void)499 int referred_from_login(void){
500   const char *zReferer = P("HTTP_REFERER");
501   char *zPattern;
502   int rc;
503   if( zReferer==0 ) return 0;
504   zPattern = mprintf("%s/login*", g.zBaseURL);
505   rc = sqlite3_strglob(zPattern, zReferer)==0;
506   fossil_free(zPattern);
507   return rc;
508 }
509 
510 /*
511 ** Return TRUE if self-registration is available.  If the zNeeded
512 ** argument is not NULL, then only return true if self-registration is
513 ** available and any of the capabilities named in zNeeded are available
514 ** to self-registered users.
515 */
login_self_register_available(const char * zNeeded)516 int login_self_register_available(const char *zNeeded){
517   CapabilityString *pCap;
518   int rc;
519   if( !db_get_boolean("self-register",0) ) return 0;
520   if( zNeeded==0 ) return 1;
521   pCap = capability_add(0, db_get("default-perms", "u"));
522   capability_expand(pCap);
523   rc = capability_has_any(pCap, zNeeded);
524   capability_free(pCap);
525   return rc;
526 }
527 
528 /*
529 ** There used to be a page named "my" that was designed to show information
530 ** about a specific user.  The "my" page was linked from the "Logged in as USER"
531 ** line on the title bar.  The "my" page was never completed so it is now
532 ** removed.  Use this page as a placeholder in older installations.
533 **
534 ** WEBPAGE: login
535 ** WEBPAGE: logout
536 ** WEBPAGE: my
537 **
538 ** The login/logout page.  Parameters:
539 **
540 **    g=URL             Jump back to this URL after login completes
541 **    anon              The g=URL is not accessible by "nobody" but is
542 **                      accessible by "anonymous"
543 */
login_page(void)544 void login_page(void){
545   const char *zUsername, *zPasswd;
546   const char *zNew1, *zNew2;
547   const char *zAnonPw = 0;
548   const char *zGoto = P("g");
549   int anonFlag;                /* Login as "anonymous" would be useful */
550   char *zErrMsg = "";
551   int uid;                     /* User id logged in user */
552   char *zSha1Pw;
553   const char *zIpAddr;         /* IP address of requestor */
554   const int noAnon = P("noanon")!=0;
555   int rememberMe;              /* If true, use persistent cookie, else
556                                   session cookie. Toggled per
557                                   checkbox. */
558 
559   login_check_credentials();
560   fossil_redirect_to_https_if_needed(1);
561   sqlite3_create_function(g.db, "constant_time_cmp", 2, SQLITE_UTF8, 0,
562                   constant_time_cmp_function, 0, 0);
563   zUsername = P("u");
564   zPasswd = P("p");
565   anonFlag = g.zLogin==0 && PB("anon");
566   /* Handle log-out requests */
567   if( P("out") ){
568     login_clear_login_data();
569     redirect_to_g();
570     return;
571   }
572 
573   /* Redirect for create-new-account requests */
574   if( P("self") ){
575     cgi_redirectf("%R/register");
576     return;
577   }
578 
579   /* Deal with password-change requests */
580   if( g.perm.Password && zPasswd
581    && (zNew1 = P("n1"))!=0 && (zNew2 = P("n2"))!=0
582   ){
583     /* If there is not a "real" login, we cannot change any password. */
584     if( g.zLogin ){
585       /* The user requests a password change */
586       zSha1Pw = sha1_shared_secret(zPasswd, g.zLogin, 0);
587       if( db_int(1, "SELECT 0 FROM user"
588                     " WHERE uid=%d"
589                     " AND (constant_time_cmp(pw,%Q)=0"
590                     "      OR constant_time_cmp(pw,%Q)=0)",
591                     g.userUid, zSha1Pw, zPasswd) ){
592         sleep(1);
593         zErrMsg =
594            @ <p><span class="loginError">
595            @ You entered an incorrect old password while attempting to change
596            @ your password.  Your password is unchanged.
597            @ </span></p>
598         ;
599       }else if( fossil_strcmp(zNew1,zNew2)!=0 ){
600         zErrMsg =
601            @ <p><span class="loginError">
602            @ The two copies of your new passwords do not match.
603            @ Your password is unchanged.
604            @ </span></p>
605         ;
606       }else{
607         char *zNewPw = sha1_shared_secret(zNew1, g.zLogin, 0);
608         char *zChngPw;
609         char *zErr;
610         int rc;
611 
612         db_unprotect(PROTECT_USER);
613         db_multi_exec(
614            "UPDATE user SET pw=%Q WHERE uid=%d", zNewPw, g.userUid
615         );
616         zChngPw = mprintf(
617            "UPDATE user"
618            "   SET pw=shared_secret(%Q,%Q,"
619            "        (SELECT value FROM config WHERE name='project-code'))"
620            " WHERE login=%Q",
621            zNew1, g.zLogin, g.zLogin
622         );
623         fossil_free(zNewPw);
624         rc = login_group_sql(zChngPw, "<p>", "</p>\n", &zErr);
625         db_protect_pop();
626         if( rc ){
627           zErrMsg = mprintf("<span class=\"loginError\">%s</span>", zErr);
628           fossil_free(zErr);
629         }else{
630           redirect_to_g();
631           return;
632         }
633       }
634     }else{
635       zErrMsg =
636          @ <p><span class="loginError">
637          @ The password cannot be changed for this type of login.
638          @ The password is unchanged.
639          @ </span></p>
640       ;
641     }
642   }
643   zIpAddr = PD("REMOTE_ADDR","nil");   /* Complete IP address for logging */
644   uid = login_is_valid_anonymous(zUsername, zPasswd, P("cs"));
645   if(zUsername==0){
646     /* Initial login page hit. */
647     rememberMe = 0;
648   }else{
649     rememberMe = P("remember")!=0;
650   }
651   if( uid>0 ){
652     login_set_anon_cookie(zIpAddr, NULL, rememberMe?0:1);
653     record_login_attempt("anonymous", zIpAddr, 1);
654     redirect_to_g();
655   }
656   if( zUsername!=0 && zPasswd!=0 && zPasswd[0]!=0 ){
657     /* Attempting to log in as a user other than anonymous.
658     */
659     uid = login_search_uid(&zUsername, zPasswd);
660     if( uid<=0 ){
661       sleep(1);
662       zErrMsg =
663          @ <p><span class="loginError">
664          @ You entered an unknown user or an incorrect password.
665          @ </span></p>
666       ;
667       record_login_attempt(zUsername, zIpAddr, 0);
668       cgi_set_status(401, "Unauthorized");
669     }else{
670       /* Non-anonymous login is successful.  Set a cookie of the form:
671       **
672       **    HASH/PROJECT/LOGIN
673       **
674       ** where HASH is a random hex number, PROJECT is either project
675       ** code prefix, and LOGIN is the user name.
676       */
677       login_set_user_cookie(zUsername, uid, NULL, rememberMe?0:1);
678       redirect_to_g();
679     }
680   }
681   style_set_current_feature("login");
682   style_header("Login/Logout");
683   style_adunit_config(ADUNIT_OFF);
684   @ %s(zErrMsg)
685   if( zGoto && !noAnon ){
686     char *zAbbrev = fossil_strdup(zGoto);
687     int i;
688     for(i=0; zAbbrev[i] && zAbbrev[i]!='?'; i++){}
689     zAbbrev[i] = 0;
690     if( g.zLogin ){
691       @ <p>Use a different login with greater privilege than <b>%h(g.zLogin)</b>
692       @ to access <b>%h(zAbbrev)</b>.
693     }else if( anonFlag ){
694       @ <p>Login as <b>anonymous</b> or any named user
695       @ to access page <b>%h(zAbbrev)</b>.
696     }else{
697       @ <p>Login as a named user to access page <b>%h(zAbbrev)</b>.
698     }
699     fossil_free(zAbbrev);
700   }
701   if( g.sslNotAvailable==0
702    && strncmp(g.zBaseURL,"https:",6)!=0
703    && db_get_boolean("https-login",0)
704   ){
705     form_begin(0, "https:%s/login", g.zBaseURL+5);
706   }else{
707     form_begin(0, "%R/login");
708   }
709   if( zGoto ){
710     @ <input type="hidden" name="g" value="%h(zGoto)" />
711   }
712   if( anonFlag ){
713     @ <input type="hidden" name="anon" value="1" />
714   }
715   if( g.zLogin ){
716     @ <p>Currently logged in as <b>%h(g.zLogin)</b>.
717     @ <input type="submit" name="out" value="Logout"></p>
718     @ </form>
719   }else{
720     unsigned int uSeed = captcha_seed();
721     if( g.zLogin==0 && (anonFlag || zGoto==0) ){
722       zAnonPw = db_text(0, "SELECT pw FROM user"
723                            " WHERE login='anonymous'"
724                            "   AND cap!=''");
725     }else{
726       zAnonPw = 0;
727     }
728     @ <table class="login_out">
729     if( P("HTTPS")==0 ){
730       @ <tr><td class="form_label">Warning:</td>
731       @ <td><span class='securityWarning'>
732       @ Login information, including the password,
733       @ will be sent in the clear over an unencrypted connection.
734       if( !g.sslNotAvailable ){
735         @ Consider logging in at
736         @ <a href='%s(g.zHttpsURL)'>%h(g.zHttpsURL)</a> instead.
737       }
738       @ </span></td></tr>
739     }
740     @ <tr>
741     @   <td class="form_label" id="userlabel1">User ID:</td>
742     @   <td><input type="text" id="u" aria-labelledby="userlabel1" name="u" \
743     @ size="30" value="%s(anonFlag?"anonymous":"")"></td>
744     @ </tr>
745     @ <tr>
746     @  <td class="form_label" id="pswdlabel">Password:</td>
747     @  <td><input aria-labelledby="pswdlabel" type="password" id="p" \
748     @ name="p" value="" size="30" />\
749     if( zAnonPw && !noAnon ){
750       captcha_speakit_button(uSeed, "Speak password for \"anonymous\"");
751     }
752     @ </td>
753     @ </tr>
754     @ <tr>
755     @   <td></td>
756     @   <td><input type="checkbox" name="remember" value="1" \
757     @ id="remember-me" %s(rememberMe ? "checked=\"checked\"" : "")>
758     @   <label for="remember-me">Remember me?</label></td>
759     @ </tr>
760     @ <tr>
761     @   <td></td>
762     @   <td><input type="submit" name="in" value="Login">
763     @ </tr>
764     if( !noAnon && login_self_register_available(0) ){
765       @ <tr>
766       @   <td></td>
767       @   <td><input type="submit" name="self" value="Create A New Account">
768       @ </tr>
769     }
770     @ </table>
771     if( zAnonPw && !noAnon ){
772       const char *zDecoded = captcha_decode(uSeed);
773       int bAutoCaptcha = db_get_boolean("auto-captcha", 0);
774       char *zCaptcha = captcha_render(zDecoded);
775 
776       @ <p><input type="hidden" name="cs" value="%u(uSeed)" />
777       @ Visitors may enter <b>anonymous</b> as the user-ID with
778       @ the 8-character hexadecimal password shown below:</p>
779       @ <div class="captcha"><table class="captcha"><tr><td>\
780       @ <pre class="captcha">
781       @ %h(zCaptcha)
782       @ </pre></td></tr></table>
783       if( bAutoCaptcha ) {
784          @ <input type="button" value="Fill out captcha" id='autofillButton' \
785          @ data-af='%s(zDecoded)' />
786          builtin_request_js("login.js");
787       }
788       @ </div>
789       free(zCaptcha);
790     }
791     @ </form>
792   }
793   if( login_is_individual() ){
794     if( g.perm.EmailAlert && alert_enabled() ){
795       @ <hr>
796       @ <p>Configure <a href="%R/alerts">Email Alerts</a>
797       @ for user <b>%h(g.zLogin)</b></p>
798     }
799     if( db_table_exists("repository","forumpost") ){
800       @ <hr><p>
801       @ <a href="%R/timeline?ss=v&y=f&vfx&u=%t(g.zLogin)">Forum
802       @ post timeline</a> for user <b>%h(g.zLogin)</b></p>
803     }
804     if( g.perm.Password ){
805       char *zRPW = fossil_random_password(12);
806       @ <hr>
807       @ <p>Change Password for user <b>%h(g.zLogin)</b>:</p>
808       form_begin(0, "%R/login");
809       @ <table>
810       @ <tr><td class="form_label" id="oldpw">Old Password:</td>
811       @ <td><input aria-labelledby="oldpw" type="password" name="p" \
812       @ size="30"/></td></tr>
813       @ <tr><td class="form_label" id="newpw">New Password:</td>
814       @ <td><input aria-labelledby="newpw" type="password" name="n1" \
815       @ size="30" /> Suggestion: %z(zRPW)</td></tr>
816       @ <tr><td class="form_label" id="reppw">Repeat New Password:</td>
817       @ <td><input aria-labledby="reppw" type="password" name="n2" \
818       @ size="30" /></td></tr>
819       @ <tr><td></td>
820       @ <td><input type="submit" value="Change Password" /></td></tr>
821       @ </table>
822       @ </form>
823     }
824   }
825   style_finish_page();
826 }
827 
828 /*
829 ** Attempt to find login credentials for user zLogin on a peer repository
830 ** with project code zCode.  Transfer those credentials to the local
831 ** repository.
832 **
833 ** Return true if a transfer was made and false if not.
834 */
login_transfer_credentials(const char * zLogin,const char * zCode,const char * zHash)835 static int login_transfer_credentials(
836   const char *zLogin,          /* Login we are looking for */
837   const char *zCode,           /* Project code of peer repository */
838   const char *zHash            /* HASH from login cookie HASH/CODE/LOGIN */
839 ){
840   sqlite3 *pOther = 0;         /* The other repository */
841   sqlite3_stmt *pStmt;         /* Query against the other repository */
842   char *zSQL;                  /* SQL of the query against other repo */
843   char *zOtherRepo;            /* Filename of the other repository */
844   int rc;                      /* Result code from SQLite library functions */
845   int nXfer = 0;               /* Number of credentials transferred */
846 
847   zOtherRepo = db_text(0,
848        "SELECT value FROM config WHERE name='peer-repo-%q'",
849        zCode
850   );
851   if( zOtherRepo==0 ) return 0;  /* No such peer repository */
852 
853   rc = sqlite3_open_v2(
854        zOtherRepo, &pOther,
855        SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE,
856        g.zVfsName
857   );
858   if( rc==SQLITE_OK ){
859     sqlite3_create_function(pOther,"now",0,SQLITE_UTF8,0,db_now_function,0,0);
860     sqlite3_create_function(pOther, "constant_time_cmp", 2, SQLITE_UTF8, 0,
861                   constant_time_cmp_function, 0, 0);
862     sqlite3_busy_timeout(pOther, 5000);
863     zSQL = mprintf(
864       "SELECT cexpire FROM user"
865       " WHERE login=%Q"
866       "   AND length(cap)>0"
867       "   AND length(pw)>0"
868       "   AND cexpire>julianday('now')"
869       "   AND constant_time_cmp(cookie,%Q)=0",
870       zLogin, zHash
871     );
872     pStmt = 0;
873     rc = sqlite3_prepare_v2(pOther, zSQL, -1, &pStmt, 0);
874     if( rc==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){
875       db_unprotect(PROTECT_USER);
876       db_multi_exec(
877         "UPDATE user SET cookie=%Q, cexpire=%.17g"
878         " WHERE login=%Q",
879         zHash,
880         sqlite3_column_double(pStmt, 0), zLogin
881       );
882       db_protect_pop();
883       nXfer++;
884     }
885     sqlite3_finalize(pStmt);
886   }
887   sqlite3_close(pOther);
888   fossil_free(zOtherRepo);
889   return nXfer;
890 }
891 
892 /*
893 ** Return TRUE if zLogin is one of the special usernames
894 */
login_is_special(const char * zLogin)895 int login_is_special(const char *zLogin){
896   if( fossil_strcmp(zLogin, "anonymous")==0 ) return 1;
897   if( fossil_strcmp(zLogin, "nobody")==0 ) return 1;
898   if( fossil_strcmp(zLogin, "developer")==0 ) return 1;
899   if( fossil_strcmp(zLogin, "reader")==0 ) return 1;
900   return 0;
901 }
902 
903 /*
904 ** Lookup the uid for a non-built-in user with zLogin and zCookie.
905 ** Return 0 if not found.
906 **
907 ** Note that this only searches for logged-in entries with matching
908 ** zCookie (db: user.cookie) entries.
909 */
login_find_user(const char * zLogin,const char * zCookie)910 static int login_find_user(
911   const char *zLogin,            /* User name */
912   const char *zCookie            /* Login cookie value */
913 ){
914   int uid;
915   if( login_is_special(zLogin) ) return 0;
916   uid = db_int(0,
917     "SELECT uid FROM user"
918     " WHERE login=%Q"
919     "   AND cexpire>julianday('now')"
920     "   AND length(cap)>0"
921     "   AND length(pw)>0"
922     "   AND constant_time_cmp(cookie,%Q)=0",
923     zLogin, zCookie
924   );
925   return uid;
926 }
927 
928 /*
929 ** Attempt to use Basic Authentication to establish the user.  Return the
930 ** (non-zero) uid if successful.  Return 0 if it does not work.
931 */
login_basic_authentication(const char * zIpAddr)932 static int login_basic_authentication(const char *zIpAddr){
933   const char *zAuth = PD("HTTP_AUTHORIZATION", 0);
934   int i;
935   int uid = 0;
936   int nDecode = 0;
937   char *zDecode = 0;
938   const char *zUsername = 0;
939   const char *zPasswd = 0;
940 
941   if( zAuth==0 ) return 0;             /* Fail: No Authentication: header */
942   while( fossil_isspace(zAuth[0]) ) zAuth++;  /* Skip leading whitespace */
943   if( strncmp(zAuth, "Basic ", 6)!=0 ){
944     return 0;  /* Fail: Not Basic Authentication */
945   }
946 
947   /* Parse out the username and password, separated by a ":" */
948   zAuth += 6;
949   while( fossil_isspace(zAuth[0]) ) zAuth++;
950   zDecode = decode64(zAuth, &nDecode);
951 
952   for(i=0; zDecode[i] && zDecode[i]!=':'; i++){}
953   if( zDecode[i] ){
954     zDecode[i] = 0;
955     zUsername = zDecode;
956     zPasswd = &zDecode[i+1];
957 
958     /* Attempting to log in as the user provided by HTTP
959     ** basic auth
960     */
961     uid = login_search_uid(&zUsername, zPasswd);
962     if( uid>0 ){
963       record_login_attempt(zUsername, zIpAddr, 1);
964     }else{
965       record_login_attempt(zUsername, zIpAddr, 0);
966 
967       /* The user attempted to login specifically with HTTP basic
968       ** auth, but provided invalid credentials. Inform them of
969       ** the failed login attempt via 401.
970       */
971       cgi_set_status(401, "Unauthorized");
972       cgi_reply();
973       fossil_exit(0);
974     }
975   }
976   fossil_free(zDecode);
977   return uid;
978 }
979 
980 /*
981 ** This routine examines the login cookie to see if it exists and
982 ** is valid.  If the login cookie checks out, it then sets global
983 ** variables appropriately.
984 **
985 **    g.userUid      Database USER.UID value.  Might be -1 for "nobody"
986 **    g.zLogin       Database USER.LOGIN value.  NULL for user "nobody"
987 **    g.perm         Permissions granted to this user
988 **    g.anon         Permissions that would be available to anonymous
989 **    g.isHuman      True if the user is human, not a spider or robot
990 **
991 */
login_check_credentials(void)992 void login_check_credentials(void){
993   int uid = 0;                  /* User id */
994   const char *zCookie;          /* Text of the login cookie */
995   const char *zIpAddr;          /* Raw IP address of the requestor */
996   const char *zCap = 0;         /* Capability string */
997   const char *zPublicPages = 0; /* GLOB patterns of public pages */
998   const char *zLogin = 0;       /* Login user for credentials */
999 
1000   /* Only run this check once.  */
1001   if( g.userUid!=0 ) return;
1002 
1003   sqlite3_create_function(g.db, "constant_time_cmp", 2, SQLITE_UTF8, 0,
1004                   constant_time_cmp_function, 0, 0);
1005 
1006   /* If the HTTP connection is coming over 127.0.0.1 and if
1007   ** local login is disabled and if we are using HTTP and not HTTPS,
1008   ** then there is no need to check user credentials.
1009   **
1010   ** This feature allows the "fossil ui" command to give the user
1011   ** full access rights without having to log in.
1012   */
1013   zIpAddr = PD("REMOTE_ADDR","nil");
1014   if( ( cgi_is_loopback(zIpAddr)
1015        || (g.fSshClient & CGI_SSH_CLIENT)!=0 )
1016    && g.useLocalauth
1017    && db_get_int("localauth",0)==0
1018    && P("HTTPS")==0
1019   ){
1020     if( g.localOpen ) zLogin = db_lget("default-user",0);
1021     if( zLogin!=0 ){
1022       uid = db_int(0, "SELECT uid FROM user WHERE login=%Q", zLogin);
1023     }else{
1024       uid = db_int(0, "SELECT uid FROM user WHERE cap LIKE '%%s%%'");
1025     }
1026     g.zLogin = db_text("?", "SELECT login FROM user WHERE uid=%d", uid);
1027     zCap = "sxy";
1028     g.noPswd = 1;
1029     g.isHuman = 1;
1030     sqlite3_snprintf(sizeof(g.zCsrfToken), g.zCsrfToken, "localhost");
1031   }
1032 
1033   /* Check the login cookie to see if it matches a known valid user.
1034   */
1035   if( uid==0 && (zCookie = P(login_cookie_name()))!=0 ){
1036     /* Parse the cookie value up into HASH/ARG/USER */
1037     char *zHash = fossil_strdup(zCookie);
1038     char *zArg = 0;
1039     char *zUser = 0;
1040     int i, c;
1041     for(i=0; (c = zHash[i])!=0; i++){
1042       if( c=='/' ){
1043         zHash[i++] = 0;
1044         if( zArg==0 ){
1045           zArg = &zHash[i];
1046         }else{
1047           zUser = &zHash[i];
1048           break;
1049         }
1050       }
1051     }
1052     if( zUser==0 ){
1053       /* Invalid cookie */
1054     }else if( fossil_strcmp(zUser, "anonymous")==0 ){
1055       /* Cookies of the form "HASH/TIME/anonymous".  The TIME must not be
1056       ** too old and the sha1 hash of TIME/SECRET must match HASH.
1057       ** SECRET is the "captcha-secret" value in the repository.
1058       */
1059       double rTime = atof(zArg);
1060       Blob b;
1061       blob_zero(&b);
1062       blob_appendf(&b, "%s/%s", zArg, db_get("captcha-secret",""));
1063       sha1sum_blob(&b, &b);
1064       if( fossil_strcmp(zHash, blob_str(&b))==0 ){
1065         uid = db_int(0,
1066             "SELECT uid FROM user WHERE login='anonymous'"
1067             " AND length(cap)>0"
1068             " AND length(pw)>0"
1069             " AND %.17g+0.25>julianday('now')",
1070             rTime
1071         );
1072       }
1073       blob_reset(&b);
1074     }else{
1075       /* Cookies of the form "HASH/CODE/USER".  Search first in the
1076       ** local user table, then the user table for project CODE if we
1077       ** are part of a login-group.
1078       */
1079       uid = login_find_user(zUser, zHash);
1080       if( uid==0 && login_transfer_credentials(zUser,zArg,zHash) ){
1081         uid = login_find_user(zUser, zHash);
1082         if( uid ) record_login_attempt(zUser, zIpAddr, 1);
1083       }
1084     }
1085     sqlite3_snprintf(sizeof(g.zCsrfToken), g.zCsrfToken, "%.10s", zHash);
1086   }
1087 
1088   /* If no user found and the REMOTE_USER environment variable is set,
1089   ** then accept the value of REMOTE_USER as the user.
1090   */
1091   if( uid==0 ){
1092     const char *zRemoteUser = P("REMOTE_USER");
1093     if( zRemoteUser && db_get_boolean("remote_user_ok",0) ){
1094       uid = db_int(0, "SELECT uid FROM user WHERE login=%Q"
1095                       " AND length(cap)>0 AND length(pw)>0", zRemoteUser);
1096     }
1097   }
1098 
1099   /* If the request didn't provide a login cookie or the login cookie didn't
1100   ** match a known valid user, check the HTTP "Authorization" header and
1101   ** see if those credentials are valid for a known user.
1102   */
1103   if( uid==0 && db_get_boolean("http_authentication_ok",0) ){
1104     uid = login_basic_authentication(zIpAddr);
1105   }
1106 
1107   /* Check for magic query parameters "resid" (for the username) and
1108   ** "token" for the password.  Both values (if they exist) will be
1109   ** obfuscated.
1110   */
1111   if( uid==0 ){
1112     char *zUsr, *zPW;
1113     if( (zUsr = unobscure(P("resid")))!=0
1114      && (zPW = unobscure(P("token")))!=0
1115     ){
1116       char *zSha1Pw = sha1_shared_secret(zPW, zUsr, 0);
1117       uid = db_int(0, "SELECT uid FROM user"
1118                       " WHERE login=%Q"
1119                       " AND (constant_time_cmp(pw,%Q)=0"
1120                       "      OR constant_time_cmp(pw,%Q)=0)",
1121                       zUsr, zSha1Pw, zPW);
1122       fossil_free(zSha1Pw);
1123     }
1124   }
1125 
1126   /* If no user found yet, try to log in as "nobody" */
1127   if( uid==0 ){
1128     uid = db_int(0, "SELECT uid FROM user WHERE login='nobody'");
1129     if( uid==0 ){
1130       /* If there is no user "nobody", then make one up - with no privileges */
1131       uid = -1;
1132       zCap = "";
1133     }
1134     sqlite3_snprintf(sizeof(g.zCsrfToken), g.zCsrfToken, "none");
1135   }
1136 
1137   /* At this point, we know that uid!=0.  Find the privileges associated
1138   ** with user uid.
1139   */
1140   assert( uid!=0 );
1141   if( zCap==0 ){
1142     Stmt s;
1143     db_prepare(&s, "SELECT login, cap FROM user WHERE uid=%d", uid);
1144     if( db_step(&s)==SQLITE_ROW ){
1145       g.zLogin = db_column_malloc(&s, 0);
1146       zCap = db_column_malloc(&s, 1);
1147     }
1148     db_finalize(&s);
1149     if( zCap==0 ){
1150       zCap = "";
1151     }
1152   }
1153   if( g.fHttpTrace && g.zLogin ){
1154     fprintf(stderr, "# login: [%s] with capabilities [%s]\n", g.zLogin, zCap);
1155   }
1156 
1157   /* Set the global variables recording the userid and login.  The
1158   ** "nobody" user is a special case in that g.zLogin==0.
1159   */
1160   g.userUid = uid;
1161   if( fossil_strcmp(g.zLogin,"nobody")==0 ){
1162     g.zLogin = 0;
1163   }
1164   if( PB("isrobot") ){
1165     g.isHuman = 0;
1166   }else if( g.zLogin==0 ){
1167     g.isHuman = isHuman(P("HTTP_USER_AGENT"));
1168   }else{
1169     g.isHuman = 1;
1170   }
1171 
1172   /* Set the capabilities */
1173   login_replace_capabilities(zCap, 0);
1174 
1175   /* The auto-hyperlink setting allows hyperlinks to be displayed for users
1176   ** who do not have the "h" permission as long as their UserAgent string
1177   ** makes it appear that they are human.  Check to see if auto-hyperlink is
1178   ** enabled for this repository and make appropriate adjustments to the
1179   ** permission flags if it is.  This should be done before the permissions
1180   ** are (potentially) copied to the anonymous permission set; otherwise,
1181   ** those will be out-of-sync.
1182   */
1183   if( zCap[0]
1184    && !g.perm.Hyperlink
1185    && g.isHuman
1186    && db_get_boolean("auto-hyperlink",1)
1187   ){
1188     g.perm.Hyperlink = 1;
1189     g.javascriptHyperlink = 1;
1190   }
1191 
1192   /*
1193   ** At this point, the capabilities for the logged in user are not going
1194   ** to be modified anymore; therefore, we can copy them over to the ones
1195   ** for the anonymous user.
1196   **
1197   ** WARNING: In the future, please do not add code after this point that
1198   **          modifies the capabilities for the logged in user.
1199   */
1200   login_set_anon_nobody_capabilities();
1201 
1202   /* If the public-pages glob pattern is defined and REQUEST_URI matches
1203   ** one of the globs in public-pages, then also add in all default-perms
1204   ** permissions.
1205   */
1206   zPublicPages = db_get("public-pages",0);
1207   if( zPublicPages!=0 ){
1208     Glob *pGlob = glob_create(zPublicPages);
1209     const char *zUri = PD("REQUEST_URI","");
1210     zUri += (int)strlen(g.zTop);
1211     if( glob_match(pGlob, zUri) ){
1212       login_set_capabilities(db_get("default-perms", "u"), 0);
1213     }
1214     glob_free(pGlob);
1215   }
1216 }
1217 
1218 /*
1219 ** Memory of settings
1220 */
1221 static int login_anon_once = 1;
1222 
1223 /*
1224 ** Add to g.perm the default privileges of users "nobody" and/or "anonymous"
1225 ** as appropriate for the user g.zLogin.
1226 **
1227 ** This routine also sets up g.anon to be either a copy of g.perm for
1228 ** all logged in uses, or the privileges that would be available to "anonymous"
1229 ** if g.zLogin==0 (meaning that the user is "nobody").
1230 */
login_set_anon_nobody_capabilities(void)1231 void login_set_anon_nobody_capabilities(void){
1232   if( login_anon_once ){
1233     const char *zCap;
1234     /* All users get privileges from "nobody" */
1235     zCap = db_text("", "SELECT cap FROM user WHERE login = 'nobody'");
1236     login_set_capabilities(zCap, 0);
1237     zCap = db_text("", "SELECT cap FROM user WHERE login = 'anonymous'");
1238     if( g.zLogin && fossil_strcmp(g.zLogin, "nobody")!=0 ){
1239       /* All logged-in users inherit privileges from "anonymous" */
1240       login_set_capabilities(zCap, 0);
1241       g.anon = g.perm;
1242     }else{
1243       /* Record the privileges of anonymous in g.anon */
1244       g.anon = g.perm;
1245       login_set_capabilities(zCap, LOGIN_ANON);
1246     }
1247     login_anon_once = 0;
1248   }
1249 }
1250 
1251 /*
1252 ** Flags passed into the 2nd argument of login_set/replace_capabilities().
1253 */
1254 #if INTERFACE
1255 #define LOGIN_IGNORE_UV  0x01         /* Ignore "u" and "v" */
1256 #define LOGIN_ANON       0x02         /* Use g.anon instead of g.perm */
1257 #endif
1258 
1259 /*
1260 ** Adds all capability flags in zCap to g.perm or g.anon.
1261 */
login_set_capabilities(const char * zCap,unsigned flags)1262 void login_set_capabilities(const char *zCap, unsigned flags){
1263   int i;
1264   FossilUserPerms *p = (flags & LOGIN_ANON) ? &g.anon : &g.perm;
1265   if(NULL==zCap){
1266     return;
1267   }
1268   for(i=0; zCap[i]; i++){
1269     switch( zCap[i] ){
1270       case 's':   p->Setup = 1; /* Fall thru into Admin */
1271       case 'a':   p->Admin = p->RdTkt = p->WrTkt = p->Zip =
1272                              p->RdWiki = p->WrWiki = p->NewWiki =
1273                              p->ApndWiki = p->Hyperlink = p->Clone =
1274                              p->NewTkt = p->Password = p->RdAddr =
1275                              p->TktFmt = p->Attach = p->ApndTkt =
1276                              p->ModWiki = p->ModTkt =
1277                              p->RdForum = p->WrForum = p->ModForum =
1278                              p->WrTForum = p->AdminForum = p->Chat =
1279                              p->EmailAlert = p->Announce = p->Debug = 1;
1280                              /* Fall thru into Read/Write */
1281       case 'i':   p->Read = p->Write = 1;                      break;
1282       case 'o':   p->Read = 1;                                 break;
1283       case 'z':   p->Zip = 1;                                  break;
1284 
1285       case 'h':   p->Hyperlink = 1;                            break;
1286       case 'g':   p->Clone = 1;                                break;
1287       case 'p':   p->Password = 1;                             break;
1288 
1289       case 'j':   p->RdWiki = 1;                               break;
1290       case 'k':   p->WrWiki = p->RdWiki = p->ApndWiki =1;      break;
1291       case 'm':   p->ApndWiki = 1;                             break;
1292       case 'f':   p->NewWiki = 1;                              break;
1293       case 'l':   p->ModWiki = 1;                              break;
1294 
1295       case 'e':   p->RdAddr = 1;                               break;
1296       case 'r':   p->RdTkt = 1;                                break;
1297       case 'n':   p->NewTkt = 1;                               break;
1298       case 'w':   p->WrTkt = p->RdTkt = p->NewTkt =
1299                   p->ApndTkt = 1;                              break;
1300       case 'c':   p->ApndTkt = 1;                              break;
1301       case 'q':   p->ModTkt = 1;                               break;
1302       case 't':   p->TktFmt = 1;                               break;
1303       case 'b':   p->Attach = 1;                               break;
1304       case 'x':   p->Private = 1;                              break;
1305       case 'y':   p->WrUnver = 1;                              break;
1306 
1307       case '6':   p->AdminForum = 1;
1308       case '5':   p->ModForum = 1;
1309       case '4':   p->WrTForum = 1;
1310       case '3':   p->WrForum = 1;
1311       case '2':   p->RdForum = 1;                              break;
1312 
1313       case '7':   p->EmailAlert = 1;                           break;
1314       case 'A':   p->Announce = 1;                             break;
1315       case 'C':   p->Chat = 1;                                 break;
1316       case 'D':   p->Debug = 1;                                break;
1317 
1318       /* The "u" privilege recursively
1319       ** inherits all privileges of the user named "reader" */
1320       case 'u': {
1321         if( p->XReader==0 ){
1322           const char *zUser;
1323           p->XReader = 1;
1324           zUser = db_text("", "SELECT cap FROM user WHERE login='reader'");
1325           login_set_capabilities(zUser, flags);
1326         }
1327         break;
1328       }
1329 
1330       /* The "v" privilege recursively
1331       ** inherits all privileges of the user named "developer" */
1332       case 'v': {
1333         if( p->XDeveloper==0 ){
1334           const char *zDev;
1335           p->XDeveloper = 1;
1336           zDev = db_text("", "SELECT cap FROM user WHERE login='developer'");
1337           login_set_capabilities(zDev, flags);
1338         }
1339         break;
1340       }
1341     }
1342   }
1343 }
1344 
1345 /*
1346 ** Zeroes out g.perm and calls login_set_capabilities(zCap,flags).
1347 */
login_replace_capabilities(const char * zCap,unsigned flags)1348 void login_replace_capabilities(const char *zCap, unsigned flags){
1349   memset(&g.perm, 0, sizeof(g.perm));
1350   login_set_capabilities(zCap, flags);
1351   login_anon_once = 1;
1352 }
1353 
1354 /*
1355 ** If the current login lacks any of the capabilities listed in
1356 ** the input, then return 0.  If all capabilities are present, then
1357 ** return 1.
1358 **
1359 ** As a special case, the 'L' pseudo-capability ID means "is logged
1360 ** in" and will return true for any non-guest user.
1361 */
login_has_capability(const char * zCap,int nCap,u32 flgs)1362 int login_has_capability(const char *zCap, int nCap, u32 flgs){
1363   int i;
1364   int rc = 1;
1365   FossilUserPerms *p = (flgs & LOGIN_ANON) ? &g.anon : &g.perm;
1366   if( nCap<0 ) nCap = strlen(zCap);
1367   for(i=0; i<nCap && rc && zCap[i]; i++){
1368     switch( zCap[i] ){
1369       case 'a':  rc = p->Admin;     break;
1370       case 'b':  rc = p->Attach;    break;
1371       case 'c':  rc = p->ApndTkt;   break;
1372       /* d unused: see comment in capabilities.c */
1373       case 'e':  rc = p->RdAddr;    break;
1374       case 'f':  rc = p->NewWiki;   break;
1375       case 'g':  rc = p->Clone;     break;
1376       case 'h':  rc = p->Hyperlink; break;
1377       case 'i':  rc = p->Write;     break;
1378       case 'j':  rc = p->RdWiki;    break;
1379       case 'k':  rc = p->WrWiki;    break;
1380       case 'l':  rc = p->ModWiki;   break;
1381       case 'm':  rc = p->ApndWiki;  break;
1382       case 'n':  rc = p->NewTkt;    break;
1383       case 'o':  rc = p->Read;      break;
1384       case 'p':  rc = p->Password;  break;
1385       case 'q':  rc = p->ModTkt;    break;
1386       case 'r':  rc = p->RdTkt;     break;
1387       case 's':  rc = p->Setup;     break;
1388       case 't':  rc = p->TktFmt;    break;
1389       /* case 'u': READER    */
1390       /* case 'v': DEVELOPER */
1391       case 'w':  rc = p->WrTkt;     break;
1392       case 'x':  rc = p->Private;   break;
1393       case 'y':  rc = p->WrUnver;   break;
1394       case 'z':  rc = p->Zip;       break;
1395       case '2':  rc = p->RdForum;   break;
1396       case '3':  rc = p->WrForum;   break;
1397       case '4':  rc = p->WrTForum;  break;
1398       case '5':  rc = p->ModForum;  break;
1399       case '6':  rc = p->AdminForum;break;
1400       case '7':  rc = p->EmailAlert;break;
1401       case 'A':  rc = p->Announce;  break;
1402       case 'C':  rc = p->Chat;      break;
1403       case 'D':  rc = p->Debug;     break;
1404       case 'L':  rc = g.zLogin && *g.zLogin; break;
1405       /* Mainenance reminder: '@' should not be used because
1406          it would semantically collide with the @ in the
1407          capexpr TH1 command. */
1408       default:   rc = 0;            break;
1409     }
1410   }
1411   return rc;
1412 }
1413 
1414 /*
1415 ** Change the login to zUser.
1416 */
login_as_user(const char * zUser)1417 void login_as_user(const char *zUser){
1418   char *zCap = "";   /* New capabilities */
1419 
1420   /* Turn off all capabilities from prior logins */
1421   memset( &g.perm, 0, sizeof(g.perm) );
1422 
1423   /* Set the global variables recording the userid and login.  The
1424   ** "nobody" user is a special case in that g.zLogin==0.
1425   */
1426   g.userUid = db_int(0, "SELECT uid FROM user WHERE login=%Q", zUser);
1427   if( g.userUid==0 ){
1428     zUser = 0;
1429     g.userUid = db_int(0, "SELECT uid FROM user WHERE login='nobody'");
1430   }
1431   if( g.userUid ){
1432     zCap = db_text("", "SELECT cap FROM user WHERE uid=%d", g.userUid);
1433   }
1434   if( fossil_strcmp(zUser,"nobody")==0 ) zUser = 0;
1435   g.zLogin = fossil_strdup(zUser);
1436 
1437   /* Set the capabilities */
1438   login_set_capabilities(zCap, 0);
1439   login_anon_once = 1;
1440   login_set_anon_nobody_capabilities();
1441 }
1442 
1443 /*
1444 ** Return true if the user is "nobody"
1445 */
login_is_nobody(void)1446 int login_is_nobody(void){
1447   return g.zLogin==0 || g.zLogin[0]==0 || fossil_strcmp(g.zLogin,"nobody")==0;
1448 }
1449 
1450 /*
1451 ** Return true if the user is a specific individual, not "nobody" or
1452 ** "anonymous".
1453 */
login_is_individual(void)1454 int login_is_individual(void){
1455   return g.zLogin!=0 && g.zLogin[0]!=0 && fossil_strcmp(g.zLogin,"nobody")!=0
1456            && fossil_strcmp(g.zLogin,"anonymous")!=0;
1457 }
1458 
1459 /*
1460 ** Return the login name.  If no login name is specified, return "nobody".
1461 */
login_name(void)1462 const char *login_name(void){
1463   return (g.zLogin && g.zLogin[0]) ? g.zLogin : "nobody";
1464 }
1465 
1466 /*
1467 ** Call this routine when the credential check fails.  It causes
1468 ** a redirect to the "login" page.
1469 */
login_needed(int anonOk)1470 void login_needed(int anonOk){
1471 #ifdef FOSSIL_ENABLE_JSON
1472   if(g.json.isJsonMode){
1473     json_err( FSL_JSON_E_DENIED, NULL, 1 );
1474     fossil_exit(0);
1475     /* NOTREACHED */
1476     assert(0);
1477   }else
1478 #endif /* FOSSIL_ENABLE_JSON */
1479   {
1480     const char *zQS = P("QUERY_STRING");
1481     const char *zPathInfo = PD("PATH_INFO","");
1482     Blob redir;
1483     blob_init(&redir, 0, 0);
1484     if( zPathInfo[0]=='/' ) zPathInfo++; /* skip leading slash */
1485     if( fossil_wants_https(1) ){
1486       blob_appendf(&redir, "%s/login?g=%T", g.zHttpsURL, zPathInfo);
1487     }else{
1488       blob_appendf(&redir, "%R/login?g=%T", zPathInfo);
1489     }
1490     if( zQS && zQS[0] ){
1491       blob_appendf(&redir, "%%3f%T", zQS);
1492     }
1493     if( anonOk ) blob_append(&redir, "&anon", 5);
1494     cgi_redirect(blob_str(&redir));
1495     /* NOTREACHED */
1496     assert(0);
1497   }
1498 }
1499 
1500 /*
1501 ** Call this routine if the user lacks g.perm.Hyperlink permission.  If
1502 ** the anonymous user has Hyperlink permission, then paint a mesage
1503 ** to inform the user that much more information is available by
1504 ** logging in as anonymous.
1505 */
login_anonymous_available(void)1506 void login_anonymous_available(void){
1507   if( !g.perm.Hyperlink && g.anon.Hyperlink ){
1508     const char *zUrl = PD("PATH_INFO", "");
1509     @ <p>Many <span class="disabled">hyperlinks are disabled.</span><br />
1510     @ Use <a href="%R/login?anon=1&amp;g=%T(zUrl)">anonymous login</a>
1511     @ to enable hyperlinks.</p>
1512   }
1513 }
1514 
1515 /*
1516 ** While rendering a form, call this routine to add the Anti-CSRF token
1517 ** as a hidden element of the form.
1518 */
login_insert_csrf_secret(void)1519 void login_insert_csrf_secret(void){
1520   @ <input type="hidden" name="csrf" value="%s(g.zCsrfToken)" />
1521 }
1522 
1523 /*
1524 ** Before using the results of a form, first call this routine to verify
1525 ** that this Anti-CSRF token is present and is valid.  If the Anti-CSRF token
1526 ** is missing or is incorrect, that indicates a cross-site scripting attack.
1527 ** If the event of an attack is detected, an error message is generated and
1528 ** all further processing is aborted.
1529 */
login_verify_csrf_secret(void)1530 void login_verify_csrf_secret(void){
1531   if( g.okCsrf ) return;
1532   if( fossil_strcmp(P("csrf"), g.zCsrfToken)==0 ){
1533     g.okCsrf = 1;
1534     return;
1535   }
1536   fossil_fatal("Cross-site request forgery attempt");
1537 }
1538 
1539 /*
1540 ** Check to see if the candidate username zUserID is already used.
1541 ** Return 1 if it is already in use.  Return 0 if the name is
1542 ** available for a self-registeration.
1543 */
login_self_choosen_userid_already_exists(const char * zUserID)1544 static int login_self_choosen_userid_already_exists(const char *zUserID){
1545   int rc = db_exists(
1546     "SELECT 1 FROM user WHERE login=%Q "
1547     "UNION ALL "
1548     "SELECT 1 FROM event WHERE user=%Q OR euser=%Q",
1549     zUserID, zUserID, zUserID
1550   );
1551   return rc;
1552 }
1553 
1554 /*
1555 ** Check an email address and confirm that it is valid for self-registration.
1556 ** The email address is known already to be well-formed.  Return true
1557 ** if the email address is on the allowed list.
1558 **
1559 ** The default behavior is that any valid email address is accepted.
1560 ** But if the "auth-sub-email" setting exists and is not empty, then
1561 ** it is a comma-separated list of GLOB patterns for email addresses
1562 ** that are authorized to self-register.
1563 */
authorized_subscription_email(const char * zEAddr)1564 int authorized_subscription_email(const char *zEAddr){
1565   char *zGlob = db_get("auth-sub-email",0);
1566   Glob *pGlob;
1567   char *zAddr;
1568   int rc;
1569 
1570   if( zGlob==0 || zGlob[0]==0 ) return 1;
1571   zGlob = fossil_strtolwr(fossil_strdup(zGlob));
1572   pGlob = glob_create(zGlob);
1573   fossil_free(zGlob);
1574 
1575   zAddr = fossil_strtolwr(fossil_strdup(zEAddr));
1576   rc = glob_match(pGlob, zAddr);
1577   fossil_free(zAddr);
1578   glob_free(pGlob);
1579   return rc!=0;
1580 }
1581 
1582 /*
1583 ** WEBPAGE: register
1584 **
1585 ** Page to allow users to self-register.  The "self-register" setting
1586 ** must be enabled for this page to operate.
1587 */
register_page(void)1588 void register_page(void){
1589   const char *zUserID, *zPasswd, *zConfirm, *zEAddr;
1590   const char *zDName;
1591   unsigned int uSeed;
1592   const char *zDecoded;
1593   int iErrLine = -1;
1594   const char *zErr = 0;
1595   int captchaIsCorrect = 0; /* True on a correct captcha */
1596   char *zCaptcha = "";      /* Value of the captcha text */
1597   char *zPerms;             /* Permissions for the default user */
1598   int canDoAlerts = 0;      /* True if receiving email alerts is possible */
1599   int doAlerts = 0;         /* True if subscription is wanted too */
1600   if( !db_get_boolean("self-register", 0) ){
1601     style_header("Registration not possible");
1602     @ <p>This project does not allow user self-registration. Please contact the
1603     @ project administrator to obtain an account.</p>
1604     style_finish_page();
1605     return;
1606   }
1607   zPerms = db_get("default-perms", "u");
1608 
1609   /* Prompt the user for email alerts if this repository is configured for
1610   ** email alerts and if the default permissions include "7" */
1611   canDoAlerts = alert_tables_exist() && (db_int(0,
1612     "SELECT fullcap(%Q) GLOB '*7*'", zPerms
1613   ) || db_get_boolean("selfreg-verify",0));
1614   doAlerts = canDoAlerts && atoi(PD("alerts","1"))!=0;
1615 
1616   zUserID = PDT("u","");
1617   zPasswd = PDT("p","");
1618   zConfirm = PDT("cp","");
1619   zEAddr = PDT("ea","");
1620   zDName = PDT("dn","");
1621 
1622   /* Verify user imputs */
1623   if( P("new")==0 || !cgi_csrf_safe(1) ){
1624     /* This is not a valid form submission.  Fall through into
1625     ** the form display */
1626   }else if( (captchaIsCorrect = captcha_is_correct(1))==0 ){
1627     iErrLine = 6;
1628     zErr = "Incorrect CAPTCHA";
1629   }else if( strlen(zUserID)<6 ){
1630     iErrLine = 1;
1631     zErr = "User ID too short. Must be at least 6 characters.";
1632   }else if( sqlite3_strglob("*[^-a-zA-Z0-9_.]*",zUserID)==0 ){
1633     iErrLine = 1;
1634     zErr = "User ID may not contain spaces or special characters.";
1635   }else if( zDName[0]==0 ){
1636     iErrLine = 2;
1637     zErr = "Required";
1638   }else if( zEAddr[0]==0 ){
1639     iErrLine = 3;
1640     zErr = "Required";
1641   }else if( email_address_is_valid(zEAddr,0)==0 ){
1642     iErrLine = 3;
1643     zErr = "Not a valid email address";
1644   }else if( authorized_subscription_email(zEAddr)==0 ){
1645     iErrLine = 3;
1646     zErr = "Not an authorized email address";
1647   }else if( strlen(zPasswd)<6 ){
1648     iErrLine = 4;
1649     zErr = "Password must be at least 6 characters long";
1650   }else if( fossil_strcmp(zPasswd,zConfirm)!=0 ){
1651     iErrLine = 5;
1652     zErr = "Passwords do not match";
1653   }else if( login_self_choosen_userid_already_exists(zUserID) ){
1654     iErrLine = 1;
1655     zErr = "This User ID is already taken. Choose something different.";
1656   }else if(
1657       /* If the email is found anywhere in USER.INFO... */
1658       db_exists("SELECT 1 FROM user WHERE info LIKE '%%%q%%'", zEAddr)
1659     ||
1660       /* Or if the email is a verify subscriber email with an associated
1661       ** user... */
1662       (alert_tables_exist() &&
1663        db_exists(
1664          "SELECT 1 FROM subscriber WHERE semail=%Q AND suname IS NOT NULL"
1665          " AND sverified",zEAddr))
1666    ){
1667     iErrLine = 3;
1668     zErr = "This email address is already claimed by another user";
1669   }else{
1670     /* If all of the tests above have passed, that means that the submitted
1671     ** form contains valid data and we can proceed to create the new login */
1672     Blob sql;
1673     int uid;
1674     char *zPass = sha1_shared_secret(zPasswd, zUserID, 0);
1675     const char *zStartPerms = zPerms;
1676     if( db_get_boolean("selfreg-verify",0) ){
1677       /* If email verification is required for self-registration, initalize
1678       ** the new user capabilities to just "7" (Sign up for email).  The
1679       ** full "default-perms" permissions will be added when they click
1680       ** the verification link on the email they are sent. */
1681       zStartPerms = "7";
1682     }
1683     blob_init(&sql, 0, 0);
1684     blob_append_sql(&sql,
1685        "INSERT INTO user(login,pw,cap,info,mtime)\n"
1686        "VALUES(%Q,%Q,%Q,"
1687        "'%q <%q>\nself-register from ip %q on '||datetime('now'),now())",
1688        zUserID, zPass, zStartPerms, zDName, zEAddr, g.zIpAddr);
1689     fossil_free(zPass);
1690     db_unprotect(PROTECT_USER);
1691     db_multi_exec("%s", blob_sql_text(&sql));
1692     db_protect_pop();
1693     uid = db_int(0, "SELECT uid FROM user WHERE login=%Q", zUserID);
1694     login_set_user_cookie(zUserID, uid, NULL, 0);
1695     if( doAlerts ){
1696       /* Also make the new user a subscriber. */
1697       Blob hdr, body;
1698       AlertSender *pSender;
1699       const char *zCode;  /* New subscriber code (in hex) */
1700       const char *zGoto = P("g");
1701       int nsub = 0;
1702       char ssub[20];
1703       CapabilityString *pCap;
1704       pCap = capability_add(0, zPerms);
1705       capability_expand(pCap);
1706       ssub[nsub++] = 'a';
1707       if( capability_has_any(pCap,"o") ) ssub[nsub++] = 'c';
1708       if( capability_has_any(pCap,"2") ) ssub[nsub++] = 'f';
1709       if( capability_has_any(pCap,"r") ) ssub[nsub++] = 't';
1710       if( capability_has_any(pCap,"j") ) ssub[nsub++] = 'w';
1711       ssub[nsub] = 0;
1712       capability_free(pCap);
1713       /* Also add the user to the subscriber table. */
1714       zCode = db_text(0,
1715         "INSERT INTO subscriber(semail,suname,"
1716         "  sverified,sdonotcall,sdigest,ssub,sctime,mtime,smip,lastContact)"
1717         " VALUES(%Q,%Q,%d,0,%d,%Q,now(),now(),%Q,now()/86400)"
1718         " ON CONFLICT(semail) DO UPDATE"
1719         "   SET suname=excluded.suname"
1720         " RETURNING hex(subscriberCode);",
1721         /* semail */    zEAddr,
1722         /* suname */    zUserID,
1723         /* sverified */ 0,
1724         /* sdigest */   0,
1725         /* ssub */      ssub,
1726         /* smip */      g.zIpAddr
1727       );
1728       if( db_exists("SELECT 1 FROM subscriber WHERE semail=%Q"
1729                     "  AND sverified", zEAddr) ){
1730         /* This the case where the user was formerly a verified subscriber
1731         ** and here they have also registered as a user as well.  It is
1732         ** not necessary to repeat the verfication step */
1733         redirect_to_g();
1734       }
1735       /* A verification email */
1736       pSender = alert_sender_new(0,0);
1737       blob_init(&hdr,0,0);
1738       blob_init(&body,0,0);
1739       blob_appendf(&hdr, "To: <%s>\n", zEAddr);
1740       blob_appendf(&hdr, "Subject: Subscription verification\n");
1741       alert_append_confirmation_message(&body, zCode);
1742       alert_send(pSender, &hdr, &body, 0);
1743       style_header("Email Verification");
1744       if( pSender->zErr ){
1745         @ <h1>Internal Error</h1>
1746         @ <p>The following internal error was encountered while trying
1747         @ to send the confirmation email:
1748         @ <blockquote><pre>
1749         @ %h(pSender->zErr)
1750         @ </pre></blockquote>
1751       }else{
1752         @ <p>An email has been sent to "%h(zEAddr)". That email contains a
1753         @ hyperlink that you must click to activate your account.</p>
1754       }
1755       alert_sender_free(pSender);
1756       if( zGoto ){
1757         @ <p><a href='%h(zGoto)'>Continue</a>
1758       }
1759       style_finish_page();
1760       return;
1761     }
1762     redirect_to_g();
1763   }
1764 
1765   /* Prepare the captcha. */
1766   if( captchaIsCorrect ){
1767     uSeed = strtoul(P("captchaseed"),0,10);
1768   }else{
1769     uSeed = captcha_seed();
1770   }
1771   zDecoded = captcha_decode(uSeed);
1772   zCaptcha = captcha_render(zDecoded);
1773 
1774   style_header("Register");
1775   /* Print out the registration form. */
1776   g.perm.Hyperlink = 1;  /* Artificially enable hyperlinks */
1777   form_begin(0, "%R/register");
1778   if( P("g") ){
1779     @ <input type="hidden" name="g" value="%h(P("g"))" />
1780   }
1781   @ <p><input type="hidden" name="captchaseed" value="%u(uSeed)" />
1782   @ <table class="login_out">
1783   @ <tr>
1784   @   <td class="form_label" align="right" id="uid">User ID:</td>
1785   @   <td><input aria-labelledby="uid" type="text" name="u" \
1786   @ value="%h(zUserID)" size="30"></td>
1787   @
1788   if( iErrLine==1 ){
1789     @ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
1790   }
1791   @ <tr>
1792   @   <td class="form_label" align="right" id="dpyname">Display Name:</td>
1793   @   <td><input aria-labelledby="dpyname" type="text" name="dn" \
1794   @ value="%h(zDName)" size="30"></td>
1795   @ </tr>
1796   if( iErrLine==2 ){
1797     @ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
1798   }
1799   @ </tr>
1800   @ <tr>
1801   @   <td class="form_label" align="right" id="emaddr">Email Address:</td>
1802   @   <td><input aria-labelledby="emaddr" type="text" name="ea" \
1803   @ value="%h(zEAddr)" size="30"></td>
1804   @ </tr>
1805   if( iErrLine==3 ){
1806     @ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
1807   }
1808   if( canDoAlerts ){
1809     int a = atoi(PD("alerts","1"));
1810     @ <tr>
1811     @   <td class="form_label" align="right" id="emalrt">Email&nbsp;Alerts?</td>
1812     @   <td><select aria-labelledby="emalrt" size='1' name='alerts'>
1813     @       <option value="1" %s(a?"selected":"")>Yes</option>
1814     @       <option value="0" %s(!a?"selected":"")>No</option>
1815     @   </select></td></tr>
1816   }
1817   @ <tr>
1818   @   <td class="form_label" align="right" id="pswd">Password:</td>
1819   @   <td><input aria-labelledby="pswd" type="password" name="p" \
1820   @ value="%h(zPasswd)" size="30"> \
1821   if( zPasswd[0]==0 ){
1822     char *zRPW = fossil_random_password(12);
1823     @ Password suggestion: %z(zRPW)</td>
1824   }else{
1825     @ </td>
1826   }
1827   @ <tr>
1828   if( iErrLine==4 ){
1829     @ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
1830   }
1831   @ <tr>
1832   @   <td class="form_label" align="right" id="pwcfrm">Confirm:</td>
1833   @   <td><input aria-labelledby="pwcfrm" type="password" name="cp" \
1834   @ value="%h(zConfirm)" size="30"></td>
1835   @ </tr>
1836   if( iErrLine==5 ){
1837     @ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
1838   }
1839   @ <tr>
1840   @   <td class="form_label" align="right" id="cptcha">Captcha:</td>
1841   @   <td><input type="text" name="captcha" aria-labelledby="cptcha" \
1842   @ value="%h(captchaIsCorrect?zDecoded:"")" size="30">
1843   captcha_speakit_button(uSeed, "Speak the captcha text");
1844   @   </td>
1845   @ </tr>
1846   if( iErrLine==6 ){
1847     @ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
1848   }
1849   @ <tr><td></td>
1850   @ <td><input type="submit" name="new" value="Register" /></td></tr>
1851   @ </table>
1852   @ <div class="captcha"><table class="captcha"><tr><td><pre class="captcha">
1853   @ %h(zCaptcha)
1854   @ </pre>
1855   @ Enter this 8-letter code in the "Captcha" box above.
1856   @ </td></tr></table></div>
1857   @ </form>
1858   style_finish_page();
1859 
1860   free(zCaptcha);
1861 }
1862 
1863 /*
1864 ** Run SQL on the repository database for every repository in our
1865 ** login group.  The SQL is run in a separate database connection.
1866 **
1867 ** Any members of the login group whose repository database file
1868 ** cannot be found is silently removed from the group.
1869 **
1870 ** Error messages accumulate and are returned in *pzErrorMsg.  The
1871 ** memory used to hold these messages should be freed using
1872 ** fossil_free() if one desired to avoid a memory leak.  The
1873 ** zPrefix and zSuffix strings surround each error message.
1874 **
1875 ** Return the number of errors.
1876 */
login_group_sql(const char * zSql,const char * zPrefix,const char * zSuffix,char ** pzErrorMsg)1877 int login_group_sql(
1878   const char *zSql,        /* The SQL to run */
1879   const char *zPrefix,     /* Prefix to each error message */
1880   const char *zSuffix,     /* Suffix to each error message */
1881   char **pzErrorMsg        /* Write error message here, if not NULL */
1882 ){
1883   sqlite3 *pPeer;          /* Connection to another database */
1884   int nErr = 0;            /* Number of errors seen so far */
1885   int rc;                  /* Result code from subroutine calls */
1886   char *zErr;              /* SQLite error text */
1887   char *zSelfCode;         /* Project code for ourself */
1888   Blob err;                /* Accumulate errors here */
1889   Stmt q;                  /* Query of all peer-* entries in CONFIG */
1890 
1891   if( zPrefix==0 ) zPrefix = "";
1892   if( zSuffix==0 ) zSuffix = "";
1893   if( pzErrorMsg ) *pzErrorMsg = 0;
1894   zSelfCode = abbreviated_project_code(db_get("project-code", "x"));
1895   blob_zero(&err);
1896   db_prepare(&q,
1897     "SELECT name, value FROM config"
1898     " WHERE name GLOB 'peer-repo-*'"
1899     "   AND name <> 'peer-repo-%q'"
1900     " ORDER BY +value",
1901     zSelfCode
1902   );
1903   while( db_step(&q)==SQLITE_ROW ){
1904     const char *zRepoName = db_column_text(&q, 1);
1905     if( file_size(zRepoName, ExtFILE)<0 ){
1906       /* Silently remove non-existent repositories from the login group. */
1907       const char *zLabel = db_column_text(&q, 0);
1908       db_unprotect(PROTECT_CONFIG);
1909       db_multi_exec(
1910          "DELETE FROM config WHERE name GLOB 'peer-*-%q'",
1911          &zLabel[10]
1912       );
1913       db_protect_pop();
1914       continue;
1915     }
1916     rc = sqlite3_open_v2(
1917          zRepoName, &pPeer,
1918          SQLITE_OPEN_READWRITE,
1919          g.zVfsName
1920     );
1921     if( rc!=SQLITE_OK ){
1922       blob_appendf(&err, "%s%s: %s%s", zPrefix, zRepoName,
1923                    sqlite3_errmsg(pPeer), zSuffix);
1924       nErr++;
1925       sqlite3_close(pPeer);
1926       continue;
1927     }
1928     sqlite3_create_function(pPeer, "shared_secret", 3, SQLITE_UTF8,
1929                             0, sha1_shared_secret_sql_function, 0, 0);
1930     sqlite3_create_function(pPeer, "now", 0,SQLITE_UTF8,0,db_now_function,0,0);
1931     sqlite3_busy_timeout(pPeer, 5000);
1932     zErr = 0;
1933     rc = sqlite3_exec(pPeer, zSql, 0, 0, &zErr);
1934     if( zErr ){
1935       blob_appendf(&err, "%s%s: %s%s", zPrefix, zRepoName, zErr, zSuffix);
1936       sqlite3_free(zErr);
1937       nErr++;
1938     }else if( rc!=SQLITE_OK ){
1939       blob_appendf(&err, "%s%s: %s%s", zPrefix, zRepoName,
1940                    sqlite3_errmsg(pPeer), zSuffix);
1941       nErr++;
1942     }
1943     sqlite3_close(pPeer);
1944   }
1945   db_finalize(&q);
1946   if( pzErrorMsg && blob_size(&err)>0 ){
1947     *pzErrorMsg = fossil_strdup(blob_str(&err));
1948   }
1949   blob_reset(&err);
1950   fossil_free(zSelfCode);
1951   return nErr;
1952 }
1953 
1954 /*
1955 ** Attempt to join a login-group.
1956 **
1957 ** If problems arise, leave an error message in *pzErrMsg.
1958 */
login_group_join(const char * zRepo,int bPwRequired,const char * zLogin,const char * zPassword,const char * zNewName,char ** pzErrMsg)1959 void login_group_join(
1960   const char *zRepo,         /* Repository file in the login group */
1961   int bPwRequired,           /* True if the login,password is required */
1962   const char *zLogin,        /* Login name for the other repo */
1963   const char *zPassword,     /* Password to prove we are authorized to join */
1964   const char *zNewName,      /* Name of new login group if making a new one */
1965   char **pzErrMsg            /* Leave an error message here */
1966 ){
1967   Blob fullName;             /* Blob for finding full pathnames */
1968   sqlite3 *pOther;           /* The other repository */
1969   int rc;                    /* Return code from sqlite3 functions */
1970   char *zOtherProjCode;      /* Project code for pOther */
1971   char *zSelfRepo;           /* Name of our repository */
1972   char *zSelfLabel;          /* Project-name for our repository */
1973   char *zSelfProjCode;       /* Our project-code */
1974   char *zSql;                /* SQL to run on all peers */
1975   const char *zSelf;         /* The ATTACH name of our repository */
1976 
1977   *pzErrMsg = 0;   /* Default to no errors */
1978   zSelf = "repository";
1979 
1980   /* Get the full pathname of the other repository */
1981   file_canonical_name(zRepo, &fullName, 0);
1982   zRepo = fossil_strdup(blob_str(&fullName));
1983   blob_reset(&fullName);
1984 
1985   /* Get the full pathname for our repository.  Also the project code
1986   ** and project name for ourself. */
1987   file_canonical_name(g.zRepositoryName, &fullName, 0);
1988   zSelfRepo = fossil_strdup(blob_str(&fullName));
1989   blob_reset(&fullName);
1990   zSelfProjCode = db_get("project-code", "unknown");
1991   zSelfLabel = db_get("project-name", 0);
1992   if( zSelfLabel==0 ){
1993     zSelfLabel = zSelfProjCode;
1994   }
1995 
1996   /* Make sure we are not trying to join ourselves */
1997   if( fossil_strcmp(zRepo, zSelfRepo)==0 ){
1998     *pzErrMsg = mprintf("The \"other\" repository is the same as this one.");
1999     return;
2000   }
2001 
2002   /* Make sure the other repository is a valid Fossil database */
2003   if( file_size(zRepo, ExtFILE)<0 ){
2004     *pzErrMsg = mprintf("repository file \"%s\" does not exist", zRepo);
2005     return;
2006   }
2007   rc = sqlite3_open_v2(
2008        zRepo, &pOther,
2009        SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE,
2010        g.zVfsName
2011   );
2012   if( rc!=SQLITE_OK ){
2013     *pzErrMsg = fossil_strdup(sqlite3_errmsg(pOther));
2014   }else{
2015     rc = sqlite3_exec(pOther, "SELECT count(*) FROM user", 0, 0, pzErrMsg);
2016   }
2017   sqlite3_close(pOther);
2018   if( rc ) return;
2019 
2020   /* Attach the other repository.  Make sure the username/password is
2021   ** valid and has Setup permission.
2022   */
2023   db_attach(zRepo, "other");
2024   zOtherProjCode = db_text("x", "SELECT value FROM other.config"
2025                                 " WHERE name='project-code'");
2026   if( bPwRequired ){
2027     char *zPwHash;             /* Password hash on pOther */
2028     zPwHash = sha1_shared_secret(zPassword, zLogin, zOtherProjCode);
2029     if( !db_exists(
2030       "SELECT 1 FROM other.user"
2031       " WHERE login=%Q AND cap GLOB '*s*'"
2032       "   AND (pw=%Q OR pw=%Q)",
2033       zLogin, zPassword, zPwHash)
2034     ){
2035       db_detach("other");
2036       *pzErrMsg = "The supplied username/password does not correspond to a"
2037                   " user Setup permission on the other repository.";
2038       return;
2039     }
2040   }
2041 
2042   /* Create all the necessary CONFIG table entries on both the
2043   ** other repository and on our own repository.
2044   */
2045   zSelfProjCode = abbreviated_project_code(zSelfProjCode);
2046   zOtherProjCode = abbreviated_project_code(zOtherProjCode);
2047   db_begin_transaction();
2048   db_unprotect(PROTECT_CONFIG);
2049   db_multi_exec(
2050     "DELETE FROM \"%w\".config WHERE name GLOB 'peer-*';"
2051     "INSERT INTO \"%w\".config(name,value) VALUES('peer-repo-%q',%Q);"
2052     "INSERT INTO \"%w\".config(name,value) "
2053     "  SELECT 'peer-name-%q', value FROM other.config"
2054     "   WHERE name='project-name';",
2055     zSelf,
2056     zSelf, zOtherProjCode, zRepo,
2057     zSelf, zOtherProjCode
2058   );
2059   db_multi_exec(
2060     "INSERT OR IGNORE INTO other.config(name,value)"
2061     " VALUES('login-group-name',%Q);"
2062     "INSERT OR IGNORE INTO other.config(name,value)"
2063     " VALUES('login-group-code',lower(hex(randomblob(8))));",
2064     zNewName
2065   );
2066   db_multi_exec(
2067     "REPLACE INTO \"%w\".config(name,value)"
2068     "  SELECT name, value FROM other.config"
2069     "   WHERE name GLOB 'peer-*' OR name GLOB 'login-group-*'",
2070     zSelf
2071   );
2072   db_protect_pop();
2073   db_end_transaction(0);
2074   db_multi_exec("DETACH other");
2075 
2076   /* Propagate the changes to all other members of the login-group */
2077   zSql = mprintf(
2078     "BEGIN;"
2079     "REPLACE INTO config(name,value,mtime) VALUES('peer-name-%q',%Q,now());"
2080     "REPLACE INTO config(name,value,mtime) VALUES('peer-repo-%q',%Q,now());"
2081     "COMMIT;",
2082     zSelfProjCode, zSelfLabel, zSelfProjCode, zSelfRepo
2083   );
2084   db_unprotect(PROTECT_CONFIG);
2085   login_group_sql(zSql, "<li> ", "</li>", pzErrMsg);
2086   db_protect_pop();
2087   fossil_free(zSql);
2088 }
2089 
2090 /*
2091 ** Leave the login group that we are currently part of.
2092 */
login_group_leave(char ** pzErrMsg)2093 void login_group_leave(char **pzErrMsg){
2094   char *zProjCode;
2095   char *zSql;
2096 
2097   *pzErrMsg = 0;
2098   zProjCode = abbreviated_project_code(db_get("project-code","x"));
2099   zSql = mprintf(
2100     "DELETE FROM config WHERE name GLOB 'peer-*-%q';"
2101     "DELETE FROM config"
2102     " WHERE name='login-group-name'"
2103     "   AND (SELECT count(*) FROM config WHERE name GLOB 'peer-*')==0;",
2104     zProjCode
2105   );
2106   fossil_free(zProjCode);
2107   db_unprotect(PROTECT_CONFIG);
2108   login_group_sql(zSql, "<li> ", "</li>", pzErrMsg);
2109   fossil_free(zSql);
2110   db_multi_exec(
2111     "DELETE FROM config "
2112     " WHERE name GLOB 'peer-*'"
2113     "    OR name GLOB 'login-group-*';"
2114   );
2115   db_protect_pop();
2116 }
2117 
2118 /*
2119 ** COMMAND: login-group*
2120 **
2121 ** Usage: %fossil login-group
2122 **    or: %fossil login-group join REPO [-name NAME]
2123 **    or: %fossil login-group leave
2124 **
2125 ** With no arguments, this command shows the login-group to which the
2126 ** repository belongs.
2127 **
2128 ** The "join" command adds this repository to login group to which REPO
2129 ** belongs, or creates a new login group between itself and REPO if REPO
2130 ** does not already belong to a login-group.  When creating a new login-
2131 ** group, the name of the new group is determined by the "--name" option.
2132 **
2133 ** The "leave" command takes the repository out of whatever login group
2134 ** it is currently a part of.
2135 **
2136 ** About Login Groups:
2137 **
2138 ** A login-group is a set of repositories that share user credentials.
2139 ** If a user is logged into one member of the group, then that user can
2140 ** access any other group member as long as they have an entry in the
2141 ** USER table of that member.  If a user changes their password using
2142 ** web interface, their password is also automatically changed in every
2143 ** other member of the login group.
2144 */
login_group_command(void)2145 void login_group_command(void){
2146   const char *zLGName;
2147   const char *zCmd;
2148   int nCmd;
2149   Stmt q;
2150   db_find_and_open_repository(0,0);
2151   if( g.argc>2 ){
2152     zCmd = g.argv[2];
2153     nCmd = (int)strlen(zCmd);
2154     if( strncmp(zCmd,"join",nCmd)==0 && nCmd>=1 ){
2155       const char *zNewName = find_option("name",0,1);
2156       const char *zOther;
2157       char *zErr = 0;
2158       verify_all_options();
2159       if( g.argc!=4 ){
2160         fossil_fatal("unknown extra arguments to \"login-group join\"");
2161       }
2162       zOther = g.argv[3];
2163       login_group_leave(&zErr);
2164       sqlite3_free(zErr);
2165       zErr = 0;
2166       login_group_join(zOther,0,0,0,zNewName,&zErr);
2167       if( zErr ){
2168         fossil_fatal("%s", zErr);
2169       }
2170     }else if( strncmp(zCmd,"leave",nCmd)==0 && nCmd>=1 ){
2171       verify_all_options();
2172       if( g.argc!=3 ){
2173         fossil_fatal("unknown extra arguments to \"login-group leave\"");
2174       }
2175       zLGName = login_group_name();
2176       if( zLGName ){
2177         char *zErr = 0;
2178         fossil_print("Leaving login-group \"%s\"\n", zLGName);
2179         login_group_leave(&zErr);
2180         if( zErr ) fossil_fatal("Oops: %s", zErr);
2181         return;
2182       }
2183     }else{
2184       fossil_fatal("unknown command \"%s\" - should be \"join\" or \"leave\"",
2185                    zCmd);
2186     }
2187   }
2188   /* Show the current login group information */
2189   zLGName = login_group_name();
2190   if( zLGName==0 ){
2191     fossil_print("Not currently a part of any login-group\n");
2192     return;
2193   }
2194   fossil_print("Now part of login-group \"%s\" with:\n", zLGName);
2195   db_prepare(&q, "SELECT value FROM config WHERE name LIKE 'peer-name-%%'");
2196   while( db_step(&q)==SQLITE_ROW ){
2197     fossil_print("  %s\n", db_column_text(&q,0));
2198   }
2199   db_finalize(&q);
2200 
2201 }
2202