Fossil SCM

fossil-scm / src / login.c
Blame History Raw 2768 lines
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
** [email protected]
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
** Compute an appropriate Anti-CSRF token into g.zCsrfToken[].
56
*/
57
static void login_create_csrf_secret(const char *zSeed){
58
unsigned char zResult[20];
59
unsigned int i;
60
61
sha1sum_binary(zSeed, zResult);
62
for(i=0; i<sizeof(g.zCsrfToken)-1; i++){
63
g.zCsrfToken[i] = "abcdefghijklmnopqrstuvwxyz"
64
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
65
"0123456789-/"[zResult[i]%64];
66
}
67
g.zCsrfToken[i] = 0;
68
}
69
70
/*
71
** Return the login-group name. Or return 0 if this repository is
72
** not a member of a login-group.
73
*/
74
const char *login_group_name(void){
75
static const char *zGroup = 0;
76
static int once = 1;
77
if( once ){
78
zGroup = db_get("login-group-name", 0);
79
once = 0;
80
}
81
return zGroup;
82
}
83
84
/*
85
** Return a path appropriate for setting a cookie.
86
**
87
** The path is g.zTop for single-repo cookies. It is "/" for
88
** cookies of a login-group.
89
*/
90
const char *login_cookie_path(void){
91
if( login_group_name()==0 ){
92
return g.zTop;
93
}else{
94
return "/";
95
}
96
}
97
98
/*
99
** Return the name of the login cookie.
100
**
101
** The login cookie name is always of the form: fossil-XXXXXXXXXXXXXXXX
102
** where the Xs are the first 16 characters of the login-group-code or
103
** of the project-code if we are not a member of any login-group.
104
*/
105
char *login_cookie_name(void){
106
static char *zCookieName = 0;
107
if( zCookieName==0 ){
108
zCookieName = db_text(0,
109
"SELECT 'fossil-' || substr(value,1,16)"
110
" FROM config"
111
" WHERE name IN ('project-code','login-group-code')"
112
" ORDER BY name /*sort*/"
113
);
114
}
115
return zCookieName;
116
}
117
118
/*
119
** Redirect to the page specified by the "g" query parameter.
120
** Or if there is no "g" query parameter, redirect to the homepage.
121
*/
122
NORETURN void login_redirect_to_g(void){
123
const char *zGoto = P("g");
124
if( zGoto ){
125
cgi_redirectf("%R/%s",zGoto);
126
}else if( (zGoto = P("fossil-goto"))!=0 && zGoto[0]!=0 ){
127
cgi_set_cookie("fossil-goto","",0,1);
128
cgi_redirect(zGoto);
129
}else{
130
fossil_redirect_home();
131
}
132
}
133
134
/*
135
** Return an abbreviated project code. The abbreviation is the first
136
** 16 characters of the project code.
137
**
138
** Memory is obtained from malloc.
139
*/
140
static char *abbreviated_project_code(const char *zFullCode){
141
return mprintf("%.16s", zFullCode);
142
}
143
144
145
/*
146
** Check to see if the anonymous login is valid. If it is valid, return
147
** the userid of the anonymous user.
148
**
149
** The zCS parameter is the "captcha seed" used for a specific
150
** anonymous login request.
151
*/
152
int login_is_valid_anonymous(
153
const char *zUsername, /* The username. Must be "anonymous" */
154
const char *zPassword, /* The supplied password */
155
const char *zCS /* The captcha seed value */
156
){
157
const char *zPw; /* The correct password shown in the captcha */
158
int uid; /* The user ID of anonymous */
159
int n = 0; /* Counter of captcha-secrets */
160
161
if( zUsername==0 ) return 0;
162
else if( zPassword==0 ) return 0;
163
else if( zCS==0 ) return 0;
164
else if( fossil_strcmp(zUsername,"anonymous")!=0 ) return 0;
165
else if( anon_cookie_lifespan()==0 ) return 0;
166
while( 1/*exit-by-break*/ ){
167
zPw = captcha_decode((unsigned int)atoi(zCS), n);
168
if( zPw==0 ) return 0;
169
if( fossil_stricmp(zPw, zPassword)==0 ) break;
170
n++;
171
}
172
uid = db_int(0, "SELECT uid FROM user WHERE login='anonymous'"
173
" AND octet_length(pw)>0 AND octet_length(cap)>0");
174
return uid;
175
}
176
177
/*
178
** Make sure the accesslog table exists. Create it if it does not
179
*/
180
void create_accesslog_table(void){
181
if( !db_table_exists("repository","accesslog") ){
182
db_unprotect(PROTECT_READONLY);
183
db_multi_exec(
184
"CREATE TABLE IF NOT EXISTS repository.accesslog("
185
" uname TEXT,"
186
" ipaddr TEXT,"
187
" success BOOLEAN,"
188
" mtime TIMESTAMP"
189
");"
190
);
191
db_protect_pop();
192
}
193
}
194
195
/*
196
** Make a record of a login attempt, if login record keeping is enabled.
197
*/
198
static void record_login_attempt(
199
const char *zUsername, /* Name of user logging in */
200
const char *zIpAddr, /* IP address from which they logged in */
201
int bSuccess /* True if the attempt was a success */
202
){
203
db_unprotect(PROTECT_READONLY);
204
if( db_get_boolean("access-log", 1) ){
205
create_accesslog_table();
206
db_multi_exec(
207
"INSERT INTO accesslog(uname,ipaddr,success,mtime)"
208
"VALUES(%Q,%Q,%d,julianday('now'));",
209
zUsername, zIpAddr, bSuccess
210
);
211
}
212
if( bSuccess ){
213
alert_user_contact(zUsername);
214
}
215
db_protect_pop();
216
}
217
218
/*
219
** Searches for the user ID matching the given name and password.
220
** On success it returns a positive value. On error it returns 0.
221
** On serious (DB-level) error it will probably exit.
222
**
223
** zUsername uses double indirection because we may re-point *zUsername
224
** at a C string allocated with fossil_strdup() if you pass an email
225
** address instead and we find that address in the user table's info
226
** field, which is expected to contain a string of the form "Human Name
227
** <[email protected]>". In that case, *zUsername will point to that
228
** user's actual login name on return, causing a leak unless the caller
229
** is diligent enough to check whether its pointer was re-pointed.
230
**
231
** zPassword may be either the plain-text form or the encrypted
232
** form of the user's password.
233
*/
234
int login_search_uid(const char **pzUsername, const char *zPasswd){
235
char *zSha1Pw = sha1_shared_secret(zPasswd, *pzUsername, 0);
236
int uid = db_int(0,
237
"SELECT uid FROM user"
238
" WHERE login=%Q"
239
" AND octet_length(cap)>0 AND octet_length(pw)>0"
240
" AND login NOT IN ('anonymous','nobody','developer','reader')"
241
" AND (pw=%Q OR (length(pw)<>40 AND pw=%Q))"
242
" AND (info NOT LIKE '%%expires 20%%'"
243
" OR substr(info,instr(lower(info),'expires')+8,10)>datetime('now'))",
244
*pzUsername, zSha1Pw, zPasswd
245
);
246
247
/* If we did not find a login on the first attempt, and the username
248
** looks like an email address, then perhaps the user entered their
249
** email address instead of their login. Try again to match the user
250
** against email addresses contained in the "info" field.
251
*/
252
if( uid==0 && strchr(*pzUsername,'@')!=0 ){
253
Stmt q;
254
db_prepare(&q,
255
"SELECT login FROM user"
256
" WHERE find_emailaddr(info)=%Q"
257
" AND instr(login,'@')==0",
258
*pzUsername
259
);
260
while( db_step(&q)==SQLITE_ROW ){
261
const char *zLogin = db_column_text(&q,0);
262
if( (uid = login_search_uid(&zLogin, zPasswd) ) != 0 ){
263
*pzUsername = fossil_strdup(zLogin);
264
break;
265
}
266
}
267
db_finalize(&q);
268
}
269
free(zSha1Pw);
270
return uid;
271
}
272
273
/*
274
** Generates a login cookie value for a non-anonymous user.
275
**
276
** The zHash parameter must be a random value which must be
277
** subsequently stored in user.cookie for later validation.
278
**
279
** The returned memory should be free()d after use.
280
*/
281
char *login_gen_user_cookie_value(const char *zUsername, const char *zHash){
282
char *zProjCode = db_get("project-code",NULL);
283
char *zCode = abbreviated_project_code(zProjCode);
284
free(zProjCode);
285
assert((zUsername && *zUsername) && "Invalid user data.");
286
return mprintf("%s/%z/%s", zHash, zCode, zUsername);
287
}
288
289
/*
290
** Generates a login cookie for NON-ANONYMOUS users. Note that this
291
** function "could" figure out the uid by itself but it currently
292
** doesn't because the code which calls this already has the uid.
293
**
294
** This function also updates the user.cookie, user.ipaddr,
295
** and user.cexpire fields for the given user.
296
**
297
** If zDest is not NULL then the generated cookie is copied to
298
** *zDdest and ownership is transferred to the caller (who should
299
** eventually pass it to free()).
300
**
301
** If bSessionCookie is true, the cookie will be a session cookie,
302
** else a persistent cookie. If it's a session cookie, the
303
** [user].[cexpire] and [user].[cookie] entries will be modified as if
304
** it were a persistent cookie because doing so is necessary for
305
** fossil's own "is this cookie still valid?" checks to work.
306
*/
307
void login_set_user_cookie(
308
const char *zUsername, /* User's name */
309
int uid, /* User's ID */
310
char **zDest, /* Optional: store generated cookie value. */
311
int bSessionCookie /* True for session-only cookie */
312
){
313
const char *zCookieName = login_cookie_name();
314
const char *zExpire = db_get("cookie-expire","8766");
315
const int expires = atoi(zExpire)*3600;
316
char *zHash = 0;
317
char *zCookie;
318
const char *zIpAddr = PD("REMOTE_ADDR","nil"); /* IP address of user */
319
320
assert((zUsername && *zUsername) && (uid > 0) && "Invalid user data.");
321
zHash = db_text(0,
322
"SELECT cookie FROM user"
323
" WHERE uid=%d"
324
" AND cexpire>julianday('now')"
325
" AND length(cookie)>30",
326
uid);
327
if( zHash==0 ) zHash = db_text(0, "SELECT hex(randomblob(25))");
328
zCookie = login_gen_user_cookie_value(zUsername, zHash);
329
cgi_set_cookie(zCookieName, zCookie, login_cookie_path(),
330
bSessionCookie ? 0 : expires);
331
record_login_attempt(zUsername, zIpAddr, 1);
332
db_unprotect(PROTECT_USER);
333
db_multi_exec("UPDATE user SET cookie=%Q,"
334
" cexpire=julianday('now')+%d/86400.0 WHERE uid=%d",
335
zHash, expires, uid);
336
db_protect_pop();
337
fossil_free(zHash);
338
if( zDest ){
339
*zDest = zCookie;
340
}else{
341
free(zCookie);
342
}
343
}
344
345
/*
346
** SETTING: anon-cookie-lifespan width=10 default=480
347
** The number of minutes for which an anonymous login cookie is
348
** valid. Anonymous logins are prohibited if this value is zero.
349
*/
350
351
352
/*
353
** The default lifetime of an anoymous cookie, in minutes.
354
*/
355
#define ANONYMOUS_COOKIE_LIFESPAN (8*60)
356
357
/*
358
** Return the lifetime of an anonymous cookie, in minutes.
359
*/
360
int anon_cookie_lifespan(void){
361
static int lifespan = -1;
362
if( lifespan<0 ){
363
lifespan = db_get_int("anon-cookie-lifespan", ANONYMOUS_COOKIE_LIFESPAN);
364
if( lifespan<0 ) lifespan = 0;
365
}
366
return lifespan;
367
}
368
369
/* Sets a cookie for an anonymous user login, which looks like this:
370
**
371
** HASH/TIME/anonymous
372
**
373
** Where HASH is the sha1sum of TIME/USERAGENT/SECRET, in which SECRET
374
** is captcha-secret and USERAGENT is the HTTP_USER_AGENT value.
375
**
376
** If zCookieDest is not NULL then the generated cookie is assigned to
377
** *zCookieDest and the caller must eventually free() it.
378
**
379
** If bSessionCookie is true, the cookie will be a session cookie.
380
**
381
** Search for tag-20250817a to find the code that recognizes this cookie.
382
*/
383
void login_set_anon_cookie(char **zCookieDest, int bSessionCookie){
384
char *zNow; /* Current time (Julian day number) */
385
char *zCookie; /* The login cookie */
386
const char *zUserAgent; /* The user agent */
387
const char *zCookieName; /* Name of the login cookie */
388
Blob b; /* Blob used during cookie construction */
389
int expires = bSessionCookie ? 0 : anon_cookie_lifespan();
390
zCookieName = login_cookie_name();
391
zNow = db_text("0", "SELECT julianday('now')");
392
assert( zCookieName && zNow );
393
blob_init(&b, zNow, -1);
394
zUserAgent = PD("HTTP_USER_AGENT","nil");
395
blob_appendf(&b, "/%s/%z", zUserAgent, captcha_secret(0));
396
sha1sum_blob(&b, &b);
397
zCookie = mprintf("%s/%s/anonymous", blob_buffer(&b), zNow);
398
blob_reset(&b);
399
cgi_set_cookie(zCookieName, zCookie, login_cookie_path(), expires);
400
if( zCookieDest ){
401
*zCookieDest = zCookie;
402
}else{
403
free(zCookie);
404
}
405
fossil_free(zNow);
406
}
407
408
/*
409
** "Unsets" the login cookie (insofar as cookies can be unset) and
410
** clears the current user's (g.userUid) login information from the
411
** user table. Sets: user.cookie, user.ipaddr, user.cexpire.
412
**
413
** We could/should arguably clear out g.userUid and g.perm here, but
414
** we don't currently do not.
415
**
416
** This is a no-op if g.userUid is 0.
417
*/
418
void login_clear_login_data(){
419
if(!g.userUid){
420
return;
421
}else{
422
const char *cookie = login_cookie_name();
423
/* To logout, change the cookie value to an empty string */
424
cgi_set_cookie(cookie, "",
425
login_cookie_path(), -86400);
426
db_unprotect(PROTECT_USER);
427
db_multi_exec("UPDATE user SET cookie=NULL, ipaddr=NULL, "
428
" cexpire=0 WHERE uid=%d"
429
" AND login NOT IN ('anonymous','nobody',"
430
" 'developer','reader')", g.userUid);
431
db_protect_pop();
432
cgi_replace_parameter(cookie, NULL);
433
cgi_replace_parameter("anon", NULL);
434
}
435
}
436
437
/*
438
** Look at the HTTP_USER_AGENT parameter and try to determine if the user agent
439
** is a manually operated browser or a bot. When in doubt, assume a bot.
440
** Return true if we believe the agent is a real person.
441
*/
442
static int isHuman(const char *zAgent){
443
if( zAgent==0 ) return 0; /* If no UserAgent, then probably a bot */
444
if( strstr(zAgent, "bot")!=0 ) return 0;
445
if( strstr(zAgent, "spider")!=0 ) return 0;
446
if( strstr(zAgent, "crawl")!=0 ) return 0;
447
/* If a URI appears in the User-Agent, it is probably a bot */
448
if( strstr(zAgent, "http")!=0 ) return 0;
449
if( strncmp(zAgent, "Mozilla/", 8)==0 ){
450
if( atoi(&zAgent[8])<4 ) return 0; /* Many bots advertise as Mozilla/3 */
451
452
/* Google AI Robot, maybe? */
453
if( strstr(zAgent, "GoogleOther)")!=0 ) return 0;
454
455
/* 2016-05-30: A pernicious spider that likes to walk Fossil timelines has
456
** been detected on the SQLite website. The spider changes its user-agent
457
** string frequently, but it always seems to include the following text:
458
*/
459
if( strstr(zAgent, "Safari/537.36Mozilla/5.0")!=0 ) return 0;
460
461
if( sqlite3_strglob("*Firefox/[1-9]*", zAgent)==0 ) return 1;
462
if( sqlite3_strglob("*Chrome/[1-9]*", zAgent)==0 ) return 1;
463
if( sqlite3_strglob("*(compatible;?MSIE?[1789]*", zAgent)==0 ) return 1;
464
if( sqlite3_strglob("*Trident/[1-9]*;?rv:[1-9]*", zAgent)==0 ){
465
return 1; /* IE11+ */
466
}
467
if( sqlite3_strglob("*AppleWebKit/[1-9]*(KHTML*", zAgent)==0 ) return 1;
468
if( sqlite3_strglob("*PaleMoon/[1-9]*", zAgent)==0 ) return 1;
469
return 0;
470
}
471
if( strncmp(zAgent, "Opera/", 6)==0 ) return 1;
472
if( strncmp(zAgent, "Safari/", 7)==0 ) return 1;
473
if( strncmp(zAgent, "Lynx/", 5)==0 ) return 1;
474
if( strncmp(zAgent, "NetSurf/", 8)==0 ) return 1;
475
return 0;
476
}
477
478
/*
479
** Make a guess at whether or not the requestor is a mobile device or
480
** a desktop device (narrow screen vs. wide screen) based the HTTP_USER_AGENT
481
** parameter. Return true for mobile and false for desktop.
482
**
483
** Caution: This is only a guess.
484
**
485
** Algorithm derived from https://developer.mozilla.org/en-US/docs/Web/
486
** HTTP/Browser_detection_using_the_user_agent#mobile_device_detection on
487
** 2021-03-01
488
*/
489
int user_agent_is_likely_mobile(void){
490
const char *zAgent = P("HTTP_USER_AGENT");
491
if( zAgent==0 ) return 0;
492
if( strstr(zAgent,"Mobi")!=0 ) return 1;
493
return 0;
494
}
495
496
/*
497
** COMMAND: test-ishuman
498
**
499
** Read lines of text from standard input. Interpret each line of text
500
** as a User-Agent string from an HTTP header. Label each line as HUMAN
501
** or ROBOT.
502
*/
503
void test_ishuman(void){
504
char zLine[3000];
505
while( fgets(zLine, sizeof(zLine), stdin) ){
506
fossil_print("%s %s", isHuman(zLine) ? "HUMAN" : "ROBOT", zLine);
507
}
508
}
509
510
/*
511
** SQL function for constant time comparison of two values.
512
** Sets result to 0 if two values are equal.
513
*/
514
static void constant_time_cmp_function(
515
sqlite3_context *context,
516
int argc,
517
sqlite3_value **argv
518
){
519
const unsigned char *buf1, *buf2;
520
int len, i;
521
unsigned char rc = 0;
522
523
assert( argc==2 );
524
len = sqlite3_value_bytes(argv[0]);
525
if( len==0 || len!=sqlite3_value_bytes(argv[1]) ){
526
rc = 1;
527
}else{
528
buf1 = sqlite3_value_text(argv[0]);
529
buf2 = sqlite3_value_text(argv[1]);
530
for( i=0; i<len; i++ ){
531
rc = rc | (buf1[i] ^ buf2[i]);
532
}
533
}
534
sqlite3_result_int(context, rc);
535
}
536
537
/*
538
** Return true if the current page was reached by a redirect from the /login
539
** page.
540
*/
541
int referred_from_login(void){
542
const char *zReferer = P("HTTP_REFERER");
543
char *zPattern;
544
int rc;
545
if( zReferer==0 ) return 0;
546
zPattern = mprintf("%s/login*", g.zBaseURL);
547
rc = sqlite3_strglob(zPattern, zReferer)==0;
548
fossil_free(zPattern);
549
return rc;
550
}
551
552
/*
553
** Return true if users are allowed to reset their own passwords.
554
*/
555
int login_self_password_reset_available(void){
556
if( !db_get_boolean("self-pw-reset",0) ) return 0;
557
if( !alert_tables_exist() ) return 0;
558
return 1;
559
}
560
561
/*
562
** Return TRUE if self-registration is available. If the zNeeded
563
** argument is not NULL, then only return true if self-registration is
564
** available and any of the capabilities named in zNeeded are available
565
** to self-registered users.
566
*/
567
int login_self_register_available(const char *zNeeded){
568
CapabilityString *pCap;
569
int rc;
570
if( !db_get_boolean("self-register",0) ) return 0;
571
if( zNeeded==0 ) return 1;
572
pCap = capability_add(0, db_get("default-perms", "u"));
573
capability_expand(pCap);
574
rc = capability_has_any(pCap, zNeeded);
575
capability_free(pCap);
576
return rc;
577
}
578
579
/*
580
** There used to be a page named "my" that was designed to show information
581
** about a specific user. The "my" page was linked from the "Logged in as USER"
582
** line on the title bar. The "my" page was never completed so it is now
583
** removed. Use this page as a placeholder in older installations.
584
**
585
** WEBPAGE: login
586
** WEBPAGE: logout
587
** WEBPAGE: my
588
**
589
** The login/logout page. Parameters:
590
**
591
** g=URL Jump back to this URL after login completes
592
** anon The g=URL is not accessible by "nobody" but is
593
** accessible by "anonymous"
594
*/
595
void login_page(void){
596
const char *zUsername, *zPasswd;
597
const char *zNew1, *zNew2;
598
const char *zAnonPw = 0;
599
const char *zGoto = P("g");
600
int anonFlag; /* Login as "anonymous" would be useful */
601
char *zErrMsg = "";
602
int uid; /* User id logged in user */
603
char *zSha1Pw;
604
const char *zIpAddr; /* IP address of requestor */
605
const int noAnon = P("noanon")!=0;
606
int rememberMe; /* If true, use persistent cookie, else
607
session cookie. Toggled per
608
checkbox. */
609
610
if( P("pwreset")!=0 && login_self_password_reset_available() ){
611
/* If the "Reset Password" button in the form was pressed, render
612
** the Request Password Reset page in place of this one. */
613
login_reqpwreset_page();
614
return;
615
}
616
617
/* If the "anon" query parameter is 1 or 2, that means rework the web-page
618
** to make it a more user-friendly captcha. Extraneous text and boxes
619
** are omitted. The user has just the captcha image and an entry box
620
** and a "Verify" button. Underneath is the same login page for user
621
** "anonymous", just displayed in an easier to digest format for one-time
622
** visitors.
623
**
624
** anon=1 is advisory and only has effect if there is not some other login
625
** cookie. anon=2 means always show the captcha.
626
*/
627
anonFlag = anon_cookie_lifespan()>0 ? atoi(PD("anon","0")) : 0;
628
if( anonFlag==2 ){
629
g.zLogin = 0;
630
}else{
631
login_check_credentials();
632
if( g.zLogin!=0 ) anonFlag = 0;
633
}
634
635
fossil_redirect_to_https_if_needed(1);
636
sqlite3_create_function(g.db, "constant_time_cmp", 2, SQLITE_UTF8, 0,
637
constant_time_cmp_function, 0, 0);
638
zUsername = P("u");
639
zPasswd = P("p");
640
641
/* Handle log-out requests */
642
if( P("out") && cgi_csrf_safe(2) ){
643
login_clear_login_data();
644
login_redirect_to_g();
645
return;
646
}
647
648
/* Redirect for create-new-account requests */
649
if( P("self") ){
650
cgi_redirectf("%R/register");
651
return;
652
}
653
654
/* Deal with password-change requests */
655
if( g.perm.Password && zPasswd
656
&& (zNew1 = P("n1"))!=0 && (zNew2 = P("n2"))!=0
657
&& cgi_csrf_safe(2)
658
){
659
/* If there is not a "real" login, we cannot change any password. */
660
if( g.zLogin ){
661
/* The user requests a password change */
662
zSha1Pw = sha1_shared_secret(zPasswd, g.zLogin, 0);
663
if( db_int(1, "SELECT 0 FROM user"
664
" WHERE uid=%d"
665
" AND (constant_time_cmp(pw,%Q)=0"
666
" OR constant_time_cmp(pw,%Q)=0)",
667
g.userUid, zSha1Pw, zPasswd) ){
668
sleep(1);
669
zErrMsg =
670
@ <p><span class="loginError">
671
@ You entered an incorrect old password while attempting to change
672
@ your password. Your password is unchanged.
673
@ </span></p>
674
;
675
}else if( fossil_strcmp(zNew1,zNew2)!=0 ){
676
zErrMsg =
677
@ <p><span class="loginError">
678
@ The two copies of your new passwords do not match.
679
@ Your password is unchanged.
680
@ </span></p>
681
;
682
}else{
683
char *zNewPw = sha1_shared_secret(zNew1, g.zLogin, 0);
684
char *zChngPw;
685
char *zErr;
686
int rc;
687
688
/* vvvvvvv--- tag-20230106-1 ----vvvvvv
689
**
690
** Replicate changes made below to tag-20230106-2
691
*/
692
admin_log("password change for user %s", g.zLogin);
693
db_unprotect(PROTECT_USER);
694
db_multi_exec(
695
"UPDATE user SET pw=%Q WHERE uid=%d", zNewPw, g.userUid
696
);
697
zChngPw = mprintf(
698
"UPDATE user"
699
" SET pw=shared_secret(%Q,%Q,"
700
" (SELECT value FROM config WHERE name='project-code'))"
701
" WHERE login=%Q",
702
zNew1, g.zLogin, g.zLogin
703
);
704
fossil_free(zNewPw);
705
rc = login_group_sql(zChngPw, "<p>", "</p>\n", &zErr);
706
db_protect_pop();
707
/*
708
** ^^^^^^^^--- tag-20230106-1 ----^^^^^^^^^
709
**
710
** Replicate changes above to tag-20230106-2
711
*/
712
713
if( rc ){
714
zErrMsg = mprintf("<span class=\"loginError\">%s</span>", zErr);
715
fossil_free(zErr);
716
}else{
717
login_redirect_to_g();
718
return;
719
}
720
}
721
}else{
722
zErrMsg =
723
@ <p><span class="loginError">
724
@ The password cannot be changed for this type of login.
725
@ The password is unchanged.
726
@ </span></p>
727
;
728
}
729
}
730
zIpAddr = PD("REMOTE_ADDR","nil"); /* Complete IP address for logging */
731
uid = login_is_valid_anonymous(zUsername, zPasswd, P("cs"));
732
if(zUsername==0){
733
/* Initial login page hit. */
734
rememberMe = 0;
735
}else{
736
rememberMe = P("remember")!=0;
737
}
738
if( uid>0 ){
739
login_set_anon_cookie(NULL, rememberMe?0:1);
740
record_login_attempt("anonymous", zIpAddr, 1);
741
login_redirect_to_g();
742
}
743
if( zUsername!=0 && zPasswd!=0 && zPasswd[0]!=0 ){
744
/* Attempting to log in as a user other than anonymous.
745
*/
746
uid = login_search_uid(&zUsername, zPasswd);
747
if( uid<=0 ){
748
sleep(1);
749
zErrMsg =
750
@ <p><span class="loginError">
751
@ You entered an unknown user or an incorrect password.
752
@ </span></p>
753
;
754
record_login_attempt(zUsername, zIpAddr, 0);
755
cgi_set_status(401, "Unauthorized");
756
}else{
757
/* Non-anonymous login is successful. Set a cookie of the form:
758
**
759
** HASH/PROJECT/LOGIN
760
**
761
** where HASH is a random hex number, PROJECT is either project
762
** code prefix, and LOGIN is the user name.
763
*/
764
login_set_user_cookie(zUsername, uid, NULL, rememberMe?0:1);
765
login_redirect_to_g();
766
}
767
}
768
style_set_current_feature("login");
769
style_header("Login/Logout");
770
if( anonFlag==2 ) g.zLogin = 0;
771
style_adunit_config(ADUNIT_OFF);
772
@ %s(zErrMsg)
773
if( zGoto && !noAnon ){
774
char *zAbbrev = fossil_strdup(zGoto);
775
int i;
776
for(i=0; zAbbrev[i] && zAbbrev[i]!='?'; i++){}
777
zAbbrev[i] = 0;
778
if( g.zLogin ){
779
@ <p>Use a different login with greater privilege than <b>%h(g.zLogin)</b>
780
@ to access <b>%h(zAbbrev)</b>.
781
}else if( anonFlag ){
782
@ <p><b>Verify that you are human by typing in the 8-character text
783
@ password shown below.</b></p>
784
}else{
785
@ <p>Login as a named user to access page <b>%h(zAbbrev)</b>.
786
}
787
fossil_free(zAbbrev);
788
}
789
if( g.sslNotAvailable==0
790
&& strncmp(g.zBaseURL,"https:",6)!=0
791
&& db_get_boolean("https-login",0)
792
){
793
form_begin(0, "https:%s/login", g.zBaseURL+5);
794
}else{
795
form_begin(0, "%R/login");
796
}
797
if( zGoto ){
798
@ <input type="hidden" name="g" value="%h(zGoto)">
799
}
800
if( anonFlag ){
801
@ <input type="hidden" name="anon" value="1">
802
@ <input type="hidden" name="u" value="anonymous">
803
}
804
if( g.zLogin ){
805
@ <p>Currently logged in as <b>%h(g.zLogin)</b>.
806
@ <input type="submit" name="out" value="Logout" autofocus></p>
807
@ </form>
808
}else{
809
unsigned int uSeed = captcha_seed();
810
if( g.zLogin==0 && (anonFlag || zGoto==0) && anon_cookie_lifespan()>0 ){
811
zAnonPw = db_text(0, "SELECT pw FROM user"
812
" WHERE login='anonymous'"
813
" AND cap!=''");
814
}else{
815
zAnonPw = 0;
816
}
817
@ <table class="login_out">
818
if( P("HTTPS")==0 && !anonFlag ){
819
@ <tr><td class="form_label">Warning:</td>
820
@ <td><span class='securityWarning'>
821
@ Login information, including the password,
822
@ will be sent in the clear over an unencrypted connection.
823
if( !g.sslNotAvailable ){
824
@ Consider logging in at
825
@ <a href='%s(g.zHttpsURL)'>%h(g.zHttpsURL)</a> instead.
826
}
827
@ </span></td></tr>
828
}
829
if( !anonFlag ){
830
@ <tr>
831
@ <td class="form_label" id="userlabel1">User ID:</td>
832
@ <td><input type="text" id="u" aria-labelledby="userlabel1" name="u" \
833
@ size="30" value="" autofocus></td>
834
@ </tr>
835
}
836
@ <tr>
837
@ <td class="form_label" id="pswdlabel">Password:</td>
838
@ <td><input aria-labelledby="pswdlabel" type="password" id="p" \
839
@ name="p" value="" size="30"%s(anonFlag ? " autofocus" : "")>
840
if( anonFlag ){
841
@ </td></tr>
842
@ <tr>
843
@ <td></td><td>\
844
captcha_speakit_button(uSeed, "Read the password out loud");
845
}else if( zAnonPw && !noAnon ){
846
captcha_speakit_button(uSeed, "Speak password for \"anonymous\"");
847
}
848
@ </td>
849
@ </tr>
850
if( !anonFlag ){
851
@ <tr>
852
@ <td></td>
853
@ <td><input type="checkbox" name="remember" value="1" \
854
@ id="remember-me" %s(rememberMe ? "checked=\"checked\"" : "")>
855
@ <label for="remember-me">Remember me?</label></td>
856
@ </tr>
857
@ <tr>
858
@ <td></td>
859
@ <td><input type="submit" name="in" value="Login">
860
@ </tr>
861
}else{
862
@ <tr>
863
@ <td></td>
864
@ <td><input type="submit" name="in" value="Verify that I am human">
865
@ </tr>
866
}
867
if( !anonFlag && !noAnon && login_self_register_available(0) ){
868
@ <tr>
869
@ <td></td>
870
@ <td><input type="submit" name="self" value="Create A New Account">
871
@ </tr>
872
}
873
if( !anonFlag && login_self_password_reset_available() ){
874
@ <tr>
875
@ <td></td>
876
@ <td><input type="submit" name="pwreset" value="Reset My Password">
877
@ </tr>
878
}
879
@ </table>
880
if( zAnonPw && !noAnon ){
881
const char *zDecoded = captcha_decode(uSeed, 0);
882
int bAutoCaptcha = db_get_boolean("auto-captcha", 0);
883
char *zCaptcha = captcha_render(zDecoded);
884
885
@ <p><input type="hidden" name="cs" value="%u(uSeed)">
886
if( !anonFlag ){
887
@ Visitors may enter <b>anonymous</b> as the user-ID with
888
@ the 8-character hexadecimal password shown below:</p>
889
}
890
@ <div class="captcha"><table class="captcha"><tr><td>\
891
@ <pre class="captcha">
892
@ %h(zCaptcha)
893
@ </pre></td></tr></table>
894
if( bAutoCaptcha && !anonFlag ) {
895
@ <input type="button" value="Fill out captcha" id='autofillButton' \
896
@ data-af='%s(zDecoded)'>
897
builtin_request_js("login.js");
898
}
899
@ </div>
900
free(zCaptcha);
901
}
902
@ </form>
903
}
904
if( login_is_individual() && !anonFlag ){
905
if( g.perm.EmailAlert && alert_enabled() ){
906
@ <hr>
907
@ <p>Configure <a href="%R/alerts">Email Alerts</a>
908
@ for user <b>%h(g.zLogin)</b></p>
909
}
910
if( db_table_exists("repository","forumpost") ){
911
@ <hr><p>
912
@ <a href="%R/timeline?ss=v&y=f&vfx&u=%t(g.zLogin)">Forum
913
@ post timeline</a> for user <b>%h(g.zLogin)</b></p>
914
}
915
}
916
if( !anonFlag ){
917
@ <hr><p>
918
@ Select your preferred <a href="%R/skins">site skin</a>.
919
@ </p>
920
@ <hr><p>
921
@ Manage your <a href="%R/cookies">cookies</a> or your
922
@ <a href="%R/tokens">access tokens</a>.</p>
923
}
924
if( login_is_individual() ){
925
if( g.perm.Password ){
926
char *zRPW = fossil_random_password(12);
927
@ <hr>
928
@ <p>Change Password for user <b>%h(g.zLogin)</b>:</p>
929
form_begin(0, "%R/login");
930
@ <table>
931
@ <tr><td class="form_label" id="oldpw">Old Password:</td>
932
@ <td><input aria-labelledby="oldpw" type="password" name="p" \
933
@ size="30"/></td></tr>
934
@ <tr><td class="form_label" id="newpw">New Password:</td>
935
@ <td><input aria-labelledby="newpw" type="password" name="n1" \
936
@ size="30"> Suggestion: %z(zRPW)</td></tr>
937
@ <tr><td class="form_label" id="reppw">Repeat New Password:</td>
938
@ <td><input aria-labledby="reppw" type="password" name="n2" \
939
@ size="30"></td></tr>
940
@ <tr><td></td>
941
@ <td><input type="submit" value="Change Password"></td></tr>
942
@ </table>
943
@ </form>
944
}
945
}
946
style_finish_page();
947
}
948
949
/*
950
** Construct an appropriate URL suffix for the /resetpw page. The
951
** suffix will be of the form:
952
**
953
** UID-TIMESTAMP-HASH
954
**
955
** Where UID and TIMESTAMP are the parameters to this function, and HASH
956
** is constructed from information that is unique to the user in question
957
** and which is not publicly available. In particular, the HASH includes
958
** the existing user password. Thus, in order to construct a URL that can
959
** change a password, an attacker must know the current password, in which
960
** case the attacker does not need to construct the URL in order to take
961
** over the account.
962
**
963
** Return a pointer to the resulting string in memory obtained
964
** from fossil_malloc().
965
*/
966
char *login_resetpw_suffix(int uid, i64 timestamp){
967
char *zHash;
968
char *zInnerSql;
969
char *zResult;
970
extern int sqlite3_shathree_init(sqlite3*,char**,const sqlite3_api_routines*);
971
if( timestamp<=0 ){ timestamp = time(0); }
972
sqlite3_shathree_init(g.db, 0, 0);
973
if( db_table_exists("repository","subscriber") ){
974
zInnerSql = mprintf(
975
"SELECT %lld, login, pw, cookie, user.mtime, user.info, subscriberCode"
976
" FROM user LEFT JOIN subscriber ON suname=login"
977
" WHERE uid=%d", timestamp, uid);
978
}else{
979
zInnerSql = mprintf(
980
"SELECT %lld, login, pw, cookie, user.mtime, user.info"
981
" FROM user WHERE uid=%d", timestamp, uid);
982
}
983
zHash = db_text(0, "SELECT lower(hex(sha3_query(%Q)))", zInnerSql);
984
fossil_free(zInnerSql);
985
zResult = mprintf("%x-%llx-%s", uid, timestamp, zHash);
986
if( strlen(zHash)<64 || strlen(zResult)<70 ){
987
/* This should never happen, but if it does, we don't want it to lead
988
** to a security breach. */
989
fossil_panic("insecure password reset hash generated\n");
990
}
991
fossil_free(zHash);
992
return zResult;
993
}
994
995
/*
996
** Check to see if the "name" query parameter is a valid resetpw suffix
997
** for a user whose password we are allowed to reset. If it is, then return
998
** the positive integer UID for that user. If the query parameter is not
999
** valid, return 0.
1000
*/
1001
static int login_resetpw_suffix_is_valid(const char *zName){
1002
int i, j;
1003
int uid;
1004
i64 timestamp;
1005
i64 now;
1006
char *zHash;
1007
if( zName==0 || strlen(zName)<70 ) goto not_valid_suffix;
1008
for(i=0; fossil_isxdigit(zName[i]); i++){}
1009
if( i<1 || zName[i]!='-' ) goto not_valid_suffix;
1010
for(j=i+1; fossil_isxdigit(zName[j]); j++){}
1011
if( j<=i+1 || zName[j]!='-' ) goto not_valid_suffix;
1012
uid = strtol(zName, 0, 16);
1013
if( uid<=0 ) goto not_valid_suffix;
1014
if( !db_exists("SELECT 1 FROM user WHERE uid=%d", uid) ){
1015
goto not_valid_suffix;
1016
}
1017
timestamp = strtoll(&zName[i+1], 0, 16);
1018
now = time(0);
1019
if( timestamp+3600 <= now ) goto not_valid_suffix;
1020
zHash = login_resetpw_suffix(uid,timestamp);
1021
if( fossil_strcmp(zHash, zName)!=0 ){
1022
fossil_free(zHash);
1023
goto not_valid_suffix;
1024
}
1025
fossil_free(zHash);
1026
return uid;
1027
1028
not_valid_suffix:
1029
return 0;
1030
}
1031
1032
/*
1033
** COMMAND: test-resetpw-url
1034
** Usage: fossil test-resetpw-url UID
1035
**
1036
** Generate and verify a /resetpw URL for user UID.
1037
**
1038
** This command is intended for unit testing the login_resetpw_suffix()
1039
** and login_resetpw_suffix_is_valid() functions.
1040
*/
1041
void test_resetpw_url(void){
1042
char *zSuffix;
1043
int uid;
1044
int xuid;
1045
char *zLogin;
1046
int i;
1047
db_find_and_open_repository(0, 0);
1048
verify_all_options();
1049
if( g.argc<3 ){
1050
usage("UID ...");
1051
}
1052
for(i=2; i<g.argc; i++){
1053
uid = atoi(g.argv[i]);
1054
zSuffix = login_resetpw_suffix(uid, 0);
1055
xuid = login_resetpw_suffix_is_valid(zSuffix);
1056
if( xuid>0 ){
1057
zLogin = db_text(0, "SELECT login FROM user WHERE uid=%d", xuid);
1058
}else{
1059
zLogin = 0;
1060
}
1061
fossil_print("/resetpw/%s %d (%s)\n",
1062
zSuffix, xuid, zLogin ? zLogin : "???");
1063
fossil_free(zSuffix);
1064
fossil_free(zLogin);
1065
}
1066
}
1067
1068
/*
1069
** WEBPAGE: resetpw
1070
**
1071
** The URL format must be like this:
1072
**
1073
** /resetpw/UID-TIMESTAMP-HASH
1074
**
1075
** Where UID is the uid of the user whose password is to be reset,
1076
** TIMESTAMP is the unix timestamp when the request was made, and
1077
** HASH is a hash based on UID, TIMESTAMP, and other information that
1078
** is unavailable to an attacher.
1079
**
1080
** With no other arguments, a form is present which allows the user to
1081
** enter a new password. When the SUBMIT button is pressed, a POST request
1082
** back to the same URL that will change the password.
1083
*/
1084
void login_resetpw(void){
1085
const char *zName;
1086
int uid;
1087
char *zRPW;
1088
const char *zNew1, *zNew2;
1089
1090
style_set_current_feature("resetpw");
1091
style_header("Reset Password");
1092
style_adunit_config(ADUNIT_OFF);
1093
zName = PD("name","");
1094
uid = login_resetpw_suffix_is_valid(zName);
1095
if( uid==0 ){
1096
@ <p><span class="loginError">
1097
@ This password-reset URL is invalid, probably because it has expired.
1098
@ Password-reset URLs have a short lifespan.
1099
@ </span></p>
1100
style_finish_page();
1101
sleep(1); /* Introduce a small delay on an invalid suffix as an
1102
** extra defense against search attacks */
1103
return;
1104
}
1105
fossil_redirect_to_https_if_needed(1);
1106
login_set_uid(uid, 0);
1107
if( g.perm.Setup || g.perm.Admin || !g.perm.Password || g.zLogin==0 ){
1108
@ <p><span class="loginError">
1109
@ Cannot change the password for user <b>%h(g.zLogin)</b>.
1110
@ </span></p>
1111
style_finish_page();
1112
return;
1113
}
1114
if( (zNew1 = P("n1"))!=0 && (zNew2 = P("n2"))!=0 ){
1115
if( fossil_strcmp(zNew1,zNew2)!=0 ){
1116
@ <p><span class="loginError">
1117
@ The two copies of your new passwords do not match.
1118
@ Try again.
1119
@ </span></p>
1120
}else{
1121
char *zNewPw = sha1_shared_secret(zNew1, g.zLogin, 0);
1122
char *zChngPw;
1123
char *zErr;
1124
int rc;
1125
1126
/* vvvvvvv--- tag-20230106-2 ----vvvvvv
1127
**
1128
** Replicate changes made below to tag-20230106-1
1129
*/
1130
admin_log("password change for user %s", g.zLogin);
1131
db_unprotect(PROTECT_USER);
1132
db_multi_exec(
1133
"UPDATE user SET pw=%Q WHERE uid=%d", zNewPw, g.userUid
1134
);
1135
zChngPw = mprintf(
1136
"UPDATE user"
1137
" SET pw=shared_secret(%Q,%Q,"
1138
" (SELECT value FROM config WHERE name='project-code'))"
1139
" WHERE login=%Q",
1140
zNew1, g.zLogin, g.zLogin
1141
);
1142
fossil_free(zNewPw);
1143
rc = login_group_sql(zChngPw, "<p>", "</p>\n", &zErr);
1144
db_protect_pop();
1145
/*
1146
** ^^^^^^^^--- tag-20230106-2 ----^^^^^^^^^
1147
**
1148
** Replicate changes above to tag-20230106-1
1149
*/
1150
1151
if( rc ){
1152
@ <p><span class='loginError'>
1153
@ %s(zErr);
1154
@ </span></p>
1155
fossil_free(zErr);
1156
}else{
1157
@ <p>Password changed successfully. Go to the
1158
@ <a href="%R/login?u=%t(g.zLogin)">Login</a> page and log in
1159
@ using the new password to continue.
1160
@ </p>
1161
style_finish_page();
1162
return;
1163
}
1164
}
1165
}
1166
zRPW = fossil_random_password(12);
1167
@ <p>Change Password for user <b>%h(g.zLogin)</b>:</p>
1168
form_begin(0, "%R/resetpw");
1169
@ <input type='hidden' name='name' value='%h(zName)'>
1170
@ <table>
1171
@ <tr><td class="form_label" id="newpw">New Password:</td>
1172
@ <td><input aria-labelledby="newpw" type="password" name="n1" \
1173
@ size="30"> Suggestion: %z(zRPW)</td></tr>
1174
@ <tr><td class="form_label" id="reppw">Repeat New Password:</td>
1175
@ <td><input aria-labledby="reppw" type="password" name="n2" \
1176
@ size="30"></td></tr>
1177
@ <tr><td></td>
1178
@ <td><input type="submit" value="Change Password"></td></tr>
1179
@ </table>
1180
@ </form>
1181
style_finish_page();
1182
}
1183
1184
/*
1185
** Attempt to find login credentials for user zLogin on a peer repository
1186
** with project code zCode. Transfer those credentials to the local
1187
** repository.
1188
**
1189
** Return true if a transfer was made and false if not.
1190
*/
1191
static int login_transfer_credentials(
1192
const char *zLogin, /* Login we are looking for */
1193
const char *zCode, /* Project code of peer repository */
1194
const char *zHash /* HASH from login cookie HASH/CODE/LOGIN */
1195
){
1196
sqlite3 *pOther = 0; /* The other repository */
1197
sqlite3_stmt *pStmt; /* Query against the other repository */
1198
char *zSQL; /* SQL of the query against other repo */
1199
char *zOtherRepo; /* Filename of the other repository */
1200
int rc; /* Result code from SQLite library functions */
1201
int nXfer = 0; /* Number of credentials transferred */
1202
1203
zOtherRepo = db_text(0,
1204
"SELECT value FROM config WHERE name='peer-repo-%q'",
1205
zCode
1206
);
1207
if( zOtherRepo==0 ) return 0; /* No such peer repository */
1208
1209
rc = sqlite3_open_v2(
1210
zOtherRepo, &pOther,
1211
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE,
1212
g.zVfsName
1213
);
1214
if( rc==SQLITE_OK ){
1215
sqlite3_create_function(pOther,"now",0,SQLITE_UTF8,0,db_now_function,0,0);
1216
sqlite3_create_function(pOther, "constant_time_cmp", 2, SQLITE_UTF8, 0,
1217
constant_time_cmp_function, 0, 0);
1218
sqlite3_busy_timeout(pOther, 5000);
1219
zSQL = mprintf(
1220
"SELECT cexpire FROM user"
1221
" WHERE login=%Q"
1222
" AND octet_length(cap)>0"
1223
" AND octet_length(pw)>0"
1224
" AND cexpire>julianday('now')"
1225
" AND constant_time_cmp(cookie,%Q)=0",
1226
zLogin, zHash
1227
);
1228
pStmt = 0;
1229
rc = sqlite3_prepare_v2(pOther, zSQL, -1, &pStmt, 0);
1230
if( rc==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){
1231
db_unprotect(PROTECT_USER);
1232
db_multi_exec(
1233
"UPDATE user SET cookie=%Q, cexpire=%.17g"
1234
" WHERE login=%Q",
1235
zHash,
1236
sqlite3_column_double(pStmt, 0), zLogin
1237
);
1238
db_protect_pop();
1239
nXfer++;
1240
}
1241
sqlite3_finalize(pStmt);
1242
}
1243
sqlite3_close(pOther);
1244
fossil_free(zOtherRepo);
1245
return nXfer;
1246
}
1247
1248
/*
1249
** Return TRUE if zLogin is one of the special usernames
1250
*/
1251
int login_is_special(const char *zLogin){
1252
if( fossil_strcmp(zLogin, "anonymous")==0 ) return 1;
1253
if( fossil_strcmp(zLogin, "nobody")==0 ) return 1;
1254
if( fossil_strcmp(zLogin, "developer")==0 ) return 1;
1255
if( fossil_strcmp(zLogin, "reader")==0 ) return 1;
1256
return 0;
1257
}
1258
1259
/*
1260
** Lookup the uid for a non-built-in user with zLogin and zCookie.
1261
** Return 0 if not found.
1262
**
1263
** Note that this only searches for logged-in entries with matching
1264
** zCookie (db: user.cookie) entries.
1265
*/
1266
static int login_find_user(
1267
const char *zLogin, /* User name */
1268
const char *zCookie /* Login cookie value */
1269
){
1270
int uid;
1271
if( login_is_special(zLogin) ) return 0;
1272
uid = db_int(0,
1273
"SELECT uid FROM user"
1274
" WHERE login=%Q"
1275
" AND cexpire>julianday('now')"
1276
" AND octet_length(cap)>0"
1277
" AND octet_length(pw)>0"
1278
" AND constant_time_cmp(cookie,%Q)=0",
1279
zLogin, zCookie
1280
);
1281
return uid;
1282
}
1283
1284
/*
1285
** Attempt to use Basic Authentication to establish the user. Return the
1286
** (non-zero) uid if successful. Return 0 if it does not work.
1287
*/
1288
static int login_basic_authentication(const char *zIpAddr){
1289
const char *zAuth = PD("HTTP_AUTHORIZATION", 0);
1290
int i;
1291
int uid = 0;
1292
int nDecode = 0;
1293
char *zDecode = 0;
1294
const char *zUsername = 0;
1295
const char *zPasswd = 0;
1296
1297
if( zAuth==0 ) return 0; /* Fail: No Authentication: header */
1298
while( fossil_isspace(zAuth[0]) ) zAuth++; /* Skip leading whitespace */
1299
if( strncmp(zAuth, "Basic ", 6)!=0 ){
1300
return 0; /* Fail: Not Basic Authentication */
1301
}
1302
1303
/* Parse out the username and password, separated by a ":" */
1304
zAuth += 6;
1305
while( fossil_isspace(zAuth[0]) ) zAuth++;
1306
zDecode = decode64(zAuth, &nDecode);
1307
1308
for(i=0; zDecode[i] && zDecode[i]!=':'; i++){}
1309
if( zDecode[i] ){
1310
zDecode[i] = 0;
1311
zUsername = zDecode;
1312
zPasswd = &zDecode[i+1];
1313
1314
/* Attempting to log in as the user provided by HTTP
1315
** basic auth
1316
*/
1317
uid = login_search_uid(&zUsername, zPasswd);
1318
if( uid>0 ){
1319
record_login_attempt(zUsername, zIpAddr, 1);
1320
}else{
1321
record_login_attempt(zUsername, zIpAddr, 0);
1322
1323
/* The user attempted to login specifically with HTTP basic
1324
** auth, but provided invalid credentials. Inform them of
1325
** the failed login attempt via 401.
1326
*/
1327
cgi_set_status(401, "Unauthorized");
1328
cgi_reply();
1329
fossil_exit(0);
1330
}
1331
}
1332
fossil_free(zDecode);
1333
return uid;
1334
}
1335
1336
/*
1337
** When this routine is called, we know that the request does not
1338
** have a login on the present repository. This routine checks to
1339
** see if their login cookie might be for another member of the
1340
** login-group.
1341
**
1342
** If this repository is not a part of any login group, then this
1343
** routine always returns false.
1344
**
1345
** If this repository is part of a login group, and the login cookie
1346
** appears to be well-formed, then return true. That might be a
1347
** false-positive, as we don't actually check to see if the login
1348
** cookie is valid for some other repository. But false-positives
1349
** are ok. This routine is used for robot defense only.
1350
*/
1351
int login_cookie_wellformed(void){
1352
const char *zCookie;
1353
int n;
1354
zCookie = P(login_cookie_name());
1355
if( zCookie==0 ){
1356
return 0;
1357
}
1358
if( !db_exists("SELECT 1 FROM config WHERE name='login-group-code'") ){
1359
return 0;
1360
}
1361
for(n=0; fossil_isXdigit(zCookie[n]); n++){}
1362
return n>48 && zCookie[n]=='/' && zCookie[n+1]!=0;
1363
}
1364
1365
/*
1366
** This routine examines the login cookie to see if it exists and
1367
** is valid. If the login cookie checks out, it then sets global
1368
** variables appropriately.
1369
**
1370
** g.userUid Database USER.UID value. Might be -1 for "nobody"
1371
** g.zLogin Database USER.LOGIN value. NULL for user "nobody"
1372
** g.perm Permissions granted to this user
1373
** g.anon Permissions that would be available to anonymous
1374
** g.isRobot True if the client is known to be a spider or robot
1375
** g.perm Populated based on user account's capabilities
1376
** g.eAuthMethod The mechanism used for authentication
1377
**
1378
*/
1379
void login_check_credentials(void){
1380
int uid = 0; /* User id */
1381
const char *zCookie; /* Text of the login cookie */
1382
const char *zIpAddr; /* Raw IP address of the requestor */
1383
const char *zCap = 0; /* Capability string */
1384
const char *zLogin = 0; /* Login user for credentials */
1385
1386
/* Only run this check once. */
1387
if( g.userUid!=0 ) return;
1388
1389
sqlite3_create_function(g.db, "constant_time_cmp", 2, SQLITE_UTF8, 0,
1390
constant_time_cmp_function, 0, 0);
1391
1392
/* If the HTTP connection is coming over 127.0.0.1 and if
1393
** local login is disabled and if we are using HTTP and not HTTPS,
1394
** then there is no need to check user credentials.
1395
**
1396
** This feature allows the "fossil ui" command to give the user
1397
** full access rights without having to log in.
1398
*/
1399
zIpAddr = PD("REMOTE_ADDR","nil");
1400
if( ( cgi_is_loopback(zIpAddr)
1401
|| (g.fSshClient & CGI_SSH_CLIENT)!=0 )
1402
&& g.useLocalauth
1403
&& db_get_booocalauth",0)==0
1404
&& P("HTTPS")==0
1405
){
1406
char *zSeed;
1407
if( g.localOpen ) zLogin = db_lget("default-user",0);
1408
if( zLogin!=0 ){
1409
uid = db_int(0, "SELECT uid FROM user WHERE login=%Q", zLogin);
1410
}else{
1411
uid = db_int(0, "SELECT uid FROM user WHERE cap LIKE '%%s%%'");
1412
}
1413
g.zLogin = db_text("?", "SELECT login FROM user WHERE uid=%d", uid);
1414
zCap = "sxy";
1415
g.noPswd = 1;
1416
g.isRobot = 0;
1417
g.eAuthMethod = AUTH_LOCAL;
1418
zSeed = db_text("??", "SELECT uid||quote(login)||quote(pw)||quote(cookie)"
1419
" FROM user WHERE uid=%d", uid);
1420
login_create_csrf_secret(zSeed);
1421
fossil_free(zSeed);
1422
}
1423
1424
/* Check the login cookie to see if it matches a known valid user.
1425
*/
1426
if( uid==0 && (zCookie = P(login_cookie_name()))!=0 ){
1427
/* Parse the cookie value up into HASH/ARG/USER */
1428
char *zHash = fossil_strdup(zCookie);
1429
char *zArg = 0;
1430
char *zUser = 0;
1431
int i, c;
1432
for(i=0; (c = zHash[i])!=0; i++){
1433
if( c=='/' ){
1434
zHash[i++] = 0;
1435
if( zArg==0 ){
1436
zArg = &zHash[i];
1437
}else{
1438
zUser = &zHash[i];
1439
break;
1440
}
1441
}
1442
}
1443
if( zUser==0 ){
1444
/* Invalid cookie */
1445
}else if( fossil_strcmp(zUser, "anonymous")==0
1446
&& anon_cookie_lifespan()>0 ){
1447
/* Cookies of the form "HASH/TIME/anonymous". The TIME must
1448
** not be more than ANONYMOUS_COOKIE_LIFESPAN seconds ago and
1449
** the sha1 hash of TIME/USERAGENT/SECRET must match HASH. USERAGENT
1450
** is the HTTP_USER_AGENT of the client and SECRET is the
1451
** "captcha-secret" value in the repository. See tag-20250817a
1452
** for the code the creates this cookie.
1453
*/
1454
double rTime = fossil_atof(zArg);
1455
const char *zUserAgent = PD("HTTP_USER_AGENT","nil");
1456
Blob b;
1457
char *zSecret;
1458
int n = 0;
1459
1460
do{
1461
blob_zero(&b);
1462
zSecret = captcha_secret(n++);
1463
if( zSecret==0 ) break;
1464
blob_appendf(&b, "%s/%s/%s", zArg, zUserAgent, zSecret);
1465
sha1sum_blob(&b, &b);
1466
if( fossil_strcmp(zHash, blob_str(&b))==0 ){
1467
uid = db_int(0,
1468
"SELECT uid FROM user WHERE login='anonymous'"
1469
" AND octet_length(cap)>0"
1470
" AND octet_length(pw)>0"
1471
" AND %.17g>julianday('now')",
1472
rTime+anon_cookie_lifespan()/1440.0
1473
);
1474
}
1475
}while( uid==0 );
1476
blob_reset(&b);
1477
}else{
1478
/* Cookies of the form "HASH/CODE/USER". Search first in the
1479
** local user table, then the user table for project CODE if we
1480
** are part of a login-group.
1481
*/
1482
uid = login_find_user(zUser, zHash);
1483
if( uid==0 && login_transfer_credentials(zUser,zArg,zHash) ){
1484
uid = login_find_user(zUser, zHash);
1485
if( uid ){
1486
record_login_attempt(zUser, zIpAddr, 1);
1487
}else{
1488
/* The login cookie is a valid login for project CODE, but no
1489
** user named USER exists on this repository. Cannot login as
1490
** USER, but at least give them "anonymous" login. */
1491
uid = db_int(0, "SELECT uid FROM user WHERE login='anonymous'"
1492
" AND octet_length(cap)>0"
1493
" AND octet_length(pw)>0");
1494
}
1495
}
1496
}
1497
if( uid ) g.eAuthMethod = AUTH_COOKIE;
1498
login_create_csrf_secret(zHash);
1499
}
1500
1501
/* If no user found and the REMOTE_USER environment variable is set,
1502
** then accept the value of REMOTE_USER as the user.
1503
*/
1504
if( uid==0 ){
1505
const char *zRemoteUser = P("REMOTE_USER");
1506
if( zRemoteUser && db_get_boolean("remote_user_ok",0) ){
1507
uid = db_int(0, "SELECT uid FROM user WHERE login=%Q"
1508
" AND octet_length(cap)>0 AND octet_length(pw)>0",
1509
zRemoteUser);
1510
if( uid ) g.eAuthMethod = AUTH_ENV;
1511
}
1512
}
1513
1514
/* If the request didn't provide a login cookie or the login cookie didn't
1515
** match a known valid user, check the HTTP "Authorization" header and
1516
** see if those credentials are valid for a known user.
1517
*/
1518
if( uid==0 && db_get_boolean("http_authentication_ok",0) ){
1519
uid = login_basic_authentication(zIpAddr);
1520
if( uid ) g.eAuthMethod = AUTH_HTTP;
1521
}
1522
1523
/* Check for magic query parameters "resid" (for the username) and
1524
** "token" for the password. Both values (if they exist) will be
1525
** obfuscated.
1526
*/
1527
if( uid==0 ){
1528
char *zUsr, *zPW;
1529
if( (zUsr = unobscure(P("resid")))!=0
1530
&& (zPW = unobscure(P("token")))!=0
1531
){
1532
char *zSha1Pw = sha1_shared_secret(zPW, zUsr, 0);
1533
uid = db_int(0, "SELECT uid FROM user"
1534
" WHERE login=%Q"
1535
" AND (constant_time_cmp(pw,%Q)=0"
1536
" OR constant_time_cmp(pw,%Q)=0)",
1537
zUsr, zSha1Pw, zPW);
1538
fossil_free(zSha1Pw);
1539
if( uid ) g.eAuthMethod = AUTH_PW;
1540
}
1541
}
1542
1543
/* If no user found yet, try to log in as "nobody" */
1544
if( uid==0 ){
1545
uid = db_int(0, "SELECT uid FROM user WHERE login='nobody'");
1546
if( uid==0 ){
1547
/* If there is no user "nobody", then make one up - with no privileges */
1548
uid = -1;
1549
zCap = "";
1550
}
1551
login_create_csrf_secret("none");
1552
}
1553
1554
login_set_uid(uid, zCap);
1555
1556
/* Maybe restrict access by robots */
1557
if( g.zLogin==0 && robot_restrict(g.zPath) ){
1558
cgi_reply();
1559
fossil_exit(0);
1560
}
1561
}
1562
1563
/*
1564
** Set the current logged in user to be uid. zCap is precomputed
1565
** (override) capabilities. If zCap==0, then look up the capabilities
1566
** in the USER table.
1567
*/
1568
int login_set_uid(int uid, const char *zCap){
1569
const char *zPublicPages = 0; /* GLOB patterns of public pages */
1570
1571
/* At this point, we know that uid!=0. Find the privileges associated
1572
** with user uid.
1573
*/
1574
assert( uid!=0 );
1575
if( zCap==0 ){
1576
Stmt s;
1577
db_prepare(&s, "SELECT login, cap FROM user WHERE uid=%d", uid);
1578
if( db_step(&s)==SQLITE_ROW ){
1579
g.zLogin = db_column_malloc(&s, 0);
1580
zCap = db_column_malloc(&s, 1);
1581
}
1582
db_finalize(&s);
1583
if( zCap==0 ){
1584
zCap = "";
1585
}
1586
}
1587
if( g.fHttpTrace && g.zLogin ){
1588
fprintf(stderr, "# login: [%s] with capabilities [%s]\n", g.zLogin, zCap);
1589
}
1590
1591
/* Set the global variables recording the userid and login. The
1592
** "nobody" user is a special case in that g.zLogin==0.
1593
*/
1594
g.userUid = uid;
1595
if( fossil_strcmp(g.zLogin,"nobody")==0 ){
1596
g.zLogin = 0;
1597
}
1598
if( PB("isrobot") ){
1599
g.isRobot = 1;
1600
}else if( g.zLogin==0 ){
1601
g.isRobot = !isHuman(P("HTTP_USER_AGENT"));
1602
}else{
1603
g.isRobot = 0;
1604
}
1605
1606
/* Set the capabilities */
1607
login_replace_capabilities(zCap, 0);
1608
1609
/* The auto-hyperlink setting allows hyperlinks to be displayed for users
1610
** who do not have the "h" permission as long as their UserAgent string
1611
** makes it appear that they are human. Check to see if auto-hyperlink is
1612
** enabled for this repository and make appropriate adjustments to the
1613
** permission flags if it is. This should be done before the permissions
1614
** are (potentially) copied to the anonymous permission set; otherwise,
1615
** those will be out-of-sync.
1616
*/
1617
if( zCap[0] && !g.perm.Hyperlink && !g.isRobot ){
1618
int autoLink = db_get_int("auto-hyperlink",1);
1619
if( autoLink==1 ){
1620
g.jsHref = 1;
1621
g.perm.Hyperlink = 1;
1622
}else if( autoLink==2 ){
1623
g.perm.Hyperlink = 1;
1624
}
1625
}
1626
1627
/*
1628
** At this point, the capabilities for the logged in user are not going
1629
** to be modified anymore; therefore, we can copy them over to the ones
1630
** for the anonymous user.
1631
**
1632
** WARNING: In the future, please do not add code after this point that
1633
** modifies the capabilities for the logged in user.
1634
*/
1635
login_set_anon_nobody_capabilities();
1636
1637
/* If the public-pages glob pattern is defined and REQUEST_URI matches
1638
** one of the globs in public-pages, then also add in all default-perms
1639
** permissions.
1640
*/
1641
zPublicPages = db_get("public-pages",0);
1642
if( zPublicPages!=0 ){
1643
const char *zUri = PD("REQUEST_URI","");
1644
zUri += (int)strlen(g.zTop);
1645
if( glob_multi_match(zPublicPages, zUri) ){
1646
login_set_capabilities(db_get("default-perms", "u"), 0);
1647
}
1648
}
1649
return g.zLogin!=0;
1650
}
1651
1652
/*
1653
** Memory of settings
1654
*/
1655
static int login_anon_once = 1;
1656
1657
/*
1658
** Add to g.perm the default privileges of users "nobody" and/or "anonymous"
1659
** as appropriate for the user g.zLogin.
1660
**
1661
** This routine also sets up g.anon to be either a copy of g.perm for
1662
** all logged in uses, or the privileges that would be available to "anonymous"
1663
** if g.zLogin==0 (meaning that the user is "nobody").
1664
*/
1665
void login_set_anon_nobody_capabilities(void){
1666
if( login_anon_once ){
1667
const char *zCap;
1668
/* All users get privileges from "nobody" */
1669
zCap = db_text("", "SELECT cap FROM user WHERE login = 'nobody'");
1670
login_set_capabilities(zCap, 0);
1671
zCap = db_text("", "SELECT cap FROM user WHERE login = 'anonymous'");
1672
if( g.zLogin && fossil_strcmp(g.zLogin, "nobody")!=0 ){
1673
/* All logged-in users inherit privileges from "anonymous" */
1674
login_set_capabilities(zCap, 0);
1675
g.anon = g.perm;
1676
}else{
1677
/* Record the privileges of anonymous in g.anon */
1678
g.anon = g.perm;
1679
login_set_capabilities(zCap, LOGIN_ANON);
1680
}
1681
login_anon_once = 0;
1682
}
1683
}
1684
1685
/*
1686
** Flags passed into the 2nd argument of login_set/replace_capabilities().
1687
*/
1688
#if INTERFACE
1689
#define LOGIN_IGNORE_UV 0x01 /* Ignore "u" and "v" */
1690
#define LOGIN_ANON 0x02 /* Use g.anon instead of g.perm */
1691
#endif
1692
1693
/*
1694
** Adds all capability flags in zCap to g.perm or g.anon.
1695
*/
1696
void login_set_capabilities(const char *zCap, unsigned flags){
1697
int i;
1698
FossilUserPerms *p = (flags & LOGIN_ANON) ? &g.anon : &g.perm;
1699
if(NULL==zCap){
1700
return;
1701
}
1702
for(i=0; zCap[i]; i++){
1703
switch( zCap[i] ){
1704
case 's': p->Setup = 1; /* Fall thru into Admin */
1705
case 'a': p->Admin = p->RdTkt = p->WrTkt = p->Zip =
1706
p->RdWiki = p->WrWiki = p->NewWiki =
1707
p->ApndWiki = p->Hyperlink = p->Clone =
1708
p->NewTkt = p->Password = p->RdAddr =
1709
p->TktFmt = p->Attach = p->ApndTkt =
1710
p->ModWiki = p->ModTkt =
1711
p->RdForum = p->WrForum = p->ModForum =
1712
p->WrTForum = p->AdminForum = p->Chat =
1713
p->EmailAlert = p->Announce = p->Debug = 1;
1714
/* Fall thru into Read/Write */
1715
case 'i': p->Read = p->Write = 1; break;
1716
case 'o': p->Read = 1; break;
1717
case 'z': p->Zip = 1; break;
1718
1719
case 'h': p->Hyperlink = 1; break;
1720
case 'g': p->Clone = 1; break;
1721
case 'p': p->Password = 1; break;
1722
1723
case 'j': p->RdWiki = 1; break;
1724
case 'k': p->WrWiki = p->RdWiki = p->ApndWiki =1; break;
1725
case 'm': p->ApndWiki = 1; break;
1726
case 'f': p->NewWiki = 1; break;
1727
case 'l': p->ModWiki = 1; break;
1728
1729
case 'e': p->RdAddr = 1; break;
1730
case 'r': p->RdTkt = 1; break;
1731
case 'n': p->NewTkt = 1; break;
1732
case 'w': p->WrTkt = p->RdTkt = p->NewTkt =
1733
p->ApndTkt = 1; break;
1734
case 'c': p->ApndTkt = 1; break;
1735
case 'q': p->ModTkt = 1; break;
1736
case 't': p->TktFmt = 1; break;
1737
case 'b': p->Attach = 1; break;
1738
case 'x': p->Private = 1; break;
1739
case 'y': p->WrUnver = 1; break;
1740
1741
case '6': p->AdminForum = 1;
1742
case '5': p->ModForum = 1;
1743
case '4': p->WrTForum = 1;
1744
case '3': p->WrForum = 1;
1745
case '2': p->RdForum = 1; break;
1746
1747
case '7': p->EmailAlert = 1; break;
1748
case 'A': p->Announce = 1; break;
1749
case 'C': p->Chat = 1; break;
1750
case 'D': p->Debug = 1; break;
1751
1752
/* The "u" privilege recursively
1753
** inherits all privileges of the user named "reader" */
1754
case 'u': {
1755
if( p->XReader==0 ){
1756
const char *zUser;
1757
p->XReader = 1;
1758
zUser = db_text("", "SELECT cap FROM user WHERE login='reader'");
1759
login_set_capabilities(zUser, flags);
1760
}
1761
break;
1762
}
1763
1764
/* The "v" privilege recursively
1765
** inherits all privileges of the user named "developer" */
1766
case 'v': {
1767
if( p->XDeveloper==0 ){
1768
const char *zDev;
1769
p->XDeveloper = 1;
1770
zDev = db_text("", "SELECT cap FROM user WHERE login='developer'");
1771
login_set_capabilities(zDev, flags);
1772
}
1773
break;
1774
}
1775
}
1776
}
1777
}
1778
1779
/*
1780
** Zeroes out g.perm and calls login_set_capabilities(zCap,flags).
1781
*/
1782
void login_replace_capabilities(const char *zCap, unsigned flags){
1783
memset(&g.perm, 0, sizeof(g.perm));
1784
login_set_capabilities(zCap, flags);
1785
login_anon_once = 1;
1786
}
1787
1788
/*
1789
** If the current login lacks any of the capabilities listed in
1790
** the input, then return 0. If all capabilities are present, then
1791
** return 1.
1792
**
1793
** As a special case, the 'L' pseudo-capability ID means "is logged
1794
** in" and will return true for any non-guest user.
1795
*/
1796
int login_has_capability(const char *zCap, int nCap, u32 flgs){
1797
int i;
1798
int rc = 1;
1799
FossilUserPerms *p = (flgs & LOGIN_ANON) ? &g.anon : &g.perm;
1800
if( nCap<0 ) nCap = strlen(zCap);
1801
for(i=0; i<nCap && rc && zCap[i]; i++){
1802
switch( zCap[i] ){
1803
case 'a': rc = p->Admin; break;
1804
case 'b': rc = p->Attach; break;
1805
case 'c': rc = p->ApndTkt; break;
1806
/* d unused: see comment in capabilities.c */
1807
case 'e': rc = p->RdAddr; break;
1808
case 'f': rc = p->NewWiki; break;
1809
case 'g': rc = p->Clone; break;
1810
case 'h': rc = p->Hyperlink; break;
1811
case 'i': rc = p->Write; break;
1812
case 'j': rc = p->RdWiki; break;
1813
case 'k': rc = p->WrWiki; break;
1814
case 'l': rc = p->ModWiki; break;
1815
case 'm': rc = p->ApndWiki; break;
1816
case 'n': rc = p->NewTkt; break;
1817
case 'o': rc = p->Read; break;
1818
case 'p': rc = p->Password; break;
1819
case 'q': rc = p->ModTkt; break;
1820
case 'r': rc = p->RdTkt; break;
1821
case 's': rc = p->Setup; break;
1822
case 't': rc = p->TktFmt; break;
1823
/* case 'u': READER */
1824
/* case 'v': DEVELOPER */
1825
case 'w': rc = p->WrTkt; break;
1826
case 'x': rc = p->Private; break;
1827
case 'y': rc = p->WrUnver; break;
1828
case 'z': rc = p->Zip; break;
1829
case '2': rc = p->RdForum; break;
1830
case '3': rc = p->WrForum; break;
1831
case '4': rc = p->WrTForum; break;
1832
case '5': rc = p->ModForum; break;
1833
case '6': rc = p->AdminForum;break;
1834
case '7': rc = p->EmailAlert;break;
1835
case 'A': rc = p->Announce; break;
1836
case 'C': rc = p->Chat; break;
1837
case 'D': rc = p->Debug; break;
1838
case 'L': rc = g.zLogin && *g.zLogin; break;
1839
/* Mainenance reminder: '@' should not be used because
1840
it would semantically collide with the @ in the
1841
capexpr TH1 command. */
1842
default: rc = 0; break;
1843
}
1844
}
1845
return rc;
1846
}
1847
1848
/*
1849
** Change the login to zUser.
1850
*/
1851
void login_as_user(const char *zUser){
1852
char *zCap = ""; /* New capabilities */
1853
1854
/* Turn off all capabilities from prior logins */
1855
memset( &g.perm, 0, sizeof(g.perm) );
1856
1857
/* Set the global variables recording the userid and login. The
1858
** "nobody" user is a special case in that g.zLogin==0.
1859
*/
1860
g.userUid = db_int(0, "SELECT uid FROM user WHERE login=%Q", zUser);
1861
if( g.userUid==0 ){
1862
zUser = 0;
1863
g.userUid = db_int(0, "SELECT uid FROM user WHERE login='nobody'");
1864
}
1865
if( g.userUid ){
1866
zCap = db_text("", "SELECT cap FROM user WHERE uid=%d", g.userUid);
1867
}
1868
if( fossil_strcmp(zUser,"nobody")==0 ) zUser = 0;
1869
g.zLogin = fossil_strdup(zUser);
1870
1871
/* Set the capabilities */
1872
login_set_capabilities(zCap, 0);
1873
login_anon_once = 1;
1874
login_set_anon_nobody_capabilities();
1875
}
1876
1877
/*
1878
** Return true if the user is "nobody"
1879
*/
1880
int login_is_nobody(void){
1881
return g.zLogin==0 || g.zLogin[0]==0 || fossil_strcmp(g.zLogin,"nobody")==0;
1882
}
1883
1884
/*
1885
** Return true if the user is a specific individual, not "nobody" or
1886
** "anonymous".
1887
*/
1888
int login_is_individual(void){
1889
return g.zLogin!=0 && g.zLogin[0]!=0 && fossil_strcmp(g.zLogin,"nobody")!=0
1890
&& fossil_strcmp(g.zLogin,"anonymous")!=0;
1891
}
1892
1893
/*
1894
** Return the login name. If no login name is specified, return "nobody".
1895
*/
1896
const char *login_name(void){
1897
return (g.zLogin && g.zLogin[0]) ? g.zLogin : "nobody";
1898
}
1899
1900
/*
1901
** Call this routine when the credential check fails. It causes
1902
** a redirect to the "login" page.
1903
*/
1904
void login_needed(int anonOk){
1905
#ifdef FOSSIL_ENABLE_JSON
1906
if(g.json.isJsonMode){
1907
json_err( FSL_JSON_E_DENIED, NULL, 1 );
1908
fossil_exit(0);
1909
/* NOTREACHED */
1910
assert(0);
1911
}else
1912
#endif /* FOSSIL_ENABLE_JSON */
1913
{
1914
const char *zQS = P("QUERY_STRING");
1915
const char *zPathInfo = PD("PATH_INFO","");
1916
Blob redir;
1917
blob_init(&redir, 0, 0);
1918
if( zPathInfo[0]=='/' ) zPathInfo++; /* skip leading slash */
1919
if( fossil_wants_https(1) ){
1920
blob_appendf(&redir, "%s/login?g=%T", g.zHttpsURL, zPathInfo);
1921
}else{
1922
blob_appendf(&redir, "%R/login?g=%T", zPathInfo);
1923
}
1924
if( zQS && zQS[0] ){
1925
blob_appendf(&redir, "%%3f%T", zQS);
1926
}
1927
if( anonOk ) blob_append(&redir, "&anon=1", 7);
1928
cgi_redirect(blob_str(&redir));
1929
/* NOTREACHED */
1930
assert(0);
1931
}
1932
}
1933
1934
/*
1935
** Call this routine if the user lacks g.perm.Hyperlink permission. If
1936
** the anonymous user has Hyperlink permission, then paint a message
1937
** to inform the user that much more information is available by
1938
** logging in as anonymous.
1939
*/
1940
void login_anonymous_available(void){
1941
if( !g.perm.Hyperlink && g.anon.Hyperlink && anon_cookie_lifespan()>0 ){
1942
const char *zUrl = PD("PATH_INFO", "");
1943
@ <p>Many <span class="disabled">hyperlinks are disabled.</span><br>
1944
@ Use <a href="%R/login?anon=1&amp;g=%T(zUrl)">anonymous login</a>
1945
@ to enable hyperlinks.</p>
1946
}
1947
}
1948
1949
/*
1950
** While rendering a form, call this routine to add the Anti-CSRF token
1951
** as a hidden element of the form.
1952
*/
1953
void login_insert_csrf_secret(void){
1954
@ <input type="hidden" name="csrf" value="%s(g.zCsrfToken)">
1955
}
1956
1957
/*
1958
** Check to see if the candidate username zUserID is already used.
1959
** Return 1 if it is already in use. Return 0 if the name is
1960
** available for a self-registration.
1961
*/
1962
static int login_self_chosen_userid_already_exists(const char *zUserID){
1963
int rc = db_exists(
1964
"SELECT 1 FROM user WHERE login=%Q "
1965
"UNION ALL "
1966
"SELECT 1 FROM event WHERE user=%Q OR euser=%Q",
1967
zUserID, zUserID, zUserID
1968
);
1969
return rc;
1970
}
1971
1972
/*
1973
** zEMail is an email address. (Example: "[email protected]".) This routine
1974
** searches for a user or subscriber that has that email address. If the
1975
** email address is used no-where in the system, return 0. If the email
1976
** address is assigned to a particular user return the UID for that user.
1977
** If the email address is used, but not by a particular user, return -1.
1978
*/
1979
static int email_address_in_use(const char *zEMail){
1980
int uid;
1981
uid = db_int(0,
1982
"SELECT uid FROM user"
1983
" WHERE info LIKE '%%<%q>%%'", zEMail);
1984
if( uid>0 ){
1985
if( db_exists("SELECT 1 FROM user WHERE uid=%d AND ("
1986
" cap GLOB '*[as]*' OR"
1987
" find_emailaddr(info)<>%Q COLLATE nocase)",
1988
uid, zEMail) ){
1989
uid = -1;
1990
}
1991
}
1992
if( uid==0 && alert_tables_exist() ){
1993
uid = db_int(0,
1994
"SELECT user.uid FROM subscriber JOIN user ON login=suname"
1995
" WHERE semail=%Q AND sverified", zEMail);
1996
if( uid ){
1997
if( db_exists("SELECT 1 FROM user WHERE uid=%d AND "
1998
" cap GLOB '*[as]*'",
1999
uid) ){
2000
uid = -1;
2001
}
2002
}
2003
}
2004
return uid;
2005
}
2006
2007
/*
2008
** COMMAND: test-email-used
2009
** Usage: fossil test-email-used EMAIL ...
2010
**
2011
** Given a list of email addresses, show the UID and LOGIN associated
2012
** with each one.
2013
*/
2014
void test_email_used(void){
2015
int i;
2016
db_find_and_open_repository(0, 0);
2017
verify_all_options();
2018
if( g.argc<3 ){
2019
usage("EMAIL ...");
2020
}
2021
for(i=2; i<g.argc; i++){
2022
const char *zEMail = g.argv[i];
2023
int uid = email_address_in_use(zEMail);
2024
if( uid==0 ){
2025
fossil_print("%s: not used\n", zEMail);
2026
}else if( uid<0 ){
2027
fossil_print("%s: used but no password reset is available\n", zEMail);
2028
}else{
2029
char *zLogin = db_text(0, "SELECT login FROM user WHERE uid=%d", uid);
2030
fossil_print("%s: UID %d (%s)\n", zEMail, uid, zLogin);
2031
fossil_free(zLogin);
2032
}
2033
}
2034
}
2035
2036
2037
/*
2038
** Check an email address and confirm that it is valid for self-registration.
2039
** The email address is known already to be well-formed. Return true
2040
** if the email address is on the allowed list.
2041
**
2042
** The default behavior is that any valid email address is accepted.
2043
** But if the "auth-sub-email" setting exists and is not empty, then
2044
** it is a comma-separated list of GLOB patterns for email addresses
2045
** that are authorized to self-register.
2046
*/
2047
int authorized_subscription_email(const char *zEAddr){
2048
char *zGlob = db_get("auth-sub-email",0);
2049
char *zAddr;
2050
int rc;
2051
2052
if( zGlob==0 || zGlob[0]==0 ) return 1;
2053
zGlob = fossil_strtolwr(fossil_strdup(zGlob));
2054
zAddr = fossil_strtolwr(fossil_strdup(zEAddr));
2055
rc = glob_multi_match(zGlob, zAddr);
2056
fossil_free(zGlob);
2057
fossil_free(zAddr);
2058
return rc!=0;
2059
}
2060
2061
/*
2062
** WEBPAGE: register
2063
**
2064
** Page to allow users to self-register. The "self-register" setting
2065
** must be enabled for this page to operate.
2066
*/
2067
void register_page(void){
2068
const char *zUserID, *zPasswd, *zConfirm, *zEAddr;
2069
const char *zDName;
2070
unsigned int uSeed;
2071
const char *zDecoded;
2072
int iErrLine = -1;
2073
const char *zErr = 0;
2074
int uid = 0; /* User id with the same email */
2075
int captchaIsCorrect = 0; /* True on a correct captcha */
2076
char *zCaptcha = ""; /* Value of the captcha text */
2077
char *zPerms; /* Permissions for the default user */
2078
int canDoAlerts = 0; /* True if receiving email alerts is possible */
2079
int doAlerts = 0; /* True if subscription is wanted too */
2080
2081
if( !db_get_boolean("self-register", 0) ){
2082
style_header("Registration not possible");
2083
@ <p>This project does not allow user self-registration. Please contact the
2084
@ project administrator to obtain an account.</p>
2085
style_finish_page();
2086
return;
2087
}
2088
if( P("pwreset")!=0 && login_self_password_reset_available() ){
2089
/* The "Request Password Reset" button was pressed, so render the
2090
** "Request Password Reset" page instead of this one. */
2091
login_reqpwreset_page();
2092
return;
2093
}
2094
zPerms = db_get("default-perms", "u");
2095
login_check_credentials();
2096
2097
/* Prompt the user for email alerts if this repository is configured for
2098
** email alerts and if the default permissions include "7" */
2099
canDoAlerts = alert_tables_exist() && (db_int(0,
2100
"SELECT fullcap(%Q) GLOB '*7*'", zPerms
2101
) || db_get_boolean("selfreg-verify",0));
2102
doAlerts = canDoAlerts && atoi(PD("alerts","1"))!=0;
2103
2104
zUserID = PDT("u","");
2105
zPasswd = PDT("p","");
2106
zConfirm = PDT("cp","");
2107
zEAddr = PDT("ea","");
2108
zDName = PDT("dn","");
2109
2110
/* Verify user imputs */
2111
if( P("new")==0 || !cgi_csrf_safe(2) ){
2112
/* This is not a valid form submission. Fall through into
2113
** the form display */
2114
}else if( (captchaIsCorrect = captcha_is_correct(1))==0 ){
2115
iErrLine = 6;
2116
zErr = "Incorrect CAPTCHA";
2117
}else if( strlen(zUserID)<6 ){
2118
iErrLine = 1;
2119
zErr = "User ID too short. Must be at least 6 characters.";
2120
}else if( sqlite3_strglob("*[^-a-zA-Z0-9_.]*",zUserID)==0 ){
2121
iErrLine = 1;
2122
zErr = "User ID may not contain spaces or special characters.";
2123
}else if( sqlite3_strlike("anonymous%", zUserID, 0)==0
2124
|| sqlite3_strlike("nobody%", zUserID, 0)==0
2125
|| sqlite3_strlike("reader%", zUserID, 0)==0
2126
|| sqlite3_strlike("developer%", zUserID, 0)==0
2127
){
2128
iErrLine = 1;
2129
zErr = "This User ID is reserved. Choose something different.";
2130
}else if( zDName[0]==0 ){
2131
iErrLine = 2;
2132
zErr = "Required";
2133
}else if( zEAddr[0]==0 ){
2134
iErrLine = 3;
2135
zErr = "Required";
2136
}else if( email_address_is_valid(zEAddr,0)==0 ){
2137
iErrLine = 3;
2138
zErr = "Not a valid email address";
2139
}else if( authorized_subscription_email(zEAddr)==0 ){
2140
iErrLine = 3;
2141
zErr = "Not an authorized email address";
2142
}else if( strlen(zPasswd)<6 ){
2143
iErrLine = 4;
2144
zErr = "Password must be at least 6 characters long";
2145
}else if( fossil_strcmp(zPasswd,zConfirm)!=0 ){
2146
iErrLine = 5;
2147
zErr = "Passwords do not match";
2148
}else if( (uid = email_address_in_use(zEAddr))!=0 ){
2149
iErrLine = 3;
2150
zErr = "This email address is already associated with a user";
2151
}else if( login_self_chosen_userid_already_exists(zUserID) ){
2152
iErrLine = 1;
2153
zErr = "This User ID is already taken. Choose something different.";
2154
}else{
2155
/* If all of the tests above have passed, that means that the submitted
2156
** form contains valid data and we can proceed to create the new login */
2157
Blob sql;
2158
int uid;
2159
char *zPass = sha1_shared_secret(zPasswd, zUserID, 0);
2160
const char *zStartPerms = zPerms;
2161
if( db_get_boolean("selfreg-verify",0) ){
2162
/* If email verification is required for self-registration, initialize
2163
** the new user capabilities to just "7" (Sign up for email). The
2164
** full "default-perms" permissions will be added when they click
2165
** the verification link on the email they are sent. */
2166
zStartPerms = "7";
2167
}
2168
blob_init(&sql, 0, 0);
2169
blob_append_sql(&sql,
2170
"INSERT INTO user(login,pw,cap,info,mtime)\n"
2171
"VALUES(%Q,%Q,%Q,"
2172
"'%q <%q>\nself-register from ip %q on '||datetime('now'),now())",
2173
zUserID, zPass, zStartPerms, zDName, zEAddr, g.zIpAddr);
2174
fossil_free(zPass);
2175
db_unprotect(PROTECT_USER);
2176
db_multi_exec("%s", blob_sql_text(&sql));
2177
db_protect_pop();
2178
uid = db_int(0, "SELECT uid FROM user WHERE login=%Q", zUserID);
2179
login_set_user_cookie(zUserID, uid, NULL, 0);
2180
if( doAlerts ){
2181
/* Also make the new user a subscriber. */
2182
Blob hdr, body;
2183
AlertSender *pSender;
2184
const char *zCode; /* New subscriber code (in hex) */
2185
const char *zGoto = P("g");
2186
int nsub = 0;
2187
char ssub[20];
2188
CapabilityString *pCap;
2189
pCap = capability_add(0, zPerms);
2190
capability_expand(pCap);
2191
ssub[nsub++] = 'a';
2192
if( capability_has_any(pCap,"o") ) ssub[nsub++] = 'c';
2193
if( capability_has_any(pCap,"2") ) ssub[nsub++] = 'f';
2194
if( capability_has_any(pCap,"r") ) ssub[nsub++] = 't';
2195
if( capability_has_any(pCap,"j") ) ssub[nsub++] = 'w';
2196
ssub[nsub] = 0;
2197
capability_free(pCap);
2198
/* Also add the user to the subscriber table. */
2199
zCode = db_text(0,
2200
"INSERT INTO subscriber(semail,suname,"
2201
" sverified,sdonotcall,sdigest,ssub,sctime,mtime,smip,lastContact)"
2202
" VALUES(%Q,%Q,%d,0,%d,%Q,now(),now(),%Q,now()/86400)"
2203
" ON CONFLICT(semail) DO UPDATE"
2204
" SET suname=excluded.suname"
2205
" RETURNING hex(subscriberCode);",
2206
/* semail */ zEAddr,
2207
/* suname */ zUserID,
2208
/* sverified */ 0,
2209
/* sdigest */ 0,
2210
/* ssub */ ssub,
2211
/* smip */ g.zIpAddr
2212
);
2213
if( db_exists("SELECT 1 FROM subscriber WHERE semail=%Q"
2214
" AND sverified", zEAddr) ){
2215
/* This the case where the user was formerly a verified subscriber
2216
** and here they have also registered as a user as well. It is
2217
** not necessary to repeat the verification step */
2218
login_redirect_to_g();
2219
}
2220
/* A verification email */
2221
pSender = alert_sender_new(0,0);
2222
blob_init(&hdr,0,0);
2223
blob_init(&body,0,0);
2224
blob_appendf(&hdr, "To: <%s>\n", zEAddr);
2225
blob_appendf(&hdr, "Subject: Subscription verification\n");
2226
alert_append_confirmation_message(&body, zCode);
2227
alert_send(pSender, &hdr, &body, 0);
2228
style_header("Email Verification");
2229
if( pSender->zErr ){
2230
@ <h1>Internal Error</h1>
2231
@ <p>The following internal error was encountered while trying
2232
@ to send the confirmation email:
2233
@ <blockquote><pre>
2234
@ %h(pSender->zErr)
2235
@ </pre></blockquote>
2236
}else{
2237
@ <p>An email has been sent to "%h(zEAddr)". That email contains a
2238
@ hyperlink that you must click to activate your account.</p>
2239
}
2240
alert_sender_free(pSender);
2241
if( zGoto ){
2242
@ <p><a href='%h(zGoto)'>Continue</a>
2243
}
2244
style_finish_page();
2245
return;
2246
}
2247
login_redirect_to_g();
2248
}
2249
2250
/* Prepare the captcha. */
2251
if( captchaIsCorrect ){
2252
uSeed = strtoul(P("captchaseed"),0,10);
2253
}else{
2254
uSeed = captcha_seed();
2255
}
2256
zDecoded = captcha_decode(uSeed, 0);
2257
zCaptcha = captcha_render(zDecoded);
2258
2259
style_header("Register");
2260
/* Print out the registration form. */
2261
g.perm.Hyperlink = 1; /* Artificially enable hyperlinks */
2262
form_begin(0, "%R/register");
2263
if( P("g") ){
2264
@ <input type="hidden" name="g" value="%h(P("g"))">
2265
}
2266
@ <p><input type="hidden" name="captchaseed" value="%u(uSeed)">
2267
@ <table class="login_out">
2268
@ <tr>
2269
@ <td class="form_label" align="right" id="uid">User ID:</td>
2270
@ <td><input aria-labelledby="uid" type="text" name="u" \
2271
@ value="%h(zUserID)" size="30" autofocus></td>
2272
@
2273
if( iErrLine==1 ){
2274
@ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
2275
}
2276
@ <tr>
2277
@ <td class="form_label" align="right" id="dpyname">Display Name:</td>
2278
@ <td><input aria-labelledby="dpyname" type="text" name="dn" \
2279
@ value="%h(zDName)" size="30"></td>
2280
@ </tr>
2281
if( iErrLine==2 ){
2282
@ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
2283
}
2284
@ </tr>
2285
@ <tr>
2286
@ <td class="form_label" align="right" id="emaddr">Email Address:</td>
2287
@ <td><input aria-labelledby="emaddr" type="text" name="ea" \
2288
@ value="%h(zEAddr)" size="30"></td>
2289
@ </tr>
2290
if( iErrLine==3 ){
2291
@ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span>
2292
if( uid>0 && login_self_password_reset_available() ){
2293
@ <br>
2294
@ <input type="submit" name="pwreset" \
2295
@ value="Request Password Reset For %h(zEAddr)">
2296
}
2297
@ </td></tr>
2298
}
2299
if( canDoAlerts ){
2300
int a = atoi(PD("alerts","1"));
2301
@ <tr>
2302
@ <td class="form_label" align="right" id="emalrt">Email&nbsp;Alerts?</td>
2303
@ <td><select aria-labelledby="emalrt" size='1' name='alerts'>
2304
@ <option value="1" %s(a?"selected":"")>Yes</option>
2305
@ <option value="0" %s(!a?"selected":"")>No</option>
2306
@ </select></td></tr>
2307
}
2308
@ <tr>
2309
@ <td class="form_label" align="right" id="pswd">Password:</td>
2310
@ <td><input aria-labelledby="pswd" type="password" name="p" \
2311
@ value="%h(zPasswd)" size="30"> \
2312
if( zPasswd[0]==0 ){
2313
char *zRPW = fossil_random_password(12);
2314
@ Password suggestion: %z(zRPW)</td>
2315
}else{
2316
@ </td>
2317
}
2318
@ <tr>
2319
if( iErrLine==4 ){
2320
@ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
2321
}
2322
@ <tr>
2323
@ <td class="form_label" align="right" id="pwcfrm">Confirm:</td>
2324
@ <td><input aria-labelledby="pwcfrm" type="password" name="cp" \
2325
@ value="%h(zConfirm)" size="30"></td>
2326
@ </tr>
2327
if( iErrLine==5 ){
2328
@ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
2329
}
2330
@ <tr>
2331
@ <td class="form_label" align="right" id="cptcha">Captcha:</td>
2332
@ <td><input type="text" name="captcha" aria-labelledby="cptcha" \
2333
@ value="%h(captchaIsCorrect?zDecoded:"")" size="30">
2334
captcha_speakit_button(uSeed, "Speak the captcha text");
2335
@ </td>
2336
@ </tr>
2337
if( iErrLine==6 ){
2338
@ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
2339
}
2340
@ <tr><td></td>
2341
@ <td><input type="submit" name="new" value="Register"></td></tr>
2342
@ </table>
2343
@ <div class="captcha"><table class="captcha"><tr><td><pre class="captcha">
2344
@ %h(zCaptcha)
2345
@ </pre>
2346
@ Enter this 8-letter code in the "Captcha" box above.
2347
@ </td></tr></table></div>
2348
@ </form>
2349
style_finish_page();
2350
2351
free(zCaptcha);
2352
}
2353
2354
/*
2355
** WEBPAGE: reqpwreset
2356
**
2357
** A web page to request a password reset.
2358
**
2359
** A form is presented where the user can enter their email address
2360
** and a captcha. If the email address entered corresponds to a known
2361
** users, an email is sent to that address that contains a link to the
2362
** /resetpw page that allows the users to enter a new password.
2363
**
2364
** This page is only available if the self-pw-reset property is enabled
2365
** and email notifications are configured and operating. Password resets
2366
** are not available to users with Admin or Setup privilege.
2367
*/
2368
void login_reqpwreset_page(void){
2369
const char *zEAddr;
2370
const char *zDecoded;
2371
unsigned int uSeed;
2372
int iErrLine = -1;
2373
const char *zErr = 0;
2374
int uid = 0; /* User id with the email zEAddr */
2375
int captchaIsCorrect = 0; /* True on a correct captcha */
2376
char *zCaptcha = ""; /* Value of the captcha text */
2377
2378
if( !login_self_password_reset_available() ){
2379
style_header("Password reset not possible");
2380
@ <p>This project does not allow users to reset their own passwords.
2381
@ If you need a password reset, you will have to negotiate that directly
2382
@ with the project administrator.
2383
style_finish_page();
2384
return;
2385
}
2386
zEAddr = PDT("ea","");
2387
2388
/* Verify user inputs */
2389
if( !cgi_csrf_safe(1) || P("reqpwreset")==0 ){
2390
/* This is the initial display of the form. No processing or error
2391
** checking is to be done. Fall through into the form display
2392
**
2393
** cgi_csrf_safe(): Nothing interesting happens on this page without
2394
** a valid captcha solution, so we only need to check referrer and that
2395
** the request is a POST.
2396
*/
2397
}else if( (captchaIsCorrect = captcha_is_correct(1))==0 ){
2398
iErrLine = 2;
2399
zErr = "Incorrect CAPTCHA";
2400
}else if( zEAddr[0]==0 ){
2401
iErrLine = 1;
2402
zErr = "Required";
2403
}else if( email_address_is_valid(zEAddr,0)==0 ){
2404
iErrLine = 1;
2405
zErr = "Not a valid email address";
2406
}else if( authorized_subscription_email(zEAddr)==0 ){
2407
iErrLine = 1;
2408
zErr = "Not an authorized email address";
2409
}else if( (uid = email_address_in_use(zEAddr))<=0 ){
2410
iErrLine = 1;
2411
zErr = "This email address is not associated with a user who has "
2412
"password reset privileges.";
2413
}else if( login_set_uid(uid,0)==0 || g.perm.Admin || g.perm.Setup
2414
|| !g.perm.Password ){
2415
iErrLine = 1;
2416
zErr = "This email address is not associated with a user who has "
2417
"password reset privileges.";
2418
}else{
2419
2420
/* If all of the tests above have passed, that means that the submitted
2421
** form contains valid data and we can proceed to issue the password
2422
** reset email. */
2423
Blob hdr, body;
2424
AlertSender *pSender;
2425
char *zUrl = login_resetpw_suffix(uid, 0);
2426
pSender = alert_sender_new(0,0);
2427
blob_init(&hdr,0,0);
2428
blob_init(&body,0,0);
2429
blob_appendf(&hdr, "To: <%s>\n", zEAddr);
2430
blob_appendf(&hdr, "Subject: Password reset for %s\n", g.zBaseURL);
2431
blob_appendf(&body,
2432
"Someone has requested to reset the password for user \"%s\"\n",
2433
g.zLogin);
2434
blob_appendf(&body, "at %s.\n\n", g.zBaseURL);
2435
blob_appendf(&body,
2436
"If you did not request this password reset, ignore\n"
2437
"this email\n\n");
2438
blob_appendf(&body,
2439
"To reset the password, visit the following link:\n\n"
2440
" %s/resetpw/%s\n\n", g.zBaseURL, zUrl);
2441
fossil_free(zUrl);
2442
alert_send(pSender, &hdr, &body, 0);
2443
style_header("Email Verification");
2444
if( pSender->zErr ){
2445
@ <h1>Internal Error</h1>
2446
@ <p>The following internal error was encountered while trying
2447
@ to send the confirmation email:
2448
@ <blockquote><pre>
2449
@ %h(pSender->zErr)
2450
@ </pre></blockquote>
2451
}else{
2452
@ <p>An email containing a hyperlink that can be used to reset
2453
@ your password has been sent to "%h(zEAddr)".</p>
2454
}
2455
alert_sender_free(pSender);
2456
style_finish_page();
2457
return;
2458
}
2459
2460
/* Prepare the captcha. */
2461
if( captchaIsCorrect ){
2462
uSeed = strtoul(P("captchaseed"),0,10);
2463
}else{
2464
uSeed = captcha_seed();
2465
}
2466
zDecoded = captcha_decode(uSeed, 0);
2467
zCaptcha = captcha_render(zDecoded);
2468
2469
style_header("Request Password Reset");
2470
/* Print out the registration form. */
2471
g.perm.Hyperlink = 1; /* Artificially enable hyperlinks */
2472
form_begin(0, "%R/reqpwreset");
2473
@ <p><input type="hidden" name="captchaseed" value="%u(uSeed)">
2474
@ <p><input type="hidden" name="reqpwreset" value="1">
2475
@ <table class="login_out">
2476
@ <tr>
2477
@ <td class="form_label" align="right" id="emaddr">Email Address:</td>
2478
@ <td><input aria-labelledby="emaddr" type="text" name="ea" \
2479
@ value="%h(zEAddr)" size="30"></td>
2480
@ </tr>
2481
if( iErrLine==1 ){
2482
@ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
2483
}
2484
@ <tr>
2485
@ <td class="form_label" align="right" id="cptcha">Captcha:</td>
2486
@ <td><input type="text" name="captcha" aria-labelledby="cptcha" \
2487
@ value="%h(captchaIsCorrect?zDecoded:"")" size="30">
2488
captcha_speakit_button(uSeed, "Speak the captcha text");
2489
@ </td>
2490
@ </tr>
2491
if( iErrLine==2 ){
2492
@ <tr><td><td><span class='loginError'>&uarr; %h(zErr)</span></td></tr>
2493
}
2494
@ <tr><td></td>
2495
@ <td><input type="submit" name="new" value="Request Password Reset"/>\
2496
@ </td></tr>
2497
@ </table>
2498
@ <div class="captcha"><table class="captcha"><tr><td><pre class="captcha">
2499
@ %h(zCaptcha)
2500
@ </pre>
2501
@ Enter this 8-letter code in the "Captcha" box above.
2502
@ </td></tr></table></div>
2503
@ </form>
2504
style_finish_page();
2505
free(zCaptcha);
2506
}
2507
2508
/*
2509
** Run SQL on the repository database for every repository in our
2510
** login group. The SQL is run in a separate database connection.
2511
**
2512
** Any members of the login group whose repository database file
2513
** cannot be found is silently removed from the group.
2514
**
2515
** Error messages accumulate and are returned in *pzErrorMsg. The
2516
** memory used to hold these messages should be freed using
2517
** fossil_free() if one desired to avoid a memory leak. The
2518
** zPrefix and zSuffix strings surround each error message.
2519
**
2520
** Return the number of errors.
2521
*/
2522
int login_group_sql(
2523
const char *zSql, /* The SQL to run */
2524
const char *zPrefix, /* Prefix to each error message */
2525
const char *zSuffix, /* Suffix to each error message */
2526
char **pzErrorMsg /* Write error message here, if not NULL */
2527
){
2528
sqlite3 *pPeer; /* Connection to another database */
2529
int nErr = 0; /* Number of errors seen so far */
2530
int rc; /* Result code from subroutine calls */
2531
char *zErr; /* SQLite error text */
2532
char *zSelfCode; /* Project code for ourself */
2533
Blob err; /* Accumulate errors here */
2534
Stmt q; /* Query of all peer-* entries in CONFIG */
2535
2536
if( zPrefix==0 ) zPrefix = "";
2537
if( zSuffix==0 ) zSuffix = "";
2538
if( pzErrorMsg ) *pzErrorMsg = 0;
2539
zSelfCode = abbreviated_project_code(db_get("project-code", "x"));
2540
blob_zero(&err);
2541
db_prepare(&q,
2542
"SELECT name, value FROM config"
2543
" WHERE name GLOB 'peer-repo-*'"
2544
" AND name <> 'peer-repo-%q'"
2545
" ORDER BY +value",
2546
zSelfCode
2547
);
2548
while( db_step(&q)==SQLITE_ROW ){
2549
const char *zRepoName = db_column_text(&q, 1);
2550
if( file_size(zRepoName, ExtFILE)<0 ){
2551
/* Silently remove non-existent repositories from the login group. */
2552
const char *zLabel = db_column_text(&q, 0);
2553
db_unprotect(PROTECT_CONFIG);
2554
db_multi_exec(
2555
"DELETE FROM config WHERE name GLOB 'peer-*-%q'",
2556
&zLabel[10]
2557
);
2558
db_protect_pop();
2559
continue;
2560
}
2561
rc = sqlite3_open_v2(
2562
zRepoName, &pPeer,
2563
SQLITE_OPEN_READWRITE,
2564
g.zVfsName
2565
);
2566
if( rc!=SQLITE_OK ){
2567
blob_appendf(&err, "%s%s: %s%s", zPrefix, zRepoName,
2568
sqlite3_errmsg(pPeer), zSuffix);
2569
nErr++;
2570
sqlite3_close(pPeer);
2571
continue;
2572
}
2573
sqlite3_create_function(pPeer, "shared_secret", 3, SQLITE_UTF8,
2574
0, sha1_shared_secret_sql_function, 0, 0);
2575
sqlite3_create_function(pPeer, "now", 0,SQLITE_UTF8,0,db_now_function,0,0);
2576
sqlite3_busy_timeout(pPeer, 5000);
2577
zErr = 0;
2578
rc = sqlite3_exec(pPeer, zSql, 0, 0, &zErr);
2579
if( zErr ){
2580
blob_appendf(&err, "%s%s: %s%s", zPrefix, zRepoName, zErr, zSuffix);
2581
sqlite3_free(zErr);
2582
nErr++;
2583
}else if( rc!=SQLITE_OK ){
2584
blob_appendf(&err, "%s%s: %s%s", zPrefix, zRepoName,
2585
sqlite3_errmsg(pPeer), zSuffix);
2586
nErr++;
2587
}
2588
sqlite3_close(pPeer);
2589
}
2590
db_finalize(&q);
2591
if( pzErrorMsg && blob_size(&err)>0 ){
2592
*pzErrorMsg = fossil_strdup(blob_str(&err));
2593
}
2594
blob_reset(&err);
2595
fossil_free(zSelfCode);
2596
return nErr;
2597
}
2598
2599
/*
2600
** Attempt to join a login-group.
2601
**
2602
** If problems arise, leave an error message in *pzErrMsg.
2603
*/
2604
void login_group_join(
2605
const char *zRepo, /* Repository file in the login group */
2606
int bPwRequired, /* True if the login,password is required */
2607
const char *zLogin, /* Login name for the other repo */
2608
const char *zPassword, /* Password to prove we are authorized to join */
2609
const char *zNewName, /* Name of new login group if making a new one */
2610
char **pzErrMsg /* Leave an error message here */
2611
){
2612
Blob fullName; /* Blob for finding full pathnames */
2613
sqlite3 *pOther; /* The other repository */
2614
int rc; /* Return code from sqlite3 functions */
2615
char *zOtherProjCode; /* Project code for pOther */
2616
char *zSelfRepo; /* Name of our repository */
2617
char *zSelfLabel; /* Project-name for our repository */
2618
char *zSelfProjCode; /* Our project-code */
2619
char *zSql; /* SQL to run on all peers */
2620
const char *zSelf; /* The ATTACH name of our repository */
2621
2622
*pzErrMsg = 0; /* Default to no errors */
2623
zSelf = "repository";
2624
2625
/* Get the full pathname of the other repository */
2626
file_canonical_name(zRepo, &fullName, 0);
2627
zRepo = fossil_strdup(blob_str(&fullName));
2628
blob_reset(&fullName);
2629
2630
/* Get the full pathname for our repository. Also the project code
2631
** and project name for ourself. */
2632
file_canonical_name(g.zRepositoryName, &fullName, 0);
2633
zSelfRepo = fossil_strdup(blob_str(&fullName));
2634
blob_reset(&fullName);
2635
zSelfProjCode = db_get("project-code", "unknown");
2636
zSelfLabel = db_get("project-name", 0);
2637
if( zSelfLabel==0 ){
2638
zSelfLabel = zSelfProjCode;
2639
}
2640
2641
/* Make sure we are not trying to join ourselves */
2642
if( fossil_strcmp(zRepo, zSelfRepo)==0 ){
2643
*pzErrMsg = mprintf("The \"other\" repository is the same as this one.");
2644
return;
2645
}
2646
2647
/* Make sure the other repository is a valid Fossil database */
2648
if( file_size(zRepo, ExtFILE)<0 ){
2649
*pzErrMsg = mprintf("repository file \"%s\" does not exist", zRepo);
2650
return;
2651
}
2652
rc = sqlite3_open_v2(
2653
zRepo, &pOther,
2654
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE,
2655
g.zVfsName
2656
);
2657
if( rc!=SQLITE_OK ){
2658
*pzErrMsg = fossil_strdup(sqlite3_errmsg(pOther));
2659
}else{
2660
rc = sqlite3_exec(pOther, "SELECT count(*) FROM user", 0, 0, pzErrMsg);
2661
}
2662
sqlite3_close(pOther);
2663
if( rc ) return;
2664
2665
/* Attach the other repository. Make sure the username/password is
2666
** valid and has Setup permission.
2667
*/
2668
db_attach(zRepo, "other");
2669
zOtherProjCode = db_text("x", "SELECT value FROM other.config"
2670
" WHERE name='project-code'");
2671
if( bPwRequired ){
2672
char *zPwHash; /* Password hash on pOther */
2673
zPwHash = sha1_shared_secret(zPassword, zLogin, zOtherProjCode);
2674
if( !db_exists(
2675
"SELECT 1 FROM other.user"
2676
" WHERE login=%Q AND cap GLOB '*s*'"
2677
" AND (pw=%Q OR pw=%Q)",
2678
zLogin, zPassword, zPwHash)
2679
){
2680
db_detach("other");
2681
*pzErrMsg = "The supplied username/password does not correspond to a"
2682
" user Setup permission on the other repository.";
2683
return;
2684
}
2685
}
2686
2687
/* Create all the necessary CONFIG table entries on both the
2688
** other repository and on our own repository.
2689
*/
2690
zSelfProjCode = abbreviated_project_code(zSelfProjCode);
2691
zOtherProjCode = abbreviated_project_code(zOtherProjCode);
2692
db_begin_transaction();
2693
db_unprotect(PROTECT_CONFIG);
2694
db_multi_exec(
2695
"DELETE FROM \"%w\".config WHERE name GLOB 'peer-*';"
2696
"INSERT INTO \"%w\".config(name,value) VALUES('peer-repo-%q',%Q);"
2697
"INSERT INTO \"%w\".config(name,value) "
2698
" SELECT 'peer-name-%q', value FROM other.config"
2699
" WHERE name='project-name';",
2700
zSelf,
2701
zSelf, zOtherProjCode, zRepo,
2702
zSelf, zOtherProjCode
2703
);
2704
db_multi_exec(
2705
"INSERT OR IGNORE INTO other.config(name,value)"
2706
" VALUES('login-group-name',%Q);"
2707
"INSERT OR IGNORE INTO other.config(name,value)"
2708
" VALUES('login-group-code',lower(hex(randomblob(8))));",
2709
zNewName
2710
);
2711
db_multi_exec(
2712
"REPLACE INTO \"%w\".config(name,value)"
2713
" SELECT name, value FROM other.config"
2714
" WHERE name GLOB 'peer-*' OR name GLOB 'login-group-*'",
2715
zSelf
2716
);
2717
db_protect_pop();
2718
db_end_transaction(0);
2719
db_multi_exec("DETACH other");
2720
2721
/* Propagate the changes to all other members of the login-group */
2722
zSql = mprintf(
2723
"BEGIN;"
2724
"REPLACE INTO config(name,value,mtime) VALUES('peer-name-%q',%Q,now());"
2725
"REPLACE INTO config(name,value,mtime) VALUES('peer-repo-%q',%Q,now());"
2726
"COMMIT;",
2727
zSelfProjCode, zSelfLabel, zSelfProjCode, zSelfRepo
2728
);
2729
db_unprotect(PROTECT_CONFIG);
2730
login_group_sql(zSql, "<li> ", "</li>", pzErrMsg);
2731
db_protect_pop();
2732
fossil_free(zSql);
2733
}
2734
2735
/*
2736
** Leave the login group that we are currently part of.
2737
*/
2738
void login_group_leave(char **pzErrMsg){
2739
char *zProjCode;
2740
char *zSql;
2741
2742
*pzErrMsg = 0;
2743
zProjCode = abbreviated_project_code(db_get("project-code","x"));
2744
zSql = mprintf(
2745
"DELETE FROM config WHERE name GLOB 'peer-*-%q';"
2746
"DELETE FROM config"
2747
" WHERE name='login-group-name'"
2748
" AND (SELECT count(*) FROM config WHERE name GLOB 'peer-*')==0;",
2749
zProjCode
2750
);
2751
fossil_free(zProjCode);
2752
db_unprotect(PROTECT_CONFIG);
2753
login_group_sql(zSql, "<li> ", "</li>", pzErrMsg);
2754
fossil_free(zSql);
2755
db_multi_exec(
2756
"DELETE FROM config "
2757
" WHERE name GLOB 'peer-*'"
2758
" OR name GLOB 'login-group-*';"
2759
);
2760
db_protect_pop();
2761
}
2762
2763
/*
2764
** COMMAND: login-group*
2765
**
2766
** Usage: %fossil login-group ?SUBCOMMAND? ?OPTIONS?
2767
**
2768
*

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button