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&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'>↑ %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'>↑ %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'>↑ %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 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'>↑ %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'>↑ %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'>↑ %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