| | @@ -17,16 +17,20 @@ |
| 17 | 17 | ** |
| 18 | 18 | ** This file contains code for generating the login and logout screens. |
| 19 | 19 | ** |
| 20 | 20 | ** Notes: |
| 21 | 21 | ** |
| 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 | +** |
| 23 | 25 | ** The capabilities of the nobody user are available to anyone, |
| 24 | 26 | ** regardless of whether or not they are logged in. The capabilities |
| 25 | 27 | ** of anonymous are only available after logging in, but the login |
| 26 | 28 | ** 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. |
| 28 | 32 | ** |
| 29 | 33 | ** The nobody user has capabilities that you want spiders to have. |
| 30 | 34 | ** The anonymous user has capabilities that you want people without |
| 31 | 35 | ** logins to have. |
| 32 | 36 | ** |
| | @@ -46,19 +50,53 @@ |
| 46 | 50 | # endif |
| 47 | 51 | #endif |
| 48 | 52 | #include <time.h> |
| 49 | 53 | |
| 50 | 54 | /* |
| 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. |
| 52 | 88 | */ |
| 53 | 89 | static char *login_cookie_name(void){ |
| 54 | 90 | static char *zCookieName = 0; |
| 55 | 91 | 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 | + ); |
| 60 | 98 | } |
| 61 | 99 | return zCookieName; |
| 62 | 100 | } |
| 63 | 101 | |
| 64 | 102 | /* |
| | @@ -73,15 +111,14 @@ |
| 73 | 111 | fossil_redirect_home(); |
| 74 | 112 | } |
| 75 | 113 | } |
| 76 | 114 | |
| 77 | 115 | /* |
| 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. |
| 83 | 120 | */ |
| 84 | 121 | static char *ipPrefix(const char *zIP){ |
| 85 | 122 | int i, j; |
| 86 | 123 | for(i=j=0; zIP[i]; i++){ |
| 87 | 124 | if( zIP[i]=='.' ){ |
| | @@ -89,11 +126,21 @@ |
| 89 | 126 | if( j==2 ) break; |
| 90 | 127 | } |
| 91 | 128 | } |
| 92 | 129 | return mprintf("%.*s", i, zIP); |
| 93 | 130 | } |
| 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 | + |
| 95 | 142 | |
| 96 | 143 | /* |
| 97 | 144 | ** Check to see if the anonymous login is valid. If it is valid, return |
| 98 | 145 | ** the userid of the anonymous user. |
| 99 | 146 | */ |
| | @@ -167,22 +214,25 @@ |
| 167 | 214 | int anonFlag; |
| 168 | 215 | char *zErrMsg = ""; |
| 169 | 216 | int uid; /* User id loged in user */ |
| 170 | 217 | char *zSha1Pw; |
| 171 | 218 | const char *zIpAddr; /* IP address of requestor */ |
| 219 | + char *zRemoteAddr; /* Abbreviated IP address of requestor */ |
| 172 | 220 | |
| 173 | 221 | login_check_credentials(); |
| 174 | 222 | zUsername = P("u"); |
| 175 | 223 | zPasswd = P("p"); |
| 176 | 224 | anonFlag = P("anon")!=0; |
| 177 | 225 | if( P("out")!=0 ){ |
| 226 | + /* To logout, change the cookie value to an empty string */ |
| 178 | 227 | const char *zCookieName = login_cookie_name(); |
| 179 | | - cgi_set_cookie(zCookieName, "", 0, -86400); |
| 228 | + cgi_set_cookie(zCookieName, "", login_cookie_path(), -86400); |
| 180 | 229 | redirect_to_g(); |
| 181 | 230 | } |
| 182 | 231 | 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); |
| 184 | 234 | if( db_int(1, "SELECT 0 FROM user" |
| 185 | 235 | " WHERE uid=%d AND (pw=%Q OR pw=%Q)", |
| 186 | 236 | g.userUid, zPasswd, zSha1Pw) ){ |
| 187 | 237 | sleep(1); |
| 188 | 238 | zErrMsg = |
| | @@ -197,43 +247,70 @@ |
| 197 | 247 | @ The two copies of your new passwords do not match. |
| 198 | 248 | @ Your password is unchanged. |
| 199 | 249 | @ </span></p> |
| 200 | 250 | ; |
| 201 | 251 | }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; |
| 203 | 255 | db_multi_exec( |
| 204 | 256 | "UPDATE user SET pw=%Q WHERE uid=%d", zNewPw, g.userUid |
| 205 | 257 | ); |
| 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 | + } |
| 208 | 273 | } |
| 209 | 274 | } |
| 210 | | - zIpAddr = PD("REMOTE_ADDR","nil"); |
| 275 | + zIpAddr = PD("REMOTE_ADDR","nil"); /* Complete IP address for logging */ |
| 276 | + zRemoteAddr = ipPrefix(zIpAddr); /* Abbreviated IP address */ |
| 211 | 277 | uid = isValidAnonymousLogin(zUsername, zPasswd); |
| 212 | 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 | + */ |
| 213 | 287 | char *zNow; /* Current time (julian day number) */ |
| 214 | 288 | char *zCookie; /* The login cookie */ |
| 215 | 289 | const char *zCookieName; /* Name of the login cookie */ |
| 216 | 290 | Blob b; /* Blob used during cookie construction */ |
| 217 | 291 | |
| 218 | 292 | zCookieName = login_cookie_name(); |
| 219 | 293 | zNow = db_text("0", "SELECT julianday('now')"); |
| 220 | 294 | 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","")); |
| 222 | 296 | 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); |
| 224 | 298 | blob_reset(&b); |
| 225 | 299 | free(zNow); |
| 226 | | - cgi_set_cookie(zCookieName, zCookie, 0, 6*3600); |
| 300 | + cgi_set_cookie(zCookieName, zCookie, login_cookie_path(), 6*3600); |
| 227 | 301 | record_login_attempt("anonymous", zIpAddr, 1); |
| 228 | 302 | redirect_to_g(); |
| 229 | 303 | } |
| 230 | 304 | 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); |
| 232 | 308 | uid = db_int(0, |
| 233 | 309 | "SELECT uid FROM user" |
| 234 | 310 | " WHERE login=%Q" |
| 311 | + " AND length(cap)>0 AND length(pw)>0" |
| 235 | 312 | " AND login NOT IN ('anonymous','nobody','developer','reader')" |
| 236 | 313 | " AND (pw=%Q OR pw=%Q)", |
| 237 | 314 | zUsername, zPasswd, zSha1Pw |
| 238 | 315 | ); |
| 239 | 316 | if( uid<=0 ){ |
| | @@ -243,23 +320,32 @@ |
| 243 | 320 | @ You entered an unknown user or an incorrect password. |
| 244 | 321 | @ </span></p> |
| 245 | 322 | ; |
| 246 | 323 | record_login_attempt(zUsername, zIpAddr, 0); |
| 247 | 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 | + */ |
| 248 | 332 | char *zCookie; |
| 249 | 333 | const char *zCookieName = login_cookie_name(); |
| 250 | 334 | const char *zExpire = db_get("cookie-expire","8766"); |
| 251 | 335 | 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); |
| 256 | 342 | record_login_attempt(zUsername, zIpAddr, 1); |
| 257 | 343 | db_multi_exec( |
| 258 | 344 | "UPDATE user SET cookie=%Q, ipaddr=%Q, " |
| 259 | 345 | " cexpire=julianday('now')+%d/86400.0 WHERE uid=%d", |
| 260 | | - zCookie, ipPrefix(zIpAddr), expires, uid |
| 346 | + zHash, zRemoteAddr, expires, uid |
| 261 | 347 | ); |
| 262 | 348 | redirect_to_g(); |
| 263 | 349 | } |
| 264 | 350 | } |
| 265 | 351 | style_header("Login/Logout"); |
| | @@ -366,34 +452,118 @@ |
| 366 | 452 | @ </form> |
| 367 | 453 | } |
| 368 | 454 | style_footer(); |
| 369 | 455 | } |
| 370 | 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 | +} |
| 371 | 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 | +} |
| 372 | 539 | |
| 373 | 540 | /* |
| 374 | 541 | ** This routine examines the login cookie to see if it exists and |
| 375 | 542 | ** 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. |
| 377 | 546 | ** |
| 378 | 547 | */ |
| 379 | 548 | void login_check_credentials(void){ |
| 380 | 549 | int uid = 0; /* User id */ |
| 381 | 550 | 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 */ |
| 383 | 552 | const char *zCap = 0; /* Capability string */ |
| 384 | 553 | |
| 385 | 554 | /* Only run this check once. */ |
| 386 | 555 | if( g.userUid!=0 ) return; |
| 387 | 556 | |
| 388 | | - |
| 389 | 557 | /* If the HTTP connection is coming over 127.0.0.1 and if |
| 390 | 558 | ** local login is disabled and if we are using HTTP and not HTTPS, |
| 391 | 559 | ** then there is no need to check user credentials. |
| 392 | 560 | ** |
| 561 | + ** This feature allows the "fossil ui" command to give the user |
| 562 | + ** full access rights without having to log in. |
| 393 | 563 | */ |
| 394 | | - zRemoteAddr = PD("REMOTE_ADDR","nil"); |
| 564 | + zRemoteAddr = ipPrefix(PD("REMOTE_ADDR","nil")); |
| 395 | 565 | if( strcmp(zRemoteAddr, "127.0.0.1")==0 |
| 396 | 566 | && g.useLocalauth |
| 397 | 567 | && db_get_int("localauth",0)==0 |
| 398 | 568 | && P("HTTPS")==0 |
| 399 | 569 | ){ |
| | @@ -405,48 +575,60 @@ |
| 405 | 575 | } |
| 406 | 576 | |
| 407 | 577 | /* Check the login cookie to see if it matches a known valid user. |
| 408 | 578 | */ |
| 409 | 579 | 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. |
| 424 | 601 | ** SECRET is the "captcha-secret" value in the repository. |
| 425 | 602 | */ |
| 426 | | - double rTime; |
| 427 | | - int i; |
| 603 | + double rTime = atof(zArg); |
| 428 | 604 | 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","")); |
| 436 | 608 | 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 | + } |
| 445 | 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 | + } |
| 446 | 628 | } |
| 447 | | - sqlite3_snprintf(sizeof(g.zCsrfToken), g.zCsrfToken, "%.10s", zCookie); |
| 629 | + sqlite3_snprintf(sizeof(g.zCsrfToken), g.zCsrfToken, "%.10s", zHash); |
| 448 | 630 | } |
| 449 | 631 | |
| 450 | 632 | /* If no user found and the REMOTE_USER environment variable is set, |
| 451 | 633 | ** the accept the value of REMOTE_USER as the user. |
| 452 | 634 | */ |
| | @@ -795,11 +977,11 @@ |
| 795 | 977 | * this %s(zUsername), or at least I don't know how to force it to.*/ |
| 796 | 978 | @ <p><span class="loginError"> |
| 797 | 979 | @ %s(zUsername) already exists. |
| 798 | 980 | @ </span></p> |
| 799 | 981 | }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); |
| 801 | 983 | int uid; |
| 802 | 984 | char *zCookie; |
| 803 | 985 | const char *zCookieName; |
| 804 | 986 | const char *zExpire; |
| 805 | 987 | int expires; |
| | @@ -817,17 +999,17 @@ |
| 817 | 999 | zExpire = db_get("cookie-expire","8766"); |
| 818 | 1000 | expires = atoi(zExpire)*3600; |
| 819 | 1001 | zIpAddr = PD("REMOTE_ADDR","nil"); |
| 820 | 1002 | |
| 821 | 1003 | 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); |
| 823 | 1005 | record_login_attempt(zUsername, zIpAddr, 1); |
| 824 | 1006 | db_multi_exec( |
| 825 | 1007 | "UPDATE user SET cookie=%Q, ipaddr=%Q, " |
| 826 | 1008 | " cexpire=julianday('now')+%d/86400.0 WHERE uid=%d", |
| 827 | 1009 | zCookie, ipPrefix(zIpAddr), expires, uid |
| 828 | | - ); |
| 1010 | + ); |
| 829 | 1011 | redirect_to_g(); |
| 830 | 1012 | |
| 831 | 1013 | } |
| 832 | 1014 | } |
| 833 | 1015 | } |
| | @@ -873,5 +1055,239 @@ |
| 873 | 1055 | @ </form> |
| 874 | 1056 | style_footer(); |
| 875 | 1057 | |
| 876 | 1058 | free(zCaptcha); |
| 877 | 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 | +} |
| 878 | 1294 | |