Fossil SCM

Merge support for login-groups and single sign-on into the trunk.

drh 2011-04-12 23:41 trunk merge
Commit a257fde3baeb85477be76a65d435d8a9a6138eb0
+1 -1
--- src/http.c
+++ src/http.c
@@ -75,11 +75,11 @@
7575
*/
7676
if( zPw && zPw[0] ){
7777
if( zPw[0]=='*' ){
7878
zPw++;
7979
}else{
80
- zPw = sha1_shared_secret(zPw, zLogin);
80
+ zPw = sha1_shared_secret(zPw, zLogin, 0);
8181
}
8282
}
8383
8484
blob_append(&pw, zPw, -1);
8585
sha1sum_blob(&pw, &sig);
8686
--- src/http.c
+++ src/http.c
@@ -75,11 +75,11 @@
75 */
76 if( zPw && zPw[0] ){
77 if( zPw[0]=='*' ){
78 zPw++;
79 }else{
80 zPw = sha1_shared_secret(zPw, zLogin);
81 }
82 }
83
84 blob_append(&pw, zPw, -1);
85 sha1sum_blob(&pw, &sig);
86
--- src/http.c
+++ src/http.c
@@ -75,11 +75,11 @@
75 */
76 if( zPw && zPw[0] ){
77 if( zPw[0]=='*' ){
78 zPw++;
79 }else{
80 zPw = sha1_shared_secret(zPw, zLogin, 0);
81 }
82 }
83
84 blob_append(&pw, zPw, -1);
85 sha1sum_blob(&pw, &sig);
86
+483 -67
--- src/login.c
+++ src/login.c
@@ -17,16 +17,20 @@
1717
**
1818
** This file contains code for generating the login and logout screens.
1919
**
2020
** Notes:
2121
**
22
-** There are two special-case user-ids: "anonymous" and "nobody".
22
+** There are four special-case user-ids: "anonymous", "nobody",
23
+** "developer" and "reader".
24
+**
2325
** The capabilities of the nobody user are available to anyone,
2426
** regardless of whether or not they are logged in. The capabilities
2527
** of anonymous are only available after logging in, but the login
2628
** screen displays the password for the anonymous login, so this
27
-** should not prevent a human user from doing so.
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.
2832
**
2933
** The nobody user has capabilities that you want spiders to have.
3034
** The anonymous user has capabilities that you want people without
3135
** logins to have.
3236
**
@@ -46,19 +50,53 @@
4650
# endif
4751
#endif
4852
#include <time.h>
4953
5054
/*
51
-** Return the name of the login cookie
55
+** Return the login-group name. Or return 0 if this repository is
56
+** not a member of a login-group.
57
+*/
58
+const char *login_group_name(void){
59
+ static const char *zGroup = 0;
60
+ static int once = 1;
61
+ if( once ){
62
+ zGroup = db_get("login-group-name", 0);
63
+ once = 0;
64
+ }
65
+ return zGroup;
66
+}
67
+
68
+/*
69
+** Return a path appropriate for setting a cookie.
70
+**
71
+** The path is g.zTop for single-repo cookies. It is "/" for
72
+** cookies of a login-group.
73
+*/
74
+static const char *login_cookie_path(void){
75
+ if( login_group_name()==0 ){
76
+ return g.zTop;
77
+ }else{
78
+ return "/";
79
+ }
80
+}
81
+
82
+/*
83
+** Return the name of the login cookie.
84
+**
85
+** The login cookie name is always of the form: fossil-XXXXXXXXXXXXXXXX
86
+** where the Xs are the first 16 characters of the login-group-code or
87
+** of the project-code if we are not a member of any login-group.
5288
*/
5389
static char *login_cookie_name(void){
5490
static char *zCookieName = 0;
5591
if( zCookieName==0 ){
56
- unsigned int h = 0;
57
- const char *z = g.zBaseURL;
58
- while( *z ){ h = (h<<3) ^ (h>>26) ^ *(z++); }
59
- zCookieName = mprintf("fossil_login_%08x", h);
92
+ zCookieName = db_text(0,
93
+ "SELECT 'fossil-' || substr(value,1,16)"
94
+ " FROM config"
95
+ " WHERE name IN ('project-code','login-group-code')"
96
+ " ORDER BY name;"
97
+ );
6098
}
6199
return zCookieName;
62100
}
63101
64102
/*
@@ -73,15 +111,14 @@
73111
fossil_redirect_home();
74112
}
75113
}
76114
77115
/*
78
-** The IP address of the client is stored as part of the anonymous
79
-** login cookie for additional security. But some clients are behind
80
-** firewalls that shift the IP address with each HTTP request. To
81
-** allow such (broken) clients to log in, extract just a prefix of the
82
-** IP address.
116
+** The IP address of the client is stored as part of login cookies.
117
+** But some clients are behind firewalls that shift the IP address
118
+** with each HTTP request. To allow such (broken) clients to log in,
119
+** extract just a prefix of the IP address.
83120
*/
84121
static char *ipPrefix(const char *zIP){
85122
int i, j;
86123
for(i=j=0; zIP[i]; i++){
87124
if( zIP[i]=='.' ){
@@ -89,11 +126,21 @@
89126
if( j==2 ) break;
90127
}
91128
}
92129
return mprintf("%.*s", i, zIP);
93130
}
94
-
131
+
132
+/*
133
+** Return an abbreviated project code. The abbreviation is the first
134
+** 16 characters of the project code.
135
+**
136
+** Memory is obtained from malloc.
137
+*/
138
+static char *abbreviated_project_code(const char *zFullCode){
139
+ return mprintf("%.16s", zFullCode);
140
+}
141
+
95142
96143
/*
97144
** Check to see if the anonymous login is valid. If it is valid, return
98145
** the userid of the anonymous user.
99146
*/
@@ -167,22 +214,25 @@
167214
int anonFlag;
168215
char *zErrMsg = "";
169216
int uid; /* User id loged in user */
170217
char *zSha1Pw;
171218
const char *zIpAddr; /* IP address of requestor */
219
+ char *zRemoteAddr; /* Abbreviated IP address of requestor */
172220
173221
login_check_credentials();
174222
zUsername = P("u");
175223
zPasswd = P("p");
176224
anonFlag = P("anon")!=0;
177225
if( P("out")!=0 ){
226
+ /* To logout, change the cookie value to an empty string */
178227
const char *zCookieName = login_cookie_name();
179
- cgi_set_cookie(zCookieName, "", 0, -86400);
228
+ cgi_set_cookie(zCookieName, "", login_cookie_path(), -86400);
180229
redirect_to_g();
181230
}
182231
if( g.okPassword && zPasswd && (zNew1 = P("n1"))!=0 && (zNew2 = P("n2"))!=0 ){
183
- zSha1Pw = sha1_shared_secret(zPasswd, g.zLogin);
232
+ /* The user requests a password change */
233
+ zSha1Pw = sha1_shared_secret(zPasswd, g.zLogin, 0);
184234
if( db_int(1, "SELECT 0 FROM user"
185235
" WHERE uid=%d AND (pw=%Q OR pw=%Q)",
186236
g.userUid, zPasswd, zSha1Pw) ){
187237
sleep(1);
188238
zErrMsg =
@@ -197,43 +247,70 @@
197247
@ The two copies of your new passwords do not match.
198248
@ Your password is unchanged.
199249
@ </span></p>
200250
;
201251
}else{
202
- char *zNewPw = sha1_shared_secret(zNew1, g.zLogin);
252
+ char *zNewPw = sha1_shared_secret(zNew1, g.zLogin, 0);
253
+ char *zChngPw;
254
+ char *zErr;
203255
db_multi_exec(
204256
"UPDATE user SET pw=%Q WHERE uid=%d", zNewPw, g.userUid
205257
);
206
- redirect_to_g();
207
- return;
258
+ fossil_free(zNewPw);
259
+ zChngPw = mprintf(
260
+ "UPDATE user"
261
+ " SET pw=shared_secret(%Q,%Q,"
262
+ " (SELECT value FROM config WHERE name='project-code'))"
263
+ " WHERE login=%Q",
264
+ zNew1, g.zLogin, g.zLogin
265
+ );
266
+ if( login_group_sql(zChngPw, "<p>", "</p>\n", &zErr) ){
267
+ zErrMsg = mprintf("<span class=\"loginError\">%s</span>", zErr);
268
+ fossil_free(zErr);
269
+ }else{
270
+ redirect_to_g();
271
+ return;
272
+ }
208273
}
209274
}
210
- zIpAddr = PD("REMOTE_ADDR","nil");
275
+ zIpAddr = PD("REMOTE_ADDR","nil"); /* Complete IP address for logging */
276
+ zRemoteAddr = ipPrefix(zIpAddr); /* Abbreviated IP address */
211277
uid = isValidAnonymousLogin(zUsername, zPasswd);
212278
if( uid>0 ){
279
+ /* Successful login as anonymous. Set a cookie that looks like
280
+ ** this:
281
+ **
282
+ ** HASH/TIME/anonymous
283
+ **
284
+ ** Where HASH is the sha1sum of TIME/IPADDR/SECRET, in which IPADDR
285
+ ** is the abbreviated IP address and SECRET is captcha-secret.
286
+ */
213287
char *zNow; /* Current time (julian day number) */
214288
char *zCookie; /* The login cookie */
215289
const char *zCookieName; /* Name of the login cookie */
216290
Blob b; /* Blob used during cookie construction */
217291
218292
zCookieName = login_cookie_name();
219293
zNow = db_text("0", "SELECT julianday('now')");
220294
blob_init(&b, zNow, -1);
221
- blob_appendf(&b, "/%z/%s", ipPrefix(zIpAddr), db_get("captcha-secret",""));
295
+ blob_appendf(&b, "/%s/%s", zRemoteAddr, db_get("captcha-secret",""));
222296
sha1sum_blob(&b, &b);
223
- zCookie = sqlite3_mprintf("anon/%s/%s", zNow, blob_buffer(&b));
297
+ zCookie = sqlite3_mprintf("%s/%s/anonymous", blob_buffer(&b), zNow);
224298
blob_reset(&b);
225299
free(zNow);
226
- cgi_set_cookie(zCookieName, zCookie, 0, 6*3600);
300
+ cgi_set_cookie(zCookieName, zCookie, login_cookie_path(), 6*3600);
227301
record_login_attempt("anonymous", zIpAddr, 1);
228302
redirect_to_g();
229303
}
230304
if( zUsername!=0 && zPasswd!=0 && zPasswd[0]!=0 ){
231
- zSha1Pw = sha1_shared_secret(zPasswd, zUsername);
305
+ /* Attempting to log in as a user other than anonymous.
306
+ */
307
+ zSha1Pw = sha1_shared_secret(zPasswd, zUsername, 0);
232308
uid = db_int(0,
233309
"SELECT uid FROM user"
234310
" WHERE login=%Q"
311
+ " AND length(cap)>0 AND length(pw)>0"
235312
" AND login NOT IN ('anonymous','nobody','developer','reader')"
236313
" AND (pw=%Q OR pw=%Q)",
237314
zUsername, zPasswd, zSha1Pw
238315
);
239316
if( uid<=0 ){
@@ -243,23 +320,32 @@
243320
@ You entered an unknown user or an incorrect password.
244321
@ </span></p>
245322
;
246323
record_login_attempt(zUsername, zIpAddr, 0);
247324
}else{
325
+ /* Non-anonymous login is successful. Set a cookie of the form:
326
+ **
327
+ ** HASH/PROJECT/LOGIN
328
+ **
329
+ ** where HASH is a random hex number, PROJECT is either project
330
+ ** code prefix, and LOGIN is the user name.
331
+ */
248332
char *zCookie;
249333
const char *zCookieName = login_cookie_name();
250334
const char *zExpire = db_get("cookie-expire","8766");
251335
int expires = atoi(zExpire)*3600;
252
- const char *zIpAddr = PD("REMOTE_ADDR","nil");
253
-
254
- zCookie = db_text(0, "SELECT '%d/' || hex(randomblob(25))", uid);
255
- cgi_set_cookie(zCookieName, zCookie, 0, expires);
336
+ char *zCode = abbreviated_project_code(db_get("project-code",""));
337
+ char *zHash;
338
+
339
+ zHash = db_text(0, "SELECT hex(randomblob(25))");
340
+ zCookie = mprintf("%s/%s/%s", zHash, zCode, zUsername);
341
+ cgi_set_cookie(zCookieName, zCookie, login_cookie_path(), expires);
256342
record_login_attempt(zUsername, zIpAddr, 1);
257343
db_multi_exec(
258344
"UPDATE user SET cookie=%Q, ipaddr=%Q, "
259345
" cexpire=julianday('now')+%d/86400.0 WHERE uid=%d",
260
- zCookie, ipPrefix(zIpAddr), expires, uid
346
+ zHash, zRemoteAddr, expires, uid
261347
);
262348
redirect_to_g();
263349
}
264350
}
265351
style_header("Login/Logout");
@@ -366,34 +452,118 @@
366452
@ </form>
367453
}
368454
style_footer();
369455
}
370456
457
+/*
458
+** Attempt to find login credentials for user zLogin on a peer repository
459
+** with project code zCode. Transfer those credentials to the local
460
+** repository.
461
+**
462
+** Return true if a transfer was made and false if not.
463
+*/
464
+static int login_transfer_credentials(
465
+ const char *zLogin, /* Login we are looking for */
466
+ const char *zCode, /* Project code of peer repository */
467
+ const char *zHash, /* HASH from login cookie HASH/CODE/LOGIN */
468
+ const char *zRemoteAddr /* Request comes from here */
469
+){
470
+ sqlite3 *pOther = 0; /* The other repository */
471
+ sqlite3_stmt *pStmt; /* Query against the other repository */
472
+ char *zSQL; /* SQL of the query against other repo */
473
+ char *zOtherRepo; /* Filename of the other repository */
474
+ int rc; /* Result code from SQLite library functions */
475
+ int nXfer = 0; /* Number of credentials transferred */
476
+
477
+ zOtherRepo = db_text(0,
478
+ "SELECT value FROM config WHERE name='peer-repo-%q'",
479
+ zCode
480
+ );
481
+ if( zOtherRepo==0 ) return 0; /* No such peer repository */
482
+
483
+ rc = sqlite3_open(zOtherRepo, &pOther);
484
+ if( rc==SQLITE_OK ){
485
+ zSQL = mprintf(
486
+ "SELECT cexpire FROM user"
487
+ " WHERE cookie=%Q"
488
+ " AND ipaddr=%Q"
489
+ " AND login=%Q"
490
+ " AND length(cap)>0"
491
+ " AND length(pw)>0"
492
+ " AND cexpire>julianday('now')",
493
+ zHash, zRemoteAddr, zLogin
494
+ );
495
+ pStmt = 0;
496
+ rc = sqlite3_prepare_v2(pOther, zSQL, -1, &pStmt, 0);
497
+ if( rc==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){
498
+ db_multi_exec(
499
+ "UPDATE user SET cookie=%Q, ipaddr=%Q, cexpire=%.17g"
500
+ " WHERE login=%Q",
501
+ zHash, zRemoteAddr,
502
+ sqlite3_column_double(pStmt, 0), zLogin
503
+ );
504
+ nXfer++;
505
+ }
506
+ sqlite3_finalize(pStmt);
507
+ }
508
+ sqlite3_close(pOther);
509
+ fossil_free(zOtherRepo);
510
+ return nXfer;
511
+}
371512
513
+/*
514
+** Lookup the uid for a user with zLogin and zCookie and zRemoteAddr.
515
+** Return 0 if not found.
516
+*/
517
+static int login_find_user(
518
+ const char *zLogin, /* User name */
519
+ const char *zCookie, /* Login cookie value */
520
+ const char *zRemoteAddr /* Abbreviated IP address for valid login */
521
+){
522
+ int uid;
523
+ if( fossil_strcmp(zLogin, "anonymous")==0 ) return 0;
524
+ if( fossil_strcmp(zLogin, "nobody")==0 ) return 0;
525
+ if( fossil_strcmp(zLogin, "developer")==0 ) return 0;
526
+ if( fossil_strcmp(zLogin, "reader")==0 ) return 0;
527
+ uid = db_int(0,
528
+ "SELECT uid FROM user"
529
+ " WHERE login=%Q"
530
+ " AND cookie=%Q"
531
+ " AND ipaddr=%Q"
532
+ " AND cexpire>julianday('now')"
533
+ " AND length(cap)>0"
534
+ " AND length(pw)>0",
535
+ zLogin, zCookie, zRemoteAddr
536
+ );
537
+ return uid;
538
+}
372539
373540
/*
374541
** This routine examines the login cookie to see if it exists and
375542
** and is valid. If the login cookie checks out, it then sets
376
-** g.zUserUuid appropriately.
543
+** global variables appropriately. Global variables set include
544
+** g.userUid and g.zLogin and of the g.okRead family of permission
545
+** booleans.
377546
**
378547
*/
379548
void login_check_credentials(void){
380549
int uid = 0; /* User id */
381550
const char *zCookie; /* Text of the login cookie */
382
- const char *zRemoteAddr; /* IP address of the requestor */
551
+ char *zRemoteAddr; /* IP address of the requestor */
383552
const char *zCap = 0; /* Capability string */
384553
385554
/* Only run this check once. */
386555
if( g.userUid!=0 ) return;
387556
388
-
389557
/* If the HTTP connection is coming over 127.0.0.1 and if
390558
** local login is disabled and if we are using HTTP and not HTTPS,
391559
** then there is no need to check user credentials.
392560
**
561
+ ** This feature allows the "fossil ui" command to give the user
562
+ ** full access rights without having to log in.
393563
*/
394
- zRemoteAddr = PD("REMOTE_ADDR","nil");
564
+ zRemoteAddr = ipPrefix(PD("REMOTE_ADDR","nil"));
395565
if( strcmp(zRemoteAddr, "127.0.0.1")==0
396566
&& g.useLocalauth
397567
&& db_get_int("localauth",0)==0
398568
&& P("HTTPS")==0
399569
){
@@ -405,48 +575,60 @@
405575
}
406576
407577
/* Check the login cookie to see if it matches a known valid user.
408578
*/
409579
if( uid==0 && (zCookie = P(login_cookie_name()))!=0 ){
410
- if( fossil_isdigit(zCookie[0]) ){
411
- /* Cookies of the form "uid/randomness". There must be a
412
- ** corresponding entry in the user table. */
413
- uid = db_int(0,
414
- "SELECT uid FROM user"
415
- " WHERE uid=%d"
416
- " AND cookie=%Q"
417
- " AND ipaddr=%Q"
418
- " AND cexpire>julianday('now')",
419
- atoi(zCookie), zCookie, ipPrefix(zRemoteAddr)
420
- );
421
- }else if( memcmp(zCookie,"anon/",5)==0 ){
422
- /* Cookies of the form "anon/TIME/HASH". The TIME must not be
423
- ** too old and the sha1 hash of TIME+IPADDR+SECRET must match HASH.
580
+ /* Parse the cookie value up into HASH/ARG/USER */
581
+ char *zHash = fossil_strdup(zCookie);
582
+ char *zArg = 0;
583
+ char *zUser = 0;
584
+ int i, c;
585
+ for(i=0; (c = zHash[i])!=0; i++){
586
+ if( c=='/' ){
587
+ zHash[i++] = 0;
588
+ if( zArg==0 ){
589
+ zArg = &zHash[i];
590
+ }else{
591
+ zUser = &zHash[i];
592
+ break;
593
+ }
594
+ }
595
+ }
596
+ if( zUser==0 ){
597
+ /* Invalid cookie */
598
+ }else if( strcmp(zUser, "anonymous")==0 ){
599
+ /* Cookies of the form "HASH/TIME/anonymous". The TIME must not be
600
+ ** too old and the sha1 hash of TIME/IPADDR/SECRET must match HASH.
424601
** SECRET is the "captcha-secret" value in the repository.
425602
*/
426
- double rTime;
427
- int i;
603
+ double rTime = atof(zArg);
428604
Blob b;
429
- rTime = atof(&zCookie[5]);
430
- for(i=5; zCookie[i] && zCookie[i]!='/'; i++){}
431
- blob_init(&b, &zCookie[5], i-5);
432
- if( zCookie[i]=='/' ){ i++; }
433
- blob_append(&b, "/", 1);
434
- blob_appendf(&b, "%z/%s", ipPrefix(zRemoteAddr),
435
- db_get("captcha-secret",""));
605
+ blob_zero(&b);
606
+ blob_appendf(&b, "%s/%s/%s",
607
+ zArg, zRemoteAddr, db_get("captcha-secret",""));
436608
sha1sum_blob(&b, &b);
437
- uid = db_int(0,
438
- "SELECT uid FROM user WHERE login='anonymous'"
439
- " AND length(cap)>0"
440
- " AND length(pw)>0"
441
- " AND %f+0.25>julianday('now')"
442
- " AND %Q=%Q",
443
- rTime, &zCookie[i], blob_buffer(&b)
444
- );
609
+ if( fossil_strcmp(zHash, blob_str(&b))==0 ){
610
+ uid = db_int(0,
611
+ "SELECT uid FROM user WHERE login='anonymous'"
612
+ " AND length(cap)>0"
613
+ " AND length(pw)>0"
614
+ " AND %.17g+0.25>julianday('now')",
615
+ rTime
616
+ );
617
+ }
445618
blob_reset(&b);
619
+ }else{
620
+ /* Cookies of the form "HASH/CODE/USER". Search first in the
621
+ ** local user table, then the user table for project CODE if we
622
+ ** are part of a login-group.
623
+ */
624
+ uid = login_find_user(zUser, zHash, zRemoteAddr);
625
+ if( uid==0 && login_transfer_credentials(zUser,zArg,zHash,zRemoteAddr) ){
626
+ uid = login_find_user(zUser, zHash, zRemoteAddr);
627
+ }
446628
}
447
- sqlite3_snprintf(sizeof(g.zCsrfToken), g.zCsrfToken, "%.10s", zCookie);
629
+ sqlite3_snprintf(sizeof(g.zCsrfToken), g.zCsrfToken, "%.10s", zHash);
448630
}
449631
450632
/* If no user found and the REMOTE_USER environment variable is set,
451633
** the accept the value of REMOTE_USER as the user.
452634
*/
@@ -795,11 +977,11 @@
795977
* this %s(zUsername), or at least I don't know how to force it to.*/
796978
@ <p><span class="loginError">
797979
@ %s(zUsername) already exists.
798980
@ </span></p>
799981
}else{
800
- char *zPw = sha1_shared_secret(blob_str(&passwd), blob_str(&login));
982
+ char *zPw = sha1_shared_secret(blob_str(&passwd), blob_str(&login), 0);
801983
int uid;
802984
char *zCookie;
803985
const char *zCookieName;
804986
const char *zExpire;
805987
int expires;
@@ -817,17 +999,17 @@
817999
zExpire = db_get("cookie-expire","8766");
8181000
expires = atoi(zExpire)*3600;
8191001
zIpAddr = PD("REMOTE_ADDR","nil");
8201002
8211003
zCookie = db_text(0, "SELECT '%d/' || hex(randomblob(25))", uid);
822
- cgi_set_cookie(zCookieName, zCookie, 0, expires);
1004
+ cgi_set_cookie(zCookieName, zCookie, login_cookie_path(), expires);
8231005
record_login_attempt(zUsername, zIpAddr, 1);
8241006
db_multi_exec(
8251007
"UPDATE user SET cookie=%Q, ipaddr=%Q, "
8261008
" cexpire=julianday('now')+%d/86400.0 WHERE uid=%d",
8271009
zCookie, ipPrefix(zIpAddr), expires, uid
828
- );
1010
+ );
8291011
redirect_to_g();
8301012
8311013
}
8321014
}
8331015
}
@@ -873,5 +1055,239 @@
8731055
@ </form>
8741056
style_footer();
8751057
8761058
free(zCaptcha);
8771059
}
1060
+
1061
+/*
1062
+** Run SQL on the repository database for every repository in our
1063
+** login group. The SQL is run in a separate database connection.
1064
+**
1065
+** Any members of the login group whose repository database file
1066
+** cannot be found is silently removed from the group.
1067
+**
1068
+** Error messages accumulate and are returned in *pzErrorMsg. The
1069
+** memory used to hold these messages should be freed using
1070
+** fossil_free() if one desired to avoid a memory leak. The
1071
+** zPrefix and zSuffix strings surround each error message.
1072
+**
1073
+** Return the number of errors.
1074
+*/
1075
+int login_group_sql(
1076
+ const char *zSql, /* The SQL to run */
1077
+ const char *zPrefix, /* Prefix to each error message */
1078
+ const char *zSuffix, /* Suffix to each error message */
1079
+ char **pzErrorMsg /* Write error message here, if not NULL */
1080
+){
1081
+ sqlite3 *pPeer; /* Connection to another database */
1082
+ int nErr = 0; /* Number of errors seen so far */
1083
+ int rc; /* Result code from subroutine calls */
1084
+ char *zErr; /* SQLite error text */
1085
+ char *zSelfCode; /* Project code for ourself */
1086
+ Blob err; /* Accumulate errors here */
1087
+ Stmt q; /* Query of all peer-* entries in CONFIG */
1088
+
1089
+ if( zPrefix==0 ) zPrefix = "";
1090
+ if( zSuffix==0 ) zSuffix = "";
1091
+ if( pzErrorMsg ) *pzErrorMsg = 0;
1092
+ zSelfCode = abbreviated_project_code(db_get("project-code", "x"));
1093
+ blob_zero(&err);
1094
+ db_prepare(&q,
1095
+ "SELECT name, value FROM config"
1096
+ " WHERE name GLOB 'peer-repo-*'"
1097
+ " AND name <> 'peer-repo-%q'"
1098
+ " ORDER BY +value",
1099
+ zSelfCode
1100
+ );
1101
+ while( db_step(&q)==SQLITE_ROW ){
1102
+ const char *zRepoName = db_column_text(&q, 1);
1103
+ if( file_size(zRepoName)<0 ){
1104
+ /* Silently remove non-existant repositories from the login group. */
1105
+ const char *zLabel = db_column_text(&q, 0);
1106
+ db_multi_exec(
1107
+ "DELETE FROM config WHERE name GLOB 'peer-*-%q'",
1108
+ &zLabel[10]
1109
+ );
1110
+ continue;
1111
+ }
1112
+ rc = sqlite3_open_v2(zRepoName, &pPeer, SQLITE_OPEN_READWRITE, 0);
1113
+ if( rc!=SQLITE_OK ){
1114
+ blob_appendf(&err, "%s%s: %s%s", zPrefix, zRepoName,
1115
+ sqlite3_errmsg(pPeer), zSuffix);
1116
+ nErr++;
1117
+ sqlite3_close(pPeer);
1118
+ continue;
1119
+ }
1120
+ sqlite3_create_function(pPeer, "shared_secret", 3, SQLITE_UTF8,
1121
+ 0, sha1_shared_secret_sql_function, 0, 0);
1122
+ zErr = 0;
1123
+ rc = sqlite3_exec(pPeer, zSql, 0, 0, &zErr);
1124
+ if( zErr ){
1125
+ blob_appendf(&err, "%s%s: %s%s", zPrefix, zRepoName, zErr, zSuffix);
1126
+ sqlite3_free(zErr);
1127
+ nErr++;
1128
+ }else if( rc!=SQLITE_OK ){
1129
+ blob_appendf(&err, "%s%s: %s%s", zPrefix, zRepoName,
1130
+ sqlite3_errmsg(pPeer), zSuffix);
1131
+ nErr++;
1132
+ }
1133
+ sqlite3_close(pPeer);
1134
+ }
1135
+ db_finalize(&q);
1136
+ if( pzErrorMsg && blob_size(&err)>0 ){
1137
+ *pzErrorMsg = fossil_strdup(blob_str(&err));
1138
+ }
1139
+ blob_reset(&err);
1140
+ fossil_free(zSelfCode);
1141
+ return nErr;
1142
+}
1143
+
1144
+/*
1145
+** Attempt to join a login-group.
1146
+**
1147
+** If problems arise, leave an error message in *pzErrMsg.
1148
+*/
1149
+void login_group_join(
1150
+ const char *zRepo, /* Repository file in the login group */
1151
+ const char *zLogin, /* Login name for the other repo */
1152
+ const char *zPassword, /* Password to prove we are authorized to join */
1153
+ const char *zNewName, /* Name of new login group if making a new one */
1154
+ char **pzErrMsg /* Leave an error message here */
1155
+){
1156
+ Blob fullName; /* Blob for finding full pathnames */
1157
+ sqlite3 *pOther; /* The other repository */
1158
+ int rc; /* Return code from sqlite3 functions */
1159
+ char *zOtherProjCode; /* Project code for pOther */
1160
+ char *zPwHash; /* Password hash on pOther */
1161
+ char *zSelfRepo; /* Name of our repository */
1162
+ char *zSelfLabel; /* Project-name for our repository */
1163
+ char *zSelfProjCode; /* Our project-code */
1164
+ char *zSql; /* SQL to run on all peers */
1165
+ const char *zSelf; /* The ATTACH name of our repository */
1166
+
1167
+ *pzErrMsg = 0; /* Default to no errors */
1168
+ zSelf = db_name("repository");
1169
+
1170
+ /* Get the full pathname of the other repository */
1171
+ file_canonical_name(zRepo, &fullName);
1172
+ zRepo = mprintf(blob_str(&fullName));
1173
+ blob_reset(&fullName);
1174
+
1175
+ /* Get the full pathname for our repository. Also the project code
1176
+ ** and project name for ourself. */
1177
+ file_canonical_name(g.zRepositoryName, &fullName);
1178
+ zSelfRepo = mprintf(blob_str(&fullName));
1179
+ blob_reset(&fullName);
1180
+ zSelfProjCode = db_get("project-code", "unknown");
1181
+ zSelfLabel = db_get("project-name", 0);
1182
+ if( zSelfLabel==0 ){
1183
+ zSelfLabel = zSelfProjCode;
1184
+ }
1185
+
1186
+ /* Make sure we are not trying to join ourselves */
1187
+ if( strcmp(zRepo, zSelfRepo)==0 ){
1188
+ *pzErrMsg = mprintf("The \"other\" repository is the same as this one.");
1189
+ return;
1190
+ }
1191
+
1192
+ /* Make sure the other repository is a valid Fossil database */
1193
+ if( file_size(zRepo)<0 ){
1194
+ *pzErrMsg = mprintf("repository file \"%s\" does not exist", zRepo);
1195
+ return;
1196
+ }
1197
+ rc = sqlite3_open(zRepo, &pOther);
1198
+ if( rc!=SQLITE_OK ){
1199
+ *pzErrMsg = mprintf(sqlite3_errmsg(pOther));
1200
+ }else{
1201
+ rc = sqlite3_exec(pOther, "SELECT count(*) FROM user", 0, 0, pzErrMsg);
1202
+ }
1203
+ sqlite3_close(pOther);
1204
+ if( rc ) return;
1205
+
1206
+ /* Attach the other respository. Make sure the username/password is
1207
+ ** valid and has Setup permission.
1208
+ */
1209
+ db_multi_exec("ATTACH %Q AS other", zRepo);
1210
+ zOtherProjCode = db_text("x", "SELECT value FROM other.config"
1211
+ " WHERE name='project-code'");
1212
+ zPwHash = sha1_shared_secret(zPassword, zLogin, zOtherProjCode);
1213
+ if( !db_exists(
1214
+ "SELECT 1 FROM other.user"
1215
+ " WHERE login=%Q AND cap GLOB '*s*'"
1216
+ " AND (pw=%Q OR pw=%Q)",
1217
+ zLogin, zPassword, zPwHash)
1218
+ ){
1219
+ db_multi_exec("DETACH other");
1220
+ *pzErrMsg = "The supplied username/password does not correspond to a"
1221
+ " user Setup permission on the other repository.";
1222
+ return;
1223
+ }
1224
+
1225
+ /* Create all the necessary CONFIG table entries on both the
1226
+ ** other repository and on our own repository.
1227
+ */
1228
+ zSelfProjCode = abbreviated_project_code(zSelfProjCode);
1229
+ zOtherProjCode = abbreviated_project_code(zOtherProjCode);
1230
+ db_begin_transaction();
1231
+ db_multi_exec(
1232
+ "DELETE FROM %s.config WHERE name GLOB 'peer-*';"
1233
+ "INSERT INTO %s.config(name,value) VALUES('peer-repo-%s',%Q);"
1234
+ "INSERT INTO %s.config(name,value) "
1235
+ " SELECT 'peer-name-%q', value FROM other.config"
1236
+ " WHERE name='project-name';",
1237
+ zSelf,
1238
+ zSelf, zOtherProjCode, zRepo,
1239
+ zSelf, zOtherProjCode
1240
+ );
1241
+ db_multi_exec(
1242
+ "INSERT OR IGNORE INTO other.config(name,value)"
1243
+ " VALUES('login-group-name',%Q);"
1244
+ "INSERT OR IGNORE INTO other.config(name,value)"
1245
+ " VALUES('login-group-code',lower(hex(randomblob(8))));",
1246
+ zNewName
1247
+ );
1248
+ db_multi_exec(
1249
+ "REPLACE INTO %s.config(name,value)"
1250
+ " SELECT name, value FROM other.config"
1251
+ " WHERE name GLOB 'peer-*' OR name GLOB 'login-group-*'",
1252
+ zSelf
1253
+ );
1254
+ db_end_transaction(0);
1255
+ db_multi_exec("DETACH other");
1256
+
1257
+ /* Propagate the changes to all other members of the login-group */
1258
+ zSql = mprintf(
1259
+ "BEGIN;"
1260
+ "REPLACE INTO config(name, value) VALUES('peer-name-%q', %Q);"
1261
+ "REPLACE INTO config(name, value) VALUES('peer-repo-%q', %Q);"
1262
+ "COMMIT;",
1263
+ zSelfProjCode, zSelfLabel, zSelfProjCode, zSelfRepo
1264
+ );
1265
+ login_group_sql(zSql, "<li> ", "</li>", pzErrMsg);
1266
+ fossil_free(zSql);
1267
+}
1268
+
1269
+/*
1270
+** Leave the login group that we are currently part of.
1271
+*/
1272
+void login_group_leave(char **pzErrMsg){
1273
+ char *zProjCode;
1274
+ char *zSql;
1275
+
1276
+ *pzErrMsg = 0;
1277
+ zProjCode = abbreviated_project_code(db_get("project-code","x"));
1278
+ zSql = mprintf(
1279
+ "DELETE FROM config WHERE name GLOB 'peer-*-%q';"
1280
+ "DELETE FROM config"
1281
+ " WHERE name='login-group-name'"
1282
+ " AND (SELECT count(*) FROM config WHERE name GLOB 'peer-*')==0;",
1283
+ zProjCode
1284
+ );
1285
+ fossil_free(zProjCode);
1286
+ login_group_sql(zSql, "<li> ", "</li>", pzErrMsg);
1287
+ fossil_free(zSql);
1288
+ db_multi_exec(
1289
+ "DELETE FROM config "
1290
+ " WHERE name GLOB 'peer-*'"
1291
+ " OR name GLOB 'login-group-*';"
1292
+ );
1293
+}
8781294
--- src/login.c
+++ src/login.c
@@ -17,16 +17,20 @@
17 **
18 ** This file contains code for generating the login and logout screens.
19 **
20 ** Notes:
21 **
22 ** There are two special-case user-ids: "anonymous" and "nobody".
 
 
23 ** The capabilities of the nobody user are available to anyone,
24 ** regardless of whether or not they are logged in. The capabilities
25 ** of anonymous are only available after logging in, but the login
26 ** screen displays the password for the anonymous login, so this
27 ** should not prevent a human user from doing so.
 
 
28 **
29 ** The nobody user has capabilities that you want spiders to have.
30 ** The anonymous user has capabilities that you want people without
31 ** logins to have.
32 **
@@ -46,19 +50,53 @@
46 # endif
47 #endif
48 #include <time.h>
49
50 /*
51 ** Return the name of the login cookie
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52 */
53 static char *login_cookie_name(void){
54 static char *zCookieName = 0;
55 if( zCookieName==0 ){
56 unsigned int h = 0;
57 const char *z = g.zBaseURL;
58 while( *z ){ h = (h<<3) ^ (h>>26) ^ *(z++); }
59 zCookieName = mprintf("fossil_login_%08x", h);
 
 
60 }
61 return zCookieName;
62 }
63
64 /*
@@ -73,15 +111,14 @@
73 fossil_redirect_home();
74 }
75 }
76
77 /*
78 ** The IP address of the client is stored as part of the anonymous
79 ** login cookie for additional security. But some clients are behind
80 ** firewalls that shift the IP address with each HTTP request. To
81 ** allow such (broken) clients to log in, extract just a prefix of the
82 ** IP address.
83 */
84 static char *ipPrefix(const char *zIP){
85 int i, j;
86 for(i=j=0; zIP[i]; i++){
87 if( zIP[i]=='.' ){
@@ -89,11 +126,21 @@
89 if( j==2 ) break;
90 }
91 }
92 return mprintf("%.*s", i, zIP);
93 }
94
 
 
 
 
 
 
 
 
 
 
95
96 /*
97 ** Check to see if the anonymous login is valid. If it is valid, return
98 ** the userid of the anonymous user.
99 */
@@ -167,22 +214,25 @@
167 int anonFlag;
168 char *zErrMsg = "";
169 int uid; /* User id loged in user */
170 char *zSha1Pw;
171 const char *zIpAddr; /* IP address of requestor */
 
172
173 login_check_credentials();
174 zUsername = P("u");
175 zPasswd = P("p");
176 anonFlag = P("anon")!=0;
177 if( P("out")!=0 ){
 
178 const char *zCookieName = login_cookie_name();
179 cgi_set_cookie(zCookieName, "", 0, -86400);
180 redirect_to_g();
181 }
182 if( g.okPassword && zPasswd && (zNew1 = P("n1"))!=0 && (zNew2 = P("n2"))!=0 ){
183 zSha1Pw = sha1_shared_secret(zPasswd, g.zLogin);
 
184 if( db_int(1, "SELECT 0 FROM user"
185 " WHERE uid=%d AND (pw=%Q OR pw=%Q)",
186 g.userUid, zPasswd, zSha1Pw) ){
187 sleep(1);
188 zErrMsg =
@@ -197,43 +247,70 @@
197 @ The two copies of your new passwords do not match.
198 @ Your password is unchanged.
199 @ </span></p>
200 ;
201 }else{
202 char *zNewPw = sha1_shared_secret(zNew1, g.zLogin);
 
 
203 db_multi_exec(
204 "UPDATE user SET pw=%Q WHERE uid=%d", zNewPw, g.userUid
205 );
206 redirect_to_g();
207 return;
 
 
 
 
 
 
 
 
 
 
 
 
 
208 }
209 }
210 zIpAddr = PD("REMOTE_ADDR","nil");
 
211 uid = isValidAnonymousLogin(zUsername, zPasswd);
212 if( uid>0 ){
 
 
 
 
 
 
 
 
213 char *zNow; /* Current time (julian day number) */
214 char *zCookie; /* The login cookie */
215 const char *zCookieName; /* Name of the login cookie */
216 Blob b; /* Blob used during cookie construction */
217
218 zCookieName = login_cookie_name();
219 zNow = db_text("0", "SELECT julianday('now')");
220 blob_init(&b, zNow, -1);
221 blob_appendf(&b, "/%z/%s", ipPrefix(zIpAddr), db_get("captcha-secret",""));
222 sha1sum_blob(&b, &b);
223 zCookie = sqlite3_mprintf("anon/%s/%s", zNow, blob_buffer(&b));
224 blob_reset(&b);
225 free(zNow);
226 cgi_set_cookie(zCookieName, zCookie, 0, 6*3600);
227 record_login_attempt("anonymous", zIpAddr, 1);
228 redirect_to_g();
229 }
230 if( zUsername!=0 && zPasswd!=0 && zPasswd[0]!=0 ){
231 zSha1Pw = sha1_shared_secret(zPasswd, zUsername);
 
 
232 uid = db_int(0,
233 "SELECT uid FROM user"
234 " WHERE login=%Q"
 
235 " AND login NOT IN ('anonymous','nobody','developer','reader')"
236 " AND (pw=%Q OR pw=%Q)",
237 zUsername, zPasswd, zSha1Pw
238 );
239 if( uid<=0 ){
@@ -243,23 +320,32 @@
243 @ You entered an unknown user or an incorrect password.
244 @ </span></p>
245 ;
246 record_login_attempt(zUsername, zIpAddr, 0);
247 }else{
 
 
 
 
 
 
 
248 char *zCookie;
249 const char *zCookieName = login_cookie_name();
250 const char *zExpire = db_get("cookie-expire","8766");
251 int expires = atoi(zExpire)*3600;
252 const char *zIpAddr = PD("REMOTE_ADDR","nil");
253
254 zCookie = db_text(0, "SELECT '%d/' || hex(randomblob(25))", uid);
255 cgi_set_cookie(zCookieName, zCookie, 0, expires);
 
 
256 record_login_attempt(zUsername, zIpAddr, 1);
257 db_multi_exec(
258 "UPDATE user SET cookie=%Q, ipaddr=%Q, "
259 " cexpire=julianday('now')+%d/86400.0 WHERE uid=%d",
260 zCookie, ipPrefix(zIpAddr), expires, uid
261 );
262 redirect_to_g();
263 }
264 }
265 style_header("Login/Logout");
@@ -366,34 +452,118 @@
366 @ </form>
367 }
368 style_footer();
369 }
370
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
371
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
372
373 /*
374 ** This routine examines the login cookie to see if it exists and
375 ** and is valid. If the login cookie checks out, it then sets
376 ** g.zUserUuid appropriately.
 
 
377 **
378 */
379 void login_check_credentials(void){
380 int uid = 0; /* User id */
381 const char *zCookie; /* Text of the login cookie */
382 const char *zRemoteAddr; /* IP address of the requestor */
383 const char *zCap = 0; /* Capability string */
384
385 /* Only run this check once. */
386 if( g.userUid!=0 ) return;
387
388
389 /* If the HTTP connection is coming over 127.0.0.1 and if
390 ** local login is disabled and if we are using HTTP and not HTTPS,
391 ** then there is no need to check user credentials.
392 **
 
 
393 */
394 zRemoteAddr = PD("REMOTE_ADDR","nil");
395 if( strcmp(zRemoteAddr, "127.0.0.1")==0
396 && g.useLocalauth
397 && db_get_int("localauth",0)==0
398 && P("HTTPS")==0
399 ){
@@ -405,48 +575,60 @@
405 }
406
407 /* Check the login cookie to see if it matches a known valid user.
408 */
409 if( uid==0 && (zCookie = P(login_cookie_name()))!=0 ){
410 if( fossil_isdigit(zCookie[0]) ){
411 /* Cookies of the form "uid/randomness". There must be a
412 ** corresponding entry in the user table. */
413 uid = db_int(0,
414 "SELECT uid FROM user"
415 " WHERE uid=%d"
416 " AND cookie=%Q"
417 " AND ipaddr=%Q"
418 " AND cexpire>julianday('now')",
419 atoi(zCookie), zCookie, ipPrefix(zRemoteAddr)
420 );
421 }else if( memcmp(zCookie,"anon/",5)==0 ){
422 /* Cookies of the form "anon/TIME/HASH". The TIME must not be
423 ** too old and the sha1 hash of TIME+IPADDR+SECRET must match HASH.
 
 
 
 
 
 
 
424 ** SECRET is the "captcha-secret" value in the repository.
425 */
426 double rTime;
427 int i;
428 Blob b;
429 rTime = atof(&zCookie[5]);
430 for(i=5; zCookie[i] && zCookie[i]!='/'; i++){}
431 blob_init(&b, &zCookie[5], i-5);
432 if( zCookie[i]=='/' ){ i++; }
433 blob_append(&b, "/", 1);
434 blob_appendf(&b, "%z/%s", ipPrefix(zRemoteAddr),
435 db_get("captcha-secret",""));
436 sha1sum_blob(&b, &b);
437 uid = db_int(0,
438 "SELECT uid FROM user WHERE login='anonymous'"
439 " AND length(cap)>0"
440 " AND length(pw)>0"
441 " AND %f+0.25>julianday('now')"
442 " AND %Q=%Q",
443 rTime, &zCookie[i], blob_buffer(&b)
444 );
 
445 blob_reset(&b);
 
 
 
 
 
 
 
 
 
446 }
447 sqlite3_snprintf(sizeof(g.zCsrfToken), g.zCsrfToken, "%.10s", zCookie);
448 }
449
450 /* If no user found and the REMOTE_USER environment variable is set,
451 ** the accept the value of REMOTE_USER as the user.
452 */
@@ -795,11 +977,11 @@
795 * this %s(zUsername), or at least I don't know how to force it to.*/
796 @ <p><span class="loginError">
797 @ %s(zUsername) already exists.
798 @ </span></p>
799 }else{
800 char *zPw = sha1_shared_secret(blob_str(&passwd), blob_str(&login));
801 int uid;
802 char *zCookie;
803 const char *zCookieName;
804 const char *zExpire;
805 int expires;
@@ -817,17 +999,17 @@
817 zExpire = db_get("cookie-expire","8766");
818 expires = atoi(zExpire)*3600;
819 zIpAddr = PD("REMOTE_ADDR","nil");
820
821 zCookie = db_text(0, "SELECT '%d/' || hex(randomblob(25))", uid);
822 cgi_set_cookie(zCookieName, zCookie, 0, expires);
823 record_login_attempt(zUsername, zIpAddr, 1);
824 db_multi_exec(
825 "UPDATE user SET cookie=%Q, ipaddr=%Q, "
826 " cexpire=julianday('now')+%d/86400.0 WHERE uid=%d",
827 zCookie, ipPrefix(zIpAddr), expires, uid
828 );
829 redirect_to_g();
830
831 }
832 }
833 }
@@ -873,5 +1055,239 @@
873 @ </form>
874 style_footer();
875
876 free(zCaptcha);
877 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
878
--- src/login.c
+++ src/login.c
@@ -17,16 +17,20 @@
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 **
@@ -46,19 +50,53 @@
50 # endif
51 #endif
52 #include <time.h>
53
54 /*
55 ** Return the login-group name. Or return 0 if this repository is
56 ** not a member of a login-group.
57 */
58 const char *login_group_name(void){
59 static const char *zGroup = 0;
60 static int once = 1;
61 if( once ){
62 zGroup = db_get("login-group-name", 0);
63 once = 0;
64 }
65 return zGroup;
66 }
67
68 /*
69 ** Return a path appropriate for setting a cookie.
70 **
71 ** The path is g.zTop for single-repo cookies. It is "/" for
72 ** cookies of a login-group.
73 */
74 static const char *login_cookie_path(void){
75 if( login_group_name()==0 ){
76 return g.zTop;
77 }else{
78 return "/";
79 }
80 }
81
82 /*
83 ** Return the name of the login cookie.
84 **
85 ** The login cookie name is always of the form: fossil-XXXXXXXXXXXXXXXX
86 ** where the Xs are the first 16 characters of the login-group-code or
87 ** of the project-code if we are not a member of any login-group.
88 */
89 static char *login_cookie_name(void){
90 static char *zCookieName = 0;
91 if( zCookieName==0 ){
92 zCookieName = db_text(0,
93 "SELECT 'fossil-' || substr(value,1,16)"
94 " FROM config"
95 " WHERE name IN ('project-code','login-group-code')"
96 " ORDER BY name;"
97 );
98 }
99 return zCookieName;
100 }
101
102 /*
@@ -73,15 +111,14 @@
111 fossil_redirect_home();
112 }
113 }
114
115 /*
116 ** The IP address of the client is stored as part of login cookies.
117 ** But some clients are behind firewalls that shift the IP address
118 ** with each HTTP request. To allow such (broken) clients to log in,
119 ** extract just a prefix of the IP address.
 
120 */
121 static char *ipPrefix(const char *zIP){
122 int i, j;
123 for(i=j=0; zIP[i]; i++){
124 if( zIP[i]=='.' ){
@@ -89,11 +126,21 @@
126 if( j==2 ) break;
127 }
128 }
129 return mprintf("%.*s", i, zIP);
130 }
131
132 /*
133 ** Return an abbreviated project code. The abbreviation is the first
134 ** 16 characters of the project code.
135 **
136 ** Memory is obtained from malloc.
137 */
138 static char *abbreviated_project_code(const char *zFullCode){
139 return mprintf("%.16s", zFullCode);
140 }
141
142
143 /*
144 ** Check to see if the anonymous login is valid. If it is valid, return
145 ** the userid of the anonymous user.
146 */
@@ -167,22 +214,25 @@
214 int anonFlag;
215 char *zErrMsg = "";
216 int uid; /* User id loged in user */
217 char *zSha1Pw;
218 const char *zIpAddr; /* IP address of requestor */
219 char *zRemoteAddr; /* Abbreviated IP address of requestor */
220
221 login_check_credentials();
222 zUsername = P("u");
223 zPasswd = P("p");
224 anonFlag = P("anon")!=0;
225 if( P("out")!=0 ){
226 /* To logout, change the cookie value to an empty string */
227 const char *zCookieName = login_cookie_name();
228 cgi_set_cookie(zCookieName, "", login_cookie_path(), -86400);
229 redirect_to_g();
230 }
231 if( g.okPassword && zPasswd && (zNew1 = P("n1"))!=0 && (zNew2 = P("n2"))!=0 ){
232 /* The user requests a password change */
233 zSha1Pw = sha1_shared_secret(zPasswd, g.zLogin, 0);
234 if( db_int(1, "SELECT 0 FROM user"
235 " WHERE uid=%d AND (pw=%Q OR pw=%Q)",
236 g.userUid, zPasswd, zSha1Pw) ){
237 sleep(1);
238 zErrMsg =
@@ -197,43 +247,70 @@
247 @ The two copies of your new passwords do not match.
248 @ Your password is unchanged.
249 @ </span></p>
250 ;
251 }else{
252 char *zNewPw = sha1_shared_secret(zNew1, g.zLogin, 0);
253 char *zChngPw;
254 char *zErr;
255 db_multi_exec(
256 "UPDATE user SET pw=%Q WHERE uid=%d", zNewPw, g.userUid
257 );
258 fossil_free(zNewPw);
259 zChngPw = mprintf(
260 "UPDATE user"
261 " SET pw=shared_secret(%Q,%Q,"
262 " (SELECT value FROM config WHERE name='project-code'))"
263 " WHERE login=%Q",
264 zNew1, g.zLogin, g.zLogin
265 );
266 if( login_group_sql(zChngPw, "<p>", "</p>\n", &zErr) ){
267 zErrMsg = mprintf("<span class=\"loginError\">%s</span>", zErr);
268 fossil_free(zErr);
269 }else{
270 redirect_to_g();
271 return;
272 }
273 }
274 }
275 zIpAddr = PD("REMOTE_ADDR","nil"); /* Complete IP address for logging */
276 zRemoteAddr = ipPrefix(zIpAddr); /* Abbreviated IP address */
277 uid = isValidAnonymousLogin(zUsername, zPasswd);
278 if( uid>0 ){
279 /* Successful login as anonymous. Set a cookie that looks like
280 ** this:
281 **
282 ** HASH/TIME/anonymous
283 **
284 ** Where HASH is the sha1sum of TIME/IPADDR/SECRET, in which IPADDR
285 ** is the abbreviated IP address and SECRET is captcha-secret.
286 */
287 char *zNow; /* Current time (julian day number) */
288 char *zCookie; /* The login cookie */
289 const char *zCookieName; /* Name of the login cookie */
290 Blob b; /* Blob used during cookie construction */
291
292 zCookieName = login_cookie_name();
293 zNow = db_text("0", "SELECT julianday('now')");
294 blob_init(&b, zNow, -1);
295 blob_appendf(&b, "/%s/%s", zRemoteAddr, db_get("captcha-secret",""));
296 sha1sum_blob(&b, &b);
297 zCookie = sqlite3_mprintf("%s/%s/anonymous", blob_buffer(&b), zNow);
298 blob_reset(&b);
299 free(zNow);
300 cgi_set_cookie(zCookieName, zCookie, login_cookie_path(), 6*3600);
301 record_login_attempt("anonymous", zIpAddr, 1);
302 redirect_to_g();
303 }
304 if( zUsername!=0 && zPasswd!=0 && zPasswd[0]!=0 ){
305 /* Attempting to log in as a user other than anonymous.
306 */
307 zSha1Pw = sha1_shared_secret(zPasswd, zUsername, 0);
308 uid = db_int(0,
309 "SELECT uid FROM user"
310 " WHERE login=%Q"
311 " AND length(cap)>0 AND length(pw)>0"
312 " AND login NOT IN ('anonymous','nobody','developer','reader')"
313 " AND (pw=%Q OR pw=%Q)",
314 zUsername, zPasswd, zSha1Pw
315 );
316 if( uid<=0 ){
@@ -243,23 +320,32 @@
320 @ You entered an unknown user or an incorrect password.
321 @ </span></p>
322 ;
323 record_login_attempt(zUsername, zIpAddr, 0);
324 }else{
325 /* Non-anonymous login is successful. Set a cookie of the form:
326 **
327 ** HASH/PROJECT/LOGIN
328 **
329 ** where HASH is a random hex number, PROJECT is either project
330 ** code prefix, and LOGIN is the user name.
331 */
332 char *zCookie;
333 const char *zCookieName = login_cookie_name();
334 const char *zExpire = db_get("cookie-expire","8766");
335 int expires = atoi(zExpire)*3600;
336 char *zCode = abbreviated_project_code(db_get("project-code",""));
337 char *zHash;
338
339 zHash = db_text(0, "SELECT hex(randomblob(25))");
340 zCookie = mprintf("%s/%s/%s", zHash, zCode, zUsername);
341 cgi_set_cookie(zCookieName, zCookie, login_cookie_path(), expires);
342 record_login_attempt(zUsername, zIpAddr, 1);
343 db_multi_exec(
344 "UPDATE user SET cookie=%Q, ipaddr=%Q, "
345 " cexpire=julianday('now')+%d/86400.0 WHERE uid=%d",
346 zHash, zRemoteAddr, expires, uid
347 );
348 redirect_to_g();
349 }
350 }
351 style_header("Login/Logout");
@@ -366,34 +452,118 @@
452 @ </form>
453 }
454 style_footer();
455 }
456
457 /*
458 ** Attempt to find login credentials for user zLogin on a peer repository
459 ** with project code zCode. Transfer those credentials to the local
460 ** repository.
461 **
462 ** Return true if a transfer was made and false if not.
463 */
464 static int login_transfer_credentials(
465 const char *zLogin, /* Login we are looking for */
466 const char *zCode, /* Project code of peer repository */
467 const char *zHash, /* HASH from login cookie HASH/CODE/LOGIN */
468 const char *zRemoteAddr /* Request comes from here */
469 ){
470 sqlite3 *pOther = 0; /* The other repository */
471 sqlite3_stmt *pStmt; /* Query against the other repository */
472 char *zSQL; /* SQL of the query against other repo */
473 char *zOtherRepo; /* Filename of the other repository */
474 int rc; /* Result code from SQLite library functions */
475 int nXfer = 0; /* Number of credentials transferred */
476
477 zOtherRepo = db_text(0,
478 "SELECT value FROM config WHERE name='peer-repo-%q'",
479 zCode
480 );
481 if( zOtherRepo==0 ) return 0; /* No such peer repository */
482
483 rc = sqlite3_open(zOtherRepo, &pOther);
484 if( rc==SQLITE_OK ){
485 zSQL = mprintf(
486 "SELECT cexpire FROM user"
487 " WHERE cookie=%Q"
488 " AND ipaddr=%Q"
489 " AND login=%Q"
490 " AND length(cap)>0"
491 " AND length(pw)>0"
492 " AND cexpire>julianday('now')",
493 zHash, zRemoteAddr, zLogin
494 );
495 pStmt = 0;
496 rc = sqlite3_prepare_v2(pOther, zSQL, -1, &pStmt, 0);
497 if( rc==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){
498 db_multi_exec(
499 "UPDATE user SET cookie=%Q, ipaddr=%Q, cexpire=%.17g"
500 " WHERE login=%Q",
501 zHash, zRemoteAddr,
502 sqlite3_column_double(pStmt, 0), zLogin
503 );
504 nXfer++;
505 }
506 sqlite3_finalize(pStmt);
507 }
508 sqlite3_close(pOther);
509 fossil_free(zOtherRepo);
510 return nXfer;
511 }
512
513 /*
514 ** Lookup the uid for a user with zLogin and zCookie and zRemoteAddr.
515 ** Return 0 if not found.
516 */
517 static int login_find_user(
518 const char *zLogin, /* User name */
519 const char *zCookie, /* Login cookie value */
520 const char *zRemoteAddr /* Abbreviated IP address for valid login */
521 ){
522 int uid;
523 if( fossil_strcmp(zLogin, "anonymous")==0 ) return 0;
524 if( fossil_strcmp(zLogin, "nobody")==0 ) return 0;
525 if( fossil_strcmp(zLogin, "developer")==0 ) return 0;
526 if( fossil_strcmp(zLogin, "reader")==0 ) return 0;
527 uid = db_int(0,
528 "SELECT uid FROM user"
529 " WHERE login=%Q"
530 " AND cookie=%Q"
531 " AND ipaddr=%Q"
532 " AND cexpire>julianday('now')"
533 " AND length(cap)>0"
534 " AND length(pw)>0",
535 zLogin, zCookie, zRemoteAddr
536 );
537 return uid;
538 }
539
540 /*
541 ** This routine examines the login cookie to see if it exists and
542 ** and is valid. If the login cookie checks out, it then sets
543 ** global variables appropriately. Global variables set include
544 ** g.userUid and g.zLogin and of the g.okRead family of permission
545 ** booleans.
546 **
547 */
548 void login_check_credentials(void){
549 int uid = 0; /* User id */
550 const char *zCookie; /* Text of the login cookie */
551 char *zRemoteAddr; /* IP address of the requestor */
552 const char *zCap = 0; /* Capability string */
553
554 /* Only run this check once. */
555 if( g.userUid!=0 ) return;
556
 
557 /* If the HTTP connection is coming over 127.0.0.1 and if
558 ** local login is disabled and if we are using HTTP and not HTTPS,
559 ** then there is no need to check user credentials.
560 **
561 ** This feature allows the "fossil ui" command to give the user
562 ** full access rights without having to log in.
563 */
564 zRemoteAddr = ipPrefix(PD("REMOTE_ADDR","nil"));
565 if( strcmp(zRemoteAddr, "127.0.0.1")==0
566 && g.useLocalauth
567 && db_get_int("localauth",0)==0
568 && P("HTTPS")==0
569 ){
@@ -405,48 +575,60 @@
575 }
576
577 /* Check the login cookie to see if it matches a known valid user.
578 */
579 if( uid==0 && (zCookie = P(login_cookie_name()))!=0 ){
580 /* Parse the cookie value up into HASH/ARG/USER */
581 char *zHash = fossil_strdup(zCookie);
582 char *zArg = 0;
583 char *zUser = 0;
584 int i, c;
585 for(i=0; (c = zHash[i])!=0; i++){
586 if( c=='/' ){
587 zHash[i++] = 0;
588 if( zArg==0 ){
589 zArg = &zHash[i];
590 }else{
591 zUser = &zHash[i];
592 break;
593 }
594 }
595 }
596 if( zUser==0 ){
597 /* Invalid cookie */
598 }else if( strcmp(zUser, "anonymous")==0 ){
599 /* Cookies of the form "HASH/TIME/anonymous". The TIME must not be
600 ** too old and the sha1 hash of TIME/IPADDR/SECRET must match HASH.
601 ** SECRET is the "captcha-secret" value in the repository.
602 */
603 double rTime = atof(zArg);
 
604 Blob b;
605 blob_zero(&b);
606 blob_appendf(&b, "%s/%s/%s",
607 zArg, zRemoteAddr, db_get("captcha-secret",""));
 
 
 
 
608 sha1sum_blob(&b, &b);
609 if( fossil_strcmp(zHash, blob_str(&b))==0 ){
610 uid = db_int(0,
611 "SELECT uid FROM user WHERE login='anonymous'"
612 " AND length(cap)>0"
613 " AND length(pw)>0"
614 " AND %.17g+0.25>julianday('now')",
615 rTime
616 );
617 }
618 blob_reset(&b);
619 }else{
620 /* Cookies of the form "HASH/CODE/USER". Search first in the
621 ** local user table, then the user table for project CODE if we
622 ** are part of a login-group.
623 */
624 uid = login_find_user(zUser, zHash, zRemoteAddr);
625 if( uid==0 && login_transfer_credentials(zUser,zArg,zHash,zRemoteAddr) ){
626 uid = login_find_user(zUser, zHash, zRemoteAddr);
627 }
628 }
629 sqlite3_snprintf(sizeof(g.zCsrfToken), g.zCsrfToken, "%.10s", zHash);
630 }
631
632 /* If no user found and the REMOTE_USER environment variable is set,
633 ** the accept the value of REMOTE_USER as the user.
634 */
@@ -795,11 +977,11 @@
977 * this %s(zUsername), or at least I don't know how to force it to.*/
978 @ <p><span class="loginError">
979 @ %s(zUsername) already exists.
980 @ </span></p>
981 }else{
982 char *zPw = sha1_shared_secret(blob_str(&passwd), blob_str(&login), 0);
983 int uid;
984 char *zCookie;
985 const char *zCookieName;
986 const char *zExpire;
987 int expires;
@@ -817,17 +999,17 @@
999 zExpire = db_get("cookie-expire","8766");
1000 expires = atoi(zExpire)*3600;
1001 zIpAddr = PD("REMOTE_ADDR","nil");
1002
1003 zCookie = db_text(0, "SELECT '%d/' || hex(randomblob(25))", uid);
1004 cgi_set_cookie(zCookieName, zCookie, login_cookie_path(), expires);
1005 record_login_attempt(zUsername, zIpAddr, 1);
1006 db_multi_exec(
1007 "UPDATE user SET cookie=%Q, ipaddr=%Q, "
1008 " cexpire=julianday('now')+%d/86400.0 WHERE uid=%d",
1009 zCookie, ipPrefix(zIpAddr), expires, uid
1010 );
1011 redirect_to_g();
1012
1013 }
1014 }
1015 }
@@ -873,5 +1055,239 @@
1055 @ </form>
1056 style_footer();
1057
1058 free(zCaptcha);
1059 }
1060
1061 /*
1062 ** Run SQL on the repository database for every repository in our
1063 ** login group. The SQL is run in a separate database connection.
1064 **
1065 ** Any members of the login group whose repository database file
1066 ** cannot be found is silently removed from the group.
1067 **
1068 ** Error messages accumulate and are returned in *pzErrorMsg. The
1069 ** memory used to hold these messages should be freed using
1070 ** fossil_free() if one desired to avoid a memory leak. The
1071 ** zPrefix and zSuffix strings surround each error message.
1072 **
1073 ** Return the number of errors.
1074 */
1075 int login_group_sql(
1076 const char *zSql, /* The SQL to run */
1077 const char *zPrefix, /* Prefix to each error message */
1078 const char *zSuffix, /* Suffix to each error message */
1079 char **pzErrorMsg /* Write error message here, if not NULL */
1080 ){
1081 sqlite3 *pPeer; /* Connection to another database */
1082 int nErr = 0; /* Number of errors seen so far */
1083 int rc; /* Result code from subroutine calls */
1084 char *zErr; /* SQLite error text */
1085 char *zSelfCode; /* Project code for ourself */
1086 Blob err; /* Accumulate errors here */
1087 Stmt q; /* Query of all peer-* entries in CONFIG */
1088
1089 if( zPrefix==0 ) zPrefix = "";
1090 if( zSuffix==0 ) zSuffix = "";
1091 if( pzErrorMsg ) *pzErrorMsg = 0;
1092 zSelfCode = abbreviated_project_code(db_get("project-code", "x"));
1093 blob_zero(&err);
1094 db_prepare(&q,
1095 "SELECT name, value FROM config"
1096 " WHERE name GLOB 'peer-repo-*'"
1097 " AND name <> 'peer-repo-%q'"
1098 " ORDER BY +value",
1099 zSelfCode
1100 );
1101 while( db_step(&q)==SQLITE_ROW ){
1102 const char *zRepoName = db_column_text(&q, 1);
1103 if( file_size(zRepoName)<0 ){
1104 /* Silently remove non-existant repositories from the login group. */
1105 const char *zLabel = db_column_text(&q, 0);
1106 db_multi_exec(
1107 "DELETE FROM config WHERE name GLOB 'peer-*-%q'",
1108 &zLabel[10]
1109 );
1110 continue;
1111 }
1112 rc = sqlite3_open_v2(zRepoName, &pPeer, SQLITE_OPEN_READWRITE, 0);
1113 if( rc!=SQLITE_OK ){
1114 blob_appendf(&err, "%s%s: %s%s", zPrefix, zRepoName,
1115 sqlite3_errmsg(pPeer), zSuffix);
1116 nErr++;
1117 sqlite3_close(pPeer);
1118 continue;
1119 }
1120 sqlite3_create_function(pPeer, "shared_secret", 3, SQLITE_UTF8,
1121 0, sha1_shared_secret_sql_function, 0, 0);
1122 zErr = 0;
1123 rc = sqlite3_exec(pPeer, zSql, 0, 0, &zErr);
1124 if( zErr ){
1125 blob_appendf(&err, "%s%s: %s%s", zPrefix, zRepoName, zErr, zSuffix);
1126 sqlite3_free(zErr);
1127 nErr++;
1128 }else if( rc!=SQLITE_OK ){
1129 blob_appendf(&err, "%s%s: %s%s", zPrefix, zRepoName,
1130 sqlite3_errmsg(pPeer), zSuffix);
1131 nErr++;
1132 }
1133 sqlite3_close(pPeer);
1134 }
1135 db_finalize(&q);
1136 if( pzErrorMsg && blob_size(&err)>0 ){
1137 *pzErrorMsg = fossil_strdup(blob_str(&err));
1138 }
1139 blob_reset(&err);
1140 fossil_free(zSelfCode);
1141 return nErr;
1142 }
1143
1144 /*
1145 ** Attempt to join a login-group.
1146 **
1147 ** If problems arise, leave an error message in *pzErrMsg.
1148 */
1149 void login_group_join(
1150 const char *zRepo, /* Repository file in the login group */
1151 const char *zLogin, /* Login name for the other repo */
1152 const char *zPassword, /* Password to prove we are authorized to join */
1153 const char *zNewName, /* Name of new login group if making a new one */
1154 char **pzErrMsg /* Leave an error message here */
1155 ){
1156 Blob fullName; /* Blob for finding full pathnames */
1157 sqlite3 *pOther; /* The other repository */
1158 int rc; /* Return code from sqlite3 functions */
1159 char *zOtherProjCode; /* Project code for pOther */
1160 char *zPwHash; /* Password hash on pOther */
1161 char *zSelfRepo; /* Name of our repository */
1162 char *zSelfLabel; /* Project-name for our repository */
1163 char *zSelfProjCode; /* Our project-code */
1164 char *zSql; /* SQL to run on all peers */
1165 const char *zSelf; /* The ATTACH name of our repository */
1166
1167 *pzErrMsg = 0; /* Default to no errors */
1168 zSelf = db_name("repository");
1169
1170 /* Get the full pathname of the other repository */
1171 file_canonical_name(zRepo, &fullName);
1172 zRepo = mprintf(blob_str(&fullName));
1173 blob_reset(&fullName);
1174
1175 /* Get the full pathname for our repository. Also the project code
1176 ** and project name for ourself. */
1177 file_canonical_name(g.zRepositoryName, &fullName);
1178 zSelfRepo = mprintf(blob_str(&fullName));
1179 blob_reset(&fullName);
1180 zSelfProjCode = db_get("project-code", "unknown");
1181 zSelfLabel = db_get("project-name", 0);
1182 if( zSelfLabel==0 ){
1183 zSelfLabel = zSelfProjCode;
1184 }
1185
1186 /* Make sure we are not trying to join ourselves */
1187 if( strcmp(zRepo, zSelfRepo)==0 ){
1188 *pzErrMsg = mprintf("The \"other\" repository is the same as this one.");
1189 return;
1190 }
1191
1192 /* Make sure the other repository is a valid Fossil database */
1193 if( file_size(zRepo)<0 ){
1194 *pzErrMsg = mprintf("repository file \"%s\" does not exist", zRepo);
1195 return;
1196 }
1197 rc = sqlite3_open(zRepo, &pOther);
1198 if( rc!=SQLITE_OK ){
1199 *pzErrMsg = mprintf(sqlite3_errmsg(pOther));
1200 }else{
1201 rc = sqlite3_exec(pOther, "SELECT count(*) FROM user", 0, 0, pzErrMsg);
1202 }
1203 sqlite3_close(pOther);
1204 if( rc ) return;
1205
1206 /* Attach the other respository. Make sure the username/password is
1207 ** valid and has Setup permission.
1208 */
1209 db_multi_exec("ATTACH %Q AS other", zRepo);
1210 zOtherProjCode = db_text("x", "SELECT value FROM other.config"
1211 " WHERE name='project-code'");
1212 zPwHash = sha1_shared_secret(zPassword, zLogin, zOtherProjCode);
1213 if( !db_exists(
1214 "SELECT 1 FROM other.user"
1215 " WHERE login=%Q AND cap GLOB '*s*'"
1216 " AND (pw=%Q OR pw=%Q)",
1217 zLogin, zPassword, zPwHash)
1218 ){
1219 db_multi_exec("DETACH other");
1220 *pzErrMsg = "The supplied username/password does not correspond to a"
1221 " user Setup permission on the other repository.";
1222 return;
1223 }
1224
1225 /* Create all the necessary CONFIG table entries on both the
1226 ** other repository and on our own repository.
1227 */
1228 zSelfProjCode = abbreviated_project_code(zSelfProjCode);
1229 zOtherProjCode = abbreviated_project_code(zOtherProjCode);
1230 db_begin_transaction();
1231 db_multi_exec(
1232 "DELETE FROM %s.config WHERE name GLOB 'peer-*';"
1233 "INSERT INTO %s.config(name,value) VALUES('peer-repo-%s',%Q);"
1234 "INSERT INTO %s.config(name,value) "
1235 " SELECT 'peer-name-%q', value FROM other.config"
1236 " WHERE name='project-name';",
1237 zSelf,
1238 zSelf, zOtherProjCode, zRepo,
1239 zSelf, zOtherProjCode
1240 );
1241 db_multi_exec(
1242 "INSERT OR IGNORE INTO other.config(name,value)"
1243 " VALUES('login-group-name',%Q);"
1244 "INSERT OR IGNORE INTO other.config(name,value)"
1245 " VALUES('login-group-code',lower(hex(randomblob(8))));",
1246 zNewName
1247 );
1248 db_multi_exec(
1249 "REPLACE INTO %s.config(name,value)"
1250 " SELECT name, value FROM other.config"
1251 " WHERE name GLOB 'peer-*' OR name GLOB 'login-group-*'",
1252 zSelf
1253 );
1254 db_end_transaction(0);
1255 db_multi_exec("DETACH other");
1256
1257 /* Propagate the changes to all other members of the login-group */
1258 zSql = mprintf(
1259 "BEGIN;"
1260 "REPLACE INTO config(name, value) VALUES('peer-name-%q', %Q);"
1261 "REPLACE INTO config(name, value) VALUES('peer-repo-%q', %Q);"
1262 "COMMIT;",
1263 zSelfProjCode, zSelfLabel, zSelfProjCode, zSelfRepo
1264 );
1265 login_group_sql(zSql, "<li> ", "</li>", pzErrMsg);
1266 fossil_free(zSql);
1267 }
1268
1269 /*
1270 ** Leave the login group that we are currently part of.
1271 */
1272 void login_group_leave(char **pzErrMsg){
1273 char *zProjCode;
1274 char *zSql;
1275
1276 *pzErrMsg = 0;
1277 zProjCode = abbreviated_project_code(db_get("project-code","x"));
1278 zSql = mprintf(
1279 "DELETE FROM config WHERE name GLOB 'peer-*-%q';"
1280 "DELETE FROM config"
1281 " WHERE name='login-group-name'"
1282 " AND (SELECT count(*) FROM config WHERE name GLOB 'peer-*')==0;",
1283 zProjCode
1284 );
1285 fossil_free(zProjCode);
1286 login_group_sql(zSql, "<li> ", "</li>", pzErrMsg);
1287 fossil_free(zSql);
1288 db_multi_exec(
1289 "DELETE FROM config "
1290 " WHERE name GLOB 'peer-*'"
1291 " OR name GLOB 'login-group-*';"
1292 );
1293 }
1294
+150 -4
--- src/setup.c
+++ src/setup.c
@@ -44,11 +44,11 @@
4444
if( zLink && zLink[0] ){
4545
@ <a href="%s(zLink)">%h(zTitle)</a>
4646
}else{
4747
@ %h(zTitle)
4848
}
49
- @ </td><td valign="top">%h(zDesc)</td></tr>
49
+ @ </td><td width="5"></td><td valign="top">%h(zDesc)</td></tr>
5050
}
5151
5252
/*
5353
** WEBPAGE: /setup
5454
*/
@@ -57,11 +57,11 @@
5757
if( !g.okSetup ){
5858
login_needed();
5959
}
6060
6161
style_header("Server Administration");
62
- @ <table border="0" cellspacing="20">
62
+ @ <table border="0" cellspacing="7">
6363
setup_menu_entry("Users", "setup_ulist",
6464
"Grant privileges to individual users.");
6565
setup_menu_entry("Access", "setup_access",
6666
"Control access settings.");
6767
setup_menu_entry("Configuration", "setup_config",
@@ -68,10 +68,13 @@
6868
"Configure the WWW components of the repository");
6969
setup_menu_entry("Settings", "setup_settings",
7070
"Web interface to the \"fossil settings\" command");
7171
setup_menu_entry("Timeline", "setup_timeline",
7272
"Timeline display preferences");
73
+ setup_menu_entry("Login-Group", "setup_login_group",
74
+ "Manage single sign-on between this repository and others"
75
+ " on the same server");
7376
setup_menu_entry("Tickets", "tktsetup",
7477
"Configure the trouble-ticketing system for this repository");
7578
setup_menu_entry("Skins", "setup_skin",
7679
"Select from a menu of prepackaged \"skins\" for the web interface");
7780
setup_menu_entry("CSS", "setup_editcss",
@@ -245,10 +248,12 @@
245248
void user_edit(void){
246249
const char *zId, *zLogin, *zInfo, *zCap, *zPw;
247250
char *oaa, *oas, *oar, *oaw, *oan, *oai, *oaj, *oao, *oap;
248251
char *oak, *oad, *oac, *oaf, *oam, *oah, *oag, *oae;
249252
char *oat, *oau, *oav, *oab, *oax, *oaz;
253
+ const char *zGroup;
254
+ const char *zOldLogin;
250255
const char *inherit[128];
251256
int doWrite;
252257
int uid;
253258
int higherUser = 0; /* True if user being edited is SETUP and the */
254259
/* user doing the editing is ADMIN. Disallow editing */
@@ -331,14 +336,15 @@
331336
332337
zCap[i] = 0;
333338
zPw = P("pw");
334339
zLogin = P("login");
335340
if( isValidPwString(zPw) ){
336
- zPw = sha1_shared_secret(zPw, zLogin);
341
+ zPw = sha1_shared_secret(zPw, zLogin, 0);
337342
}else{
338343
zPw = db_text(0, "SELECT pw FROM user WHERE uid=%d", uid);
339344
}
345
+ zOldLogin = db_text(0, "SELECT login FROM user WHERE uid=%d", uid);
340346
if( uid>0 &&
341347
db_exists("SELECT 1 FROM user WHERE login=%Q AND uid!=%d", zLogin, uid)
342348
){
343349
style_header("User Creation Error");
344350
@ <span class="loginError">Login "%h(zLogin)" is already used by
@@ -352,10 +358,43 @@
352358
db_multi_exec(
353359
"REPLACE INTO user(uid,login,info,pw,cap) "
354360
"VALUES(nullif(%d,0),%Q,%Q,%Q,'%s')",
355361
uid, P("login"), P("info"), zPw, zCap
356362
);
363
+ if( atoi(PD("all","0"))>0 ){
364
+ Blob sql;
365
+ char *zErr = 0;
366
+ blob_zero(&sql);
367
+ if( zOldLogin==0 ){
368
+ blob_appendf(&sql,
369
+ "INSERT INTO user(login)"
370
+ " SELECT %Q WHERE NOT EXISTS(SELECT 1 FROM user WHERE login=%Q);",
371
+ zLogin, zLogin
372
+ );
373
+ zOldLogin = zLogin;
374
+ }
375
+ blob_appendf(&sql,
376
+ "UPDATE user SET login=%Q,"
377
+ " pw=coalesce(shared_secret(%Q,%Q,"
378
+ "(SELECT value FROM config WHERE name='project-code')),pw),"
379
+ " info=%Q,"
380
+ " cap=%Q"
381
+ " WHERE login=%Q;",
382
+ zLogin, P("pw"), zLogin, P("info"), zCap,
383
+ zOldLogin
384
+ );
385
+ login_group_sql(blob_str(&sql), "<li> ", " </li>\n", &zErr);
386
+ blob_reset(&sql);
387
+ if( zErr ){
388
+ style_header("User Change Error");
389
+ @ <span class="loginError">%s(zErr)</span>
390
+ @
391
+ @ <p><a href="setup_uedit?id=%d(uid)">[Bummer]</a></p>
392
+ style_footer();
393
+ return;
394
+ }
395
+ }
357396
cgi_redirect("setup_ulist");
358397
return;
359398
}
360399
361400
/* Load the existing information about the user, if any
@@ -502,10 +541,21 @@
502541
}else{
503542
/* Show an empty password as an empty input field */
504543
@ <td><input type="password" name="pw" value="" /></td>
505544
}
506545
@ </tr>
546
+ zGroup = login_group_name();
547
+ if( zGroup ){
548
+ @ <tr>
549
+ @ <td valign="top" align="right">Scope:</td>
550
+ @ <td valign="top">
551
+ @ <input type="radio" name="all" checked value="0">
552
+ @ Apply changes to this repository only.<br />
553
+ @ <input type="radio" name="all" value="1">
554
+ @ Apply changes to all repositories in the "<b>%h(zGroup)</b>"
555
+ @ login group.</td></tr>
556
+ }
507557
if( !higherUser ){
508558
@ <tr>
509559
@ <td>&nbsp;</td>
510560
@ <td><input type="submit" name="submit" value="Apply Changes" /></td>
511561
@ </tr>
@@ -823,11 +873,12 @@
823873
@ can register under any user name. This option is useful for public projects
824874
@ where you do not want everyone in any ticket discussion to be named
825875
@ "Anonymous".</p>
826876
827877
@ <hr />
828
- entry_attribute("Default privileges", 10, "default-perms", "defaultperms", "u");
878
+ entry_attribute("Default privileges", 10, "default-perms",
879
+ "defaultperms", "u");
829880
@ <p>Permissions given to users that register themselves using the HTTP UI
830881
@ or are registered by the administrator using the command line interface.
831882
@ </p>
832883
833884
@ <hr />
@@ -843,10 +894,105 @@
843894
@ <p><input type="submit" name="submit" value="Apply Changes" /></p>
844895
@ </div></form>
845896
db_end_transaction(0);
846897
style_footer();
847898
}
899
+
900
+/*
901
+** WEBPAGE: setup_login_group
902
+*/
903
+void setup_login_group(void){
904
+ const char *zGroup;
905
+ char *zErrMsg = 0;
906
+ Blob fullName;
907
+ char *zSelfRepo;
908
+ const char *zRepo = PD("repo", "");
909
+ const char *zLogin = PD("login", "");
910
+ const char *zPw = PD("pw", "");
911
+ const char *zNewName = PD("newname", "New Login Group");
912
+
913
+ login_check_credentials();
914
+ if( !g.okSetup ){
915
+ login_needed();
916
+ }
917
+ file_canonical_name(g.zRepositoryName, &fullName);
918
+ zSelfRepo = mprintf(blob_str(&fullName));
919
+ blob_reset(&fullName);
920
+ if( P("join")!=0 ){
921
+ login_group_join(zRepo, zLogin, zPw, zNewName, &zErrMsg);
922
+ }else if( P("leave") ){
923
+ login_group_leave(&zErrMsg);
924
+ }
925
+ style_header("Login Group Configuration");
926
+ if( zErrMsg ){
927
+ @ <p class="generalError">%s(zErrMsg)</p>
928
+ }
929
+ zGroup = login_group_name();
930
+ if( zGroup==0 ){
931
+ @ <p>This repository (in the file named "%h(zSelfRepo)")
932
+ @ is not currently part of any login-group.
933
+ @ To join a login group, fill out the form below.</p>
934
+ @
935
+ @ <form action="%s(g.zTop)/setup_login_group" method="post"><div>
936
+ login_insert_csrf_secret();
937
+ @ <blockquote><table broder="0">
938
+ @
939
+ @ <tr><td align="right"><b>Repository filename in group to join:</b></td>
940
+ @ <td width="5"></td><td>
941
+ @ <input type="text" size="50" value="%h(zRepo)" name="repo"></td></tr>
942
+ @
943
+ @ <td align="right"><b>Login on the above repo:</b></td>
944
+ @ <td width="5"></td><td>
945
+ @ <input type="text" size="20" value="%h(zLogin)" name="login"></td></tr>
946
+ @
947
+ @ <td align="right"><b>Password:</b></td>
948
+ @ <td width="5"></td><td>
949
+ @ <input type="password" size="20" name="pw"></td></tr>
950
+ @
951
+ @ <tr><td align="right"><b>Name of login-group:</b></td>
952
+ @ <td width="5"></td><td>
953
+ @ <input type="text" size="30" value="%h(zNewName)" name="newname">
954
+ @ (only used if creating a new login-group).</td></tr>
955
+ @
956
+ @ <tr><td colspan="3" align="center">
957
+ @ <input type="submit" value="Join" name="join"></td></tr>
958
+ @ </table>
959
+ }else{
960
+ Stmt q;
961
+ int n = 0;
962
+ @ <p>This repository (in the file "%h(zSelfRepo)")
963
+ @ is currently part of the "<b>%h(zGroup)</b>" login group.
964
+ @ Other repositories in that group are:</p>
965
+ @ <table border="0" cellspacing="4">
966
+ @ <tr><td colspan="2"><th align="left">Project Name<td>
967
+ @ <th align="left">Repository File</tr>
968
+ db_prepare(&q,
969
+ "SELECT value,"
970
+ " (SELECT value FROM config"
971
+ " WHERE name=('peer-name-' || substr(x.name,11)))"
972
+ " FROM config AS x"
973
+ " WHERE name GLOB 'peer-repo-*'"
974
+ " ORDER BY value"
975
+ );
976
+ while( db_step(&q)==SQLITE_ROW ){
977
+ const char *zRepo = db_column_text(&q, 0);
978
+ const char *zTitle = db_column_text(&q, 1);
979
+ n++;
980
+ @ <tr><td align="right">%d(n).</td><td width="4">
981
+ @ <td>%h(zTitle)<td width="10"><td>%h(zRepo)</tr>
982
+ }
983
+ db_finalize(&q);
984
+ @ </table>
985
+ @
986
+ @ <p><form action="%s(g.zTop)/setup_login_group" method="post"><div>
987
+ login_insert_csrf_secret();
988
+ @ To leave this login group press
989
+ @ <input type="submit" value="Leave Login Group" name="leave">
990
+ @ </form></p>
991
+ }
992
+ style_footer();
993
+}
848994
849995
/*
850996
** WEBPAGE: setup_timeline
851997
*/
852998
void setup_timeline(void){
853999
--- src/setup.c
+++ src/setup.c
@@ -44,11 +44,11 @@
44 if( zLink && zLink[0] ){
45 @ <a href="%s(zLink)">%h(zTitle)</a>
46 }else{
47 @ %h(zTitle)
48 }
49 @ </td><td valign="top">%h(zDesc)</td></tr>
50 }
51
52 /*
53 ** WEBPAGE: /setup
54 */
@@ -57,11 +57,11 @@
57 if( !g.okSetup ){
58 login_needed();
59 }
60
61 style_header("Server Administration");
62 @ <table border="0" cellspacing="20">
63 setup_menu_entry("Users", "setup_ulist",
64 "Grant privileges to individual users.");
65 setup_menu_entry("Access", "setup_access",
66 "Control access settings.");
67 setup_menu_entry("Configuration", "setup_config",
@@ -68,10 +68,13 @@
68 "Configure the WWW components of the repository");
69 setup_menu_entry("Settings", "setup_settings",
70 "Web interface to the \"fossil settings\" command");
71 setup_menu_entry("Timeline", "setup_timeline",
72 "Timeline display preferences");
 
 
 
73 setup_menu_entry("Tickets", "tktsetup",
74 "Configure the trouble-ticketing system for this repository");
75 setup_menu_entry("Skins", "setup_skin",
76 "Select from a menu of prepackaged \"skins\" for the web interface");
77 setup_menu_entry("CSS", "setup_editcss",
@@ -245,10 +248,12 @@
245 void user_edit(void){
246 const char *zId, *zLogin, *zInfo, *zCap, *zPw;
247 char *oaa, *oas, *oar, *oaw, *oan, *oai, *oaj, *oao, *oap;
248 char *oak, *oad, *oac, *oaf, *oam, *oah, *oag, *oae;
249 char *oat, *oau, *oav, *oab, *oax, *oaz;
 
 
250 const char *inherit[128];
251 int doWrite;
252 int uid;
253 int higherUser = 0; /* True if user being edited is SETUP and the */
254 /* user doing the editing is ADMIN. Disallow editing */
@@ -331,14 +336,15 @@
331
332 zCap[i] = 0;
333 zPw = P("pw");
334 zLogin = P("login");
335 if( isValidPwString(zPw) ){
336 zPw = sha1_shared_secret(zPw, zLogin);
337 }else{
338 zPw = db_text(0, "SELECT pw FROM user WHERE uid=%d", uid);
339 }
 
340 if( uid>0 &&
341 db_exists("SELECT 1 FROM user WHERE login=%Q AND uid!=%d", zLogin, uid)
342 ){
343 style_header("User Creation Error");
344 @ <span class="loginError">Login "%h(zLogin)" is already used by
@@ -352,10 +358,43 @@
352 db_multi_exec(
353 "REPLACE INTO user(uid,login,info,pw,cap) "
354 "VALUES(nullif(%d,0),%Q,%Q,%Q,'%s')",
355 uid, P("login"), P("info"), zPw, zCap
356 );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
357 cgi_redirect("setup_ulist");
358 return;
359 }
360
361 /* Load the existing information about the user, if any
@@ -502,10 +541,21 @@
502 }else{
503 /* Show an empty password as an empty input field */
504 @ <td><input type="password" name="pw" value="" /></td>
505 }
506 @ </tr>
 
 
 
 
 
 
 
 
 
 
 
507 if( !higherUser ){
508 @ <tr>
509 @ <td>&nbsp;</td>
510 @ <td><input type="submit" name="submit" value="Apply Changes" /></td>
511 @ </tr>
@@ -823,11 +873,12 @@
823 @ can register under any user name. This option is useful for public projects
824 @ where you do not want everyone in any ticket discussion to be named
825 @ "Anonymous".</p>
826
827 @ <hr />
828 entry_attribute("Default privileges", 10, "default-perms", "defaultperms", "u");
 
829 @ <p>Permissions given to users that register themselves using the HTTP UI
830 @ or are registered by the administrator using the command line interface.
831 @ </p>
832
833 @ <hr />
@@ -843,10 +894,105 @@
843 @ <p><input type="submit" name="submit" value="Apply Changes" /></p>
844 @ </div></form>
845 db_end_transaction(0);
846 style_footer();
847 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
848
849 /*
850 ** WEBPAGE: setup_timeline
851 */
852 void setup_timeline(void){
853
--- src/setup.c
+++ src/setup.c
@@ -44,11 +44,11 @@
44 if( zLink && zLink[0] ){
45 @ <a href="%s(zLink)">%h(zTitle)</a>
46 }else{
47 @ %h(zTitle)
48 }
49 @ </td><td width="5"></td><td valign="top">%h(zDesc)</td></tr>
50 }
51
52 /*
53 ** WEBPAGE: /setup
54 */
@@ -57,11 +57,11 @@
57 if( !g.okSetup ){
58 login_needed();
59 }
60
61 style_header("Server Administration");
62 @ <table border="0" cellspacing="7">
63 setup_menu_entry("Users", "setup_ulist",
64 "Grant privileges to individual users.");
65 setup_menu_entry("Access", "setup_access",
66 "Control access settings.");
67 setup_menu_entry("Configuration", "setup_config",
@@ -68,10 +68,13 @@
68 "Configure the WWW components of the repository");
69 setup_menu_entry("Settings", "setup_settings",
70 "Web interface to the \"fossil settings\" command");
71 setup_menu_entry("Timeline", "setup_timeline",
72 "Timeline display preferences");
73 setup_menu_entry("Login-Group", "setup_login_group",
74 "Manage single sign-on between this repository and others"
75 " on the same server");
76 setup_menu_entry("Tickets", "tktsetup",
77 "Configure the trouble-ticketing system for this repository");
78 setup_menu_entry("Skins", "setup_skin",
79 "Select from a menu of prepackaged \"skins\" for the web interface");
80 setup_menu_entry("CSS", "setup_editcss",
@@ -245,10 +248,12 @@
248 void user_edit(void){
249 const char *zId, *zLogin, *zInfo, *zCap, *zPw;
250 char *oaa, *oas, *oar, *oaw, *oan, *oai, *oaj, *oao, *oap;
251 char *oak, *oad, *oac, *oaf, *oam, *oah, *oag, *oae;
252 char *oat, *oau, *oav, *oab, *oax, *oaz;
253 const char *zGroup;
254 const char *zOldLogin;
255 const char *inherit[128];
256 int doWrite;
257 int uid;
258 int higherUser = 0; /* True if user being edited is SETUP and the */
259 /* user doing the editing is ADMIN. Disallow editing */
@@ -331,14 +336,15 @@
336
337 zCap[i] = 0;
338 zPw = P("pw");
339 zLogin = P("login");
340 if( isValidPwString(zPw) ){
341 zPw = sha1_shared_secret(zPw, zLogin, 0);
342 }else{
343 zPw = db_text(0, "SELECT pw FROM user WHERE uid=%d", uid);
344 }
345 zOldLogin = db_text(0, "SELECT login FROM user WHERE uid=%d", uid);
346 if( uid>0 &&
347 db_exists("SELECT 1 FROM user WHERE login=%Q AND uid!=%d", zLogin, uid)
348 ){
349 style_header("User Creation Error");
350 @ <span class="loginError">Login "%h(zLogin)" is already used by
@@ -352,10 +358,43 @@
358 db_multi_exec(
359 "REPLACE INTO user(uid,login,info,pw,cap) "
360 "VALUES(nullif(%d,0),%Q,%Q,%Q,'%s')",
361 uid, P("login"), P("info"), zPw, zCap
362 );
363 if( atoi(PD("all","0"))>0 ){
364 Blob sql;
365 char *zErr = 0;
366 blob_zero(&sql);
367 if( zOldLogin==0 ){
368 blob_appendf(&sql,
369 "INSERT INTO user(login)"
370 " SELECT %Q WHERE NOT EXISTS(SELECT 1 FROM user WHERE login=%Q);",
371 zLogin, zLogin
372 );
373 zOldLogin = zLogin;
374 }
375 blob_appendf(&sql,
376 "UPDATE user SET login=%Q,"
377 " pw=coalesce(shared_secret(%Q,%Q,"
378 "(SELECT value FROM config WHERE name='project-code')),pw),"
379 " info=%Q,"
380 " cap=%Q"
381 " WHERE login=%Q;",
382 zLogin, P("pw"), zLogin, P("info"), zCap,
383 zOldLogin
384 );
385 login_group_sql(blob_str(&sql), "<li> ", " </li>\n", &zErr);
386 blob_reset(&sql);
387 if( zErr ){
388 style_header("User Change Error");
389 @ <span class="loginError">%s(zErr)</span>
390 @
391 @ <p><a href="setup_uedit?id=%d(uid)">[Bummer]</a></p>
392 style_footer();
393 return;
394 }
395 }
396 cgi_redirect("setup_ulist");
397 return;
398 }
399
400 /* Load the existing information about the user, if any
@@ -502,10 +541,21 @@
541 }else{
542 /* Show an empty password as an empty input field */
543 @ <td><input type="password" name="pw" value="" /></td>
544 }
545 @ </tr>
546 zGroup = login_group_name();
547 if( zGroup ){
548 @ <tr>
549 @ <td valign="top" align="right">Scope:</td>
550 @ <td valign="top">
551 @ <input type="radio" name="all" checked value="0">
552 @ Apply changes to this repository only.<br />
553 @ <input type="radio" name="all" value="1">
554 @ Apply changes to all repositories in the "<b>%h(zGroup)</b>"
555 @ login group.</td></tr>
556 }
557 if( !higherUser ){
558 @ <tr>
559 @ <td>&nbsp;</td>
560 @ <td><input type="submit" name="submit" value="Apply Changes" /></td>
561 @ </tr>
@@ -823,11 +873,12 @@
873 @ can register under any user name. This option is useful for public projects
874 @ where you do not want everyone in any ticket discussion to be named
875 @ "Anonymous".</p>
876
877 @ <hr />
878 entry_attribute("Default privileges", 10, "default-perms",
879 "defaultperms", "u");
880 @ <p>Permissions given to users that register themselves using the HTTP UI
881 @ or are registered by the administrator using the command line interface.
882 @ </p>
883
884 @ <hr />
@@ -843,10 +894,105 @@
894 @ <p><input type="submit" name="submit" value="Apply Changes" /></p>
895 @ </div></form>
896 db_end_transaction(0);
897 style_footer();
898 }
899
900 /*
901 ** WEBPAGE: setup_login_group
902 */
903 void setup_login_group(void){
904 const char *zGroup;
905 char *zErrMsg = 0;
906 Blob fullName;
907 char *zSelfRepo;
908 const char *zRepo = PD("repo", "");
909 const char *zLogin = PD("login", "");
910 const char *zPw = PD("pw", "");
911 const char *zNewName = PD("newname", "New Login Group");
912
913 login_check_credentials();
914 if( !g.okSetup ){
915 login_needed();
916 }
917 file_canonical_name(g.zRepositoryName, &fullName);
918 zSelfRepo = mprintf(blob_str(&fullName));
919 blob_reset(&fullName);
920 if( P("join")!=0 ){
921 login_group_join(zRepo, zLogin, zPw, zNewName, &zErrMsg);
922 }else if( P("leave") ){
923 login_group_leave(&zErrMsg);
924 }
925 style_header("Login Group Configuration");
926 if( zErrMsg ){
927 @ <p class="generalError">%s(zErrMsg)</p>
928 }
929 zGroup = login_group_name();
930 if( zGroup==0 ){
931 @ <p>This repository (in the file named "%h(zSelfRepo)")
932 @ is not currently part of any login-group.
933 @ To join a login group, fill out the form below.</p>
934 @
935 @ <form action="%s(g.zTop)/setup_login_group" method="post"><div>
936 login_insert_csrf_secret();
937 @ <blockquote><table broder="0">
938 @
939 @ <tr><td align="right"><b>Repository filename in group to join:</b></td>
940 @ <td width="5"></td><td>
941 @ <input type="text" size="50" value="%h(zRepo)" name="repo"></td></tr>
942 @
943 @ <td align="right"><b>Login on the above repo:</b></td>
944 @ <td width="5"></td><td>
945 @ <input type="text" size="20" value="%h(zLogin)" name="login"></td></tr>
946 @
947 @ <td align="right"><b>Password:</b></td>
948 @ <td width="5"></td><td>
949 @ <input type="password" size="20" name="pw"></td></tr>
950 @
951 @ <tr><td align="right"><b>Name of login-group:</b></td>
952 @ <td width="5"></td><td>
953 @ <input type="text" size="30" value="%h(zNewName)" name="newname">
954 @ (only used if creating a new login-group).</td></tr>
955 @
956 @ <tr><td colspan="3" align="center">
957 @ <input type="submit" value="Join" name="join"></td></tr>
958 @ </table>
959 }else{
960 Stmt q;
961 int n = 0;
962 @ <p>This repository (in the file "%h(zSelfRepo)")
963 @ is currently part of the "<b>%h(zGroup)</b>" login group.
964 @ Other repositories in that group are:</p>
965 @ <table border="0" cellspacing="4">
966 @ <tr><td colspan="2"><th align="left">Project Name<td>
967 @ <th align="left">Repository File</tr>
968 db_prepare(&q,
969 "SELECT value,"
970 " (SELECT value FROM config"
971 " WHERE name=('peer-name-' || substr(x.name,11)))"
972 " FROM config AS x"
973 " WHERE name GLOB 'peer-repo-*'"
974 " ORDER BY value"
975 );
976 while( db_step(&q)==SQLITE_ROW ){
977 const char *zRepo = db_column_text(&q, 0);
978 const char *zTitle = db_column_text(&q, 1);
979 n++;
980 @ <tr><td align="right">%d(n).</td><td width="4">
981 @ <td>%h(zTitle)<td width="10"><td>%h(zRepo)</tr>
982 }
983 db_finalize(&q);
984 @ </table>
985 @
986 @ <p><form action="%s(g.zTop)/setup_login_group" method="post"><div>
987 login_insert_csrf_secret();
988 @ To leave this login group press
989 @ <input type="submit" value="Leave Login Group" name="leave">
990 @ </form></p>
991 }
992 style_footer();
993 }
994
995 /*
996 ** WEBPAGE: setup_timeline
997 */
998 void setup_timeline(void){
999
+53 -12
--- src/sha1.c
+++ src/sha1.c
@@ -342,36 +342,77 @@
342342
** value stored in the USER.PW field of the database. By mixing in the
343343
** login name and the project id with the hash, different shared secrets
344344
** are obtained even if two users select the same password, or if a
345345
** single user selects the same password for multiple projects.
346346
*/
347
-char *sha1_shared_secret(const char *zPw, const char *zLogin){
347
+char *sha1_shared_secret(
348
+ const char *zPw, /* The password to encrypt */
349
+ const char *zLogin, /* Username */
350
+ const char *zProjCode /* Project-code. Use built-in project code if NULL */
351
+){
348352
static char *zProjectId = 0;
349353
SHA1Context ctx;
350354
unsigned char zResult[20];
351355
char zDigest[41];
352356
353357
SHA1Init(&ctx);
354
- if( zProjectId==0 ){
355
- zProjectId = db_get("project-code", 0);
356
-
357
- /* On the first xfer request of a clone, the project-code is not yet
358
- ** known. Use the cleartext password, since that is all we have.
359
- */
360
- if( zProjectId==0 ){
361
- return mprintf("%s", zPw);
362
- }
363
- }
364
- SHA1Update(&ctx, (unsigned char*)zProjectId, strlen(zProjectId));
358
+ if( zProjCode==0 ){
359
+ if( zProjectId==0 ){
360
+ zProjectId = db_get("project-code", 0);
361
+
362
+ /* On the first xfer request of a clone, the project-code is not yet
363
+ ** known. Use the cleartext password, since that is all we have.
364
+ */
365
+ if( zProjectId==0 ){
366
+ return mprintf("%s", zPw);
367
+ }
368
+ }
369
+ zProjCode = zProjectId;
370
+ }
371
+ SHA1Update(&ctx, (unsigned char*)zProjCode, strlen(zProjCode));
365372
SHA1Update(&ctx, (unsigned char*)"/", 1);
366373
SHA1Update(&ctx, (unsigned char*)zLogin, strlen(zLogin));
367374
SHA1Update(&ctx, (unsigned char*)"/", 1);
368375
SHA1Update(&ctx, (unsigned const char*)zPw, strlen(zPw));
369376
SHA1Final(&ctx, zResult);
370377
DigestToBase16(zResult, zDigest);
371378
return mprintf("%s", zDigest);
372379
}
380
+
381
+/*
382
+** Implement the shared_secret() SQL function. shared_secret() takes two or
383
+** three arguments; the third argument is optional.
384
+**
385
+** (1) The cleartext password
386
+** (2) The login name
387
+** (3) The project code
388
+**
389
+** Returns sha1($password/$login/$projcode).
390
+*/
391
+void sha1_shared_secret_sql_function(
392
+ sqlite3_context *context,
393
+ int argc,
394
+ sqlite3_value **argv
395
+){
396
+ const char *zPw;
397
+ const char *zLogin;
398
+ const char *zProjid;
399
+
400
+ assert( argc==2 || argc==3 );
401
+ zPw = (const char*)sqlite3_value_text(argv[0]);
402
+ if( zPw==0 || zPw[0]==0 ) return;
403
+ zLogin = (const char*)sqlite3_value_text(argv[1]);
404
+ if( zLogin==0 ) return;
405
+ if( argc==3 ){
406
+ zProjid = (const char*)sqlite3_value_text(argv[2]);
407
+ if( zProjid && zProjid[0]==0 ) zProjid = 0;
408
+ }else{
409
+ zProjid = 0;
410
+ }
411
+ sqlite3_result_text(context, sha1_shared_secret(zPw, zLogin, zProjid), -1,
412
+ fossil_free);
413
+}
373414
374415
/*
375416
** COMMAND: sha1sum
376417
** %fossil sha1sum FILE...
377418
**
378419
--- src/sha1.c
+++ src/sha1.c
@@ -342,36 +342,77 @@
342 ** value stored in the USER.PW field of the database. By mixing in the
343 ** login name and the project id with the hash, different shared secrets
344 ** are obtained even if two users select the same password, or if a
345 ** single user selects the same password for multiple projects.
346 */
347 char *sha1_shared_secret(const char *zPw, const char *zLogin){
 
 
 
 
348 static char *zProjectId = 0;
349 SHA1Context ctx;
350 unsigned char zResult[20];
351 char zDigest[41];
352
353 SHA1Init(&ctx);
354 if( zProjectId==0 ){
355 zProjectId = db_get("project-code", 0);
356
357 /* On the first xfer request of a clone, the project-code is not yet
358 ** known. Use the cleartext password, since that is all we have.
359 */
360 if( zProjectId==0 ){
361 return mprintf("%s", zPw);
362 }
363 }
364 SHA1Update(&ctx, (unsigned char*)zProjectId, strlen(zProjectId));
 
 
 
365 SHA1Update(&ctx, (unsigned char*)"/", 1);
366 SHA1Update(&ctx, (unsigned char*)zLogin, strlen(zLogin));
367 SHA1Update(&ctx, (unsigned char*)"/", 1);
368 SHA1Update(&ctx, (unsigned const char*)zPw, strlen(zPw));
369 SHA1Final(&ctx, zResult);
370 DigestToBase16(zResult, zDigest);
371 return mprintf("%s", zDigest);
372 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
373
374 /*
375 ** COMMAND: sha1sum
376 ** %fossil sha1sum FILE...
377 **
378
--- src/sha1.c
+++ src/sha1.c
@@ -342,36 +342,77 @@
342 ** value stored in the USER.PW field of the database. By mixing in the
343 ** login name and the project id with the hash, different shared secrets
344 ** are obtained even if two users select the same password, or if a
345 ** single user selects the same password for multiple projects.
346 */
347 char *sha1_shared_secret(
348 const char *zPw, /* The password to encrypt */
349 const char *zLogin, /* Username */
350 const char *zProjCode /* Project-code. Use built-in project code if NULL */
351 ){
352 static char *zProjectId = 0;
353 SHA1Context ctx;
354 unsigned char zResult[20];
355 char zDigest[41];
356
357 SHA1Init(&ctx);
358 if( zProjCode==0 ){
359 if( zProjectId==0 ){
360 zProjectId = db_get("project-code", 0);
361
362 /* On the first xfer request of a clone, the project-code is not yet
363 ** known. Use the cleartext password, since that is all we have.
364 */
365 if( zProjectId==0 ){
366 return mprintf("%s", zPw);
367 }
368 }
369 zProjCode = zProjectId;
370 }
371 SHA1Update(&ctx, (unsigned char*)zProjCode, strlen(zProjCode));
372 SHA1Update(&ctx, (unsigned char*)"/", 1);
373 SHA1Update(&ctx, (unsigned char*)zLogin, strlen(zLogin));
374 SHA1Update(&ctx, (unsigned char*)"/", 1);
375 SHA1Update(&ctx, (unsigned const char*)zPw, strlen(zPw));
376 SHA1Final(&ctx, zResult);
377 DigestToBase16(zResult, zDigest);
378 return mprintf("%s", zDigest);
379 }
380
381 /*
382 ** Implement the shared_secret() SQL function. shared_secret() takes two or
383 ** three arguments; the third argument is optional.
384 **
385 ** (1) The cleartext password
386 ** (2) The login name
387 ** (3) The project code
388 **
389 ** Returns sha1($password/$login/$projcode).
390 */
391 void sha1_shared_secret_sql_function(
392 sqlite3_context *context,
393 int argc,
394 sqlite3_value **argv
395 ){
396 const char *zPw;
397 const char *zLogin;
398 const char *zProjid;
399
400 assert( argc==2 || argc==3 );
401 zPw = (const char*)sqlite3_value_text(argv[0]);
402 if( zPw==0 || zPw[0]==0 ) return;
403 zLogin = (const char*)sqlite3_value_text(argv[1]);
404 if( zLogin==0 ) return;
405 if( argc==3 ){
406 zProjid = (const char*)sqlite3_value_text(argv[2]);
407 if( zProjid && zProjid[0]==0 ) zProjid = 0;
408 }else{
409 zProjid = 0;
410 }
411 sqlite3_result_text(context, sha1_shared_secret(zPw, zLogin, zProjid), -1,
412 fossil_free);
413 }
414
415 /*
416 ** COMMAND: sha1sum
417 ** %fossil sha1sum FILE...
418 **
419
+5 -22
--- src/user.c
+++ src/user.c
@@ -202,11 +202,11 @@
202202
if( g.argc>=6 ){
203203
blob_init(&passwd, g.argv[5], -1);
204204
}else{
205205
prompt_for_password("password: ", &passwd, 1);
206206
}
207
- zPw = sha1_shared_secret(blob_str(&passwd), blob_str(&login));
207
+ zPw = sha1_shared_secret(blob_str(&passwd), blob_str(&login), 0);
208208
db_multi_exec(
209209
"INSERT INTO user(login,pw,cap,info)"
210210
"VALUES(%B,%Q,%B,%B)",
211211
&login, zPw, &caps, &contact
212212
);
@@ -248,11 +248,11 @@
248248
prompt_for_password(zPrompt, &pw, 1);
249249
}
250250
if( blob_size(&pw)==0 ){
251251
printf("password unchanged\n");
252252
}else{
253
- char *zSecret = sha1_shared_secret(blob_str(&pw), g.argv[3]);
253
+ char *zSecret = sha1_shared_secret(blob_str(&pw), g.argv[3], 0);
254254
db_multi_exec("UPDATE user SET pw=%Q WHERE uid=%d", zSecret, uid);
255255
free(zSecret);
256256
}
257257
}else if( n>=2 && strncmp(g.argv[2],"capabilities",2)==0 ){
258258
int uid;
@@ -348,27 +348,10 @@
348348
g.userUid = db_last_insert_rowid();
349349
g.zLogin = "anonymous";
350350
}
351351
}
352352
353
-/*
354
-** Compute the shared secret for a user.
355
-*/
356
-static void user_sha1_shared_secret_func(
357
- sqlite3_context *context,
358
- int argc,
359
- sqlite3_value **argv
360
-){
361
- char *zPw;
362
- char *zLogin;
363
- assert( argc==2 );
364
- zPw = (char*)sqlite3_value_text(argv[0]);
365
- zLogin = (char*)sqlite3_value_text(argv[1]);
366
- if( zPw && zLogin ){
367
- sqlite3_result_text(context, sha1_shared_secret(zPw, zLogin), -1, free);
368
- }
369
-}
370353
371354
/*
372355
** COMMAND: test-hash-passwords
373356
**
374357
** Usage: %fossil test-hash-passwords REPOSITORY
@@ -378,14 +361,14 @@
378361
** has are unchanged.
379362
*/
380363
void user_hash_passwords_cmd(void){
381364
if( g.argc!=3 ) usage("REPOSITORY");
382365
db_open_repository(g.argv[2]);
383
- sqlite3_create_function(g.db, "sha1_shared_secret", 2, SQLITE_UTF8, 0,
384
- user_sha1_shared_secret_func, 0, 0);
366
+ sqlite3_create_function(g.db, "shared_secret", 2, SQLITE_UTF8, 0,
367
+ sha1_shared_secret_sql_function, 0, 0);
385368
db_multi_exec(
386
- "UPDATE user SET pw=sha1_shared_secret(pw,login)"
369
+ "UPDATE user SET pw=shared_secret(pw,login)"
387370
" WHERE length(pw)>0 AND length(pw)!=40"
388371
);
389372
}
390373
391374
/*
392375
--- src/user.c
+++ src/user.c
@@ -202,11 +202,11 @@
202 if( g.argc>=6 ){
203 blob_init(&passwd, g.argv[5], -1);
204 }else{
205 prompt_for_password("password: ", &passwd, 1);
206 }
207 zPw = sha1_shared_secret(blob_str(&passwd), blob_str(&login));
208 db_multi_exec(
209 "INSERT INTO user(login,pw,cap,info)"
210 "VALUES(%B,%Q,%B,%B)",
211 &login, zPw, &caps, &contact
212 );
@@ -248,11 +248,11 @@
248 prompt_for_password(zPrompt, &pw, 1);
249 }
250 if( blob_size(&pw)==0 ){
251 printf("password unchanged\n");
252 }else{
253 char *zSecret = sha1_shared_secret(blob_str(&pw), g.argv[3]);
254 db_multi_exec("UPDATE user SET pw=%Q WHERE uid=%d", zSecret, uid);
255 free(zSecret);
256 }
257 }else if( n>=2 && strncmp(g.argv[2],"capabilities",2)==0 ){
258 int uid;
@@ -348,27 +348,10 @@
348 g.userUid = db_last_insert_rowid();
349 g.zLogin = "anonymous";
350 }
351 }
352
353 /*
354 ** Compute the shared secret for a user.
355 */
356 static void user_sha1_shared_secret_func(
357 sqlite3_context *context,
358 int argc,
359 sqlite3_value **argv
360 ){
361 char *zPw;
362 char *zLogin;
363 assert( argc==2 );
364 zPw = (char*)sqlite3_value_text(argv[0]);
365 zLogin = (char*)sqlite3_value_text(argv[1]);
366 if( zPw && zLogin ){
367 sqlite3_result_text(context, sha1_shared_secret(zPw, zLogin), -1, free);
368 }
369 }
370
371 /*
372 ** COMMAND: test-hash-passwords
373 **
374 ** Usage: %fossil test-hash-passwords REPOSITORY
@@ -378,14 +361,14 @@
378 ** has are unchanged.
379 */
380 void user_hash_passwords_cmd(void){
381 if( g.argc!=3 ) usage("REPOSITORY");
382 db_open_repository(g.argv[2]);
383 sqlite3_create_function(g.db, "sha1_shared_secret", 2, SQLITE_UTF8, 0,
384 user_sha1_shared_secret_func, 0, 0);
385 db_multi_exec(
386 "UPDATE user SET pw=sha1_shared_secret(pw,login)"
387 " WHERE length(pw)>0 AND length(pw)!=40"
388 );
389 }
390
391 /*
392
--- src/user.c
+++ src/user.c
@@ -202,11 +202,11 @@
202 if( g.argc>=6 ){
203 blob_init(&passwd, g.argv[5], -1);
204 }else{
205 prompt_for_password("password: ", &passwd, 1);
206 }
207 zPw = sha1_shared_secret(blob_str(&passwd), blob_str(&login), 0);
208 db_multi_exec(
209 "INSERT INTO user(login,pw,cap,info)"
210 "VALUES(%B,%Q,%B,%B)",
211 &login, zPw, &caps, &contact
212 );
@@ -248,11 +248,11 @@
248 prompt_for_password(zPrompt, &pw, 1);
249 }
250 if( blob_size(&pw)==0 ){
251 printf("password unchanged\n");
252 }else{
253 char *zSecret = sha1_shared_secret(blob_str(&pw), g.argv[3], 0);
254 db_multi_exec("UPDATE user SET pw=%Q WHERE uid=%d", zSecret, uid);
255 free(zSecret);
256 }
257 }else if( n>=2 && strncmp(g.argv[2],"capabilities",2)==0 ){
258 int uid;
@@ -348,27 +348,10 @@
348 g.userUid = db_last_insert_rowid();
349 g.zLogin = "anonymous";
350 }
351 }
352
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
353
354 /*
355 ** COMMAND: test-hash-passwords
356 **
357 ** Usage: %fossil test-hash-passwords REPOSITORY
@@ -378,14 +361,14 @@
361 ** has are unchanged.
362 */
363 void user_hash_passwords_cmd(void){
364 if( g.argc!=3 ) usage("REPOSITORY");
365 db_open_repository(g.argv[2]);
366 sqlite3_create_function(g.db, "shared_secret", 2, SQLITE_UTF8, 0,
367 sha1_shared_secret_sql_function, 0, 0);
368 db_multi_exec(
369 "UPDATE user SET pw=shared_secret(pw,login)"
370 " WHERE length(pw)>0 AND length(pw)!=40"
371 );
372 }
373
374 /*
375
+1 -1
--- src/xfer.c
+++ src/xfer.c
@@ -579,11 +579,11 @@
579579
/* If this server stores cleartext passwords and the password did not
580580
** match, then perhaps the client is sending SHA1 passwords. Try
581581
** again with the SHA1 password.
582582
*/
583583
const char *zPw = db_column_text(&q, 0);
584
- char *zSecret = sha1_shared_secret(zPw, blob_str(pLogin));
584
+ char *zSecret = sha1_shared_secret(zPw, blob_str(pLogin), 0);
585585
blob_zero(&combined);
586586
blob_copy(&combined, pNonce);
587587
blob_append(&combined, zSecret, -1);
588588
free(zSecret);
589589
sha1sum_blob(&combined, &hash);
590590
--- src/xfer.c
+++ src/xfer.c
@@ -579,11 +579,11 @@
579 /* If this server stores cleartext passwords and the password did not
580 ** match, then perhaps the client is sending SHA1 passwords. Try
581 ** again with the SHA1 password.
582 */
583 const char *zPw = db_column_text(&q, 0);
584 char *zSecret = sha1_shared_secret(zPw, blob_str(pLogin));
585 blob_zero(&combined);
586 blob_copy(&combined, pNonce);
587 blob_append(&combined, zSecret, -1);
588 free(zSecret);
589 sha1sum_blob(&combined, &hash);
590
--- src/xfer.c
+++ src/xfer.c
@@ -579,11 +579,11 @@
579 /* If this server stores cleartext passwords and the password did not
580 ** match, then perhaps the client is sending SHA1 passwords. Try
581 ** again with the SHA1 password.
582 */
583 const char *zPw = db_column_text(&q, 0);
584 char *zSecret = sha1_shared_secret(zPw, blob_str(pLogin), 0);
585 blob_zero(&combined);
586 blob_copy(&combined, pNonce);
587 blob_append(&combined, zSecret, -1);
588 free(zSecret);
589 sha1sum_blob(&combined, &hash);
590

Keyboard Shortcuts

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