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