.. i'm not sure that i like it enough to justify the idea of
maintaining two files where one file is sufficient.
The current implementation uses one single cookie database shared for
all repositories in the same directory, which can be excluded from
backups, and deleted (or, better, emptied by SQL script) to have
everybody logged off. But it's possible to modify the code to use one
single cookie database per system, or per repository.
.. the login cookie db could become a point of locking contention ...
Would WAL mode prevent this, mostly?
Below are my current "works for me on Windows and FreeBSD" patches. I
hope we still have the same definitions of "surprisingly simple" :)
The 2nd patch is only required with my previous patch to change ETag
generation to produce a "login-time-sensitive" hash.
I'm sorry e-mail processing may insert one or two unwanted line breaks
after column 72, as Fossil seems to use a source code line length
limit of 80 chars.
Some notes are included directly with the patch file headers, but I'd
like to emphasize that I haven't bothered making things work with
login groups, so far.
I'd be happy to do more work towards a more generalized "separate
(shared) database for non-repository contents, such as 'volatile' or
'system-specific' state information" approach, should this be
considered interesting for Fossil.
--Florian
===================== Patch for Fossil [e08f9c04] ======================
Baseline: Fossil [e08f9c0423]
Proof-of-concept to outsource login cookie information to a separate
database named "cookiestore", saved as "fossil-cookiestore.sqlite" in
the directory of the main repository database, and attached on demand.
The "cookiestore" database is left attached until shutdown; it may be
safer to have it detached explicitly as soon as possible.
HTTP cache handlers, and any other code relying on "user.cexpire", must
query "cookiestore.user.cexpire", instead.
Support to share login credentials across login groups is not
implemented by this patch; in fact, this may even break login group
features.
Admins changing their own password through the /setup_uedit page (not
through the /login page) are no longer logged out automatically.
To prevent writes to the main repository database caused by read-only
web server access, the "PRAGMA optimize" call needs to be removed, and
the "access_log" feature needs to be disabled (the logs could be
recorded to a plain text file, or outsourced to a separate database, if
required).
There may be more elegant SQL queries to work with the connected tables,
either by using JOINs, or FOREIGN KEYs (yet the latter have been
disabled by Fossil).
Windows batch file to dump or tweak the "cookiestore" database:
:: @echo off
:: setlocal
:: set c=fossil-cookiestore.sqlite
:: if not exist "%c%" goto:eof
:: (
:: echo ATTACH '%c%' AS 'c';
:: echo -- PRAGMA c.journal_mode;
:: echo -- PRAGMA c.page_size;
:: echo -- PRAGMA c.auto_vacuum;
:: echo SELECT * FROM c.user;
:: echo -- UPDATE c.user SET cexpire=0;
:: ) | fossil sql --no-repository
Index: src/login.c
==================================================================
--- src/login.c
+++ src/login.c
@@ -143,10 +143,53 @@
*/
static char *abbreviated_project_code(const char *zFullCode){
return mprintf("%.16s", zFullCode);
}
+/*
+** Attach the fossil-cookiestore.sqlite db to store login cookies.
+*/
+void attach_cookiestore()
+{
+ static int attached_cookiestore = 0;
+ char *zDBName;
+ Blob bDBFullName;
+ char *zProjCode;
+
+ if (attached_cookiestore) return;
+
+ zDBName = mprintf("%s/../fossil-cookiestore.sqlite",g.zRepositoryName);
+ file_canonical_name(zDBName,&bDBFullName,0);
+ sqlite3_free(zDBName);
+ db_attach(blob_str(&bDBFullName),"cookiestore");
+ blob_reset(&bDBFullName);
+
+ /* Initialize */
+ db_multi_exec(
+ "CREATE TABLE IF NOT EXISTS cookiestore.user( "
+ "repo TEXT, uid INTEGER, login TEXT, "
+ "cookie TEXT, ipaddr TEXT, cexpire DATETIME,"
+ "PRIMARY KEY (repo, uid), "
+ "UNIQUE (repo, uid, login) ON CONFLICT REPLACE );");
+ /* Clear expired cookies */
+ zProjCode = db_get("project-code",NULL);
+ db_multi_exec(
+ "DELETE FROM cookiestore.user WHERE "
+ "repo=%Q AND cexpire<julianday('now');",zProjCode);
+ free(zProjCode);
+ /* Shrink if not inside BEGIN..COMMIT */
+ if (sqlite3_get_autocommit(g.db)!=0)
+ db_multi_exec(
+ "PRAGMA cookiestore.journal_mode=DELETE;"
+ "PRAGMA cookiestore.page_size=512;"
+ "PRAGMA cookiestore.auto_vacuum=FULL;"
+ "VACUUM cookiestore;"
+ "PRAGMA cookiestore.journal_mode=WAL;");
+
+ attached_cookiestore = 1; /* Check with "PRAGMA database_list"? */
+}
+
/*
** Check to see if the anonymous login is valid. If it is valid, return
** the userid of the anonymous user.
**
@@ -266,30 +309,33 @@
int expires = atoi(zExpire)*3600;
char *zHash;
char *zCookie;
const char *zIpAddr = PD("REMOTE_ADDR","nil"); /* IP address of user */
char *zRemoteAddr = ipPrefix(zIpAddr); /* Abbreviated IP address */
+ char *zProjCode = db_get("project-code",NULL);
assert((zUsername && *zUsername) && (uid > 0) && "Invalid user data.");
+ attach_cookiestore();
zHash = db_text(0,
- "SELECT cookie FROM user"
- " WHERE uid=%d"
+ "SELECT cookie FROM cookiestore.user"
+ " WHERE repo=%Q AND uid=%d"
" AND ipaddr=%Q"
" AND cexpire>julianday('now')"
" AND length(cookie)>30",
- uid, zRemoteAddr);
+ zProjCode, uid, zRemoteAddr);
if( zHash==0 ) zHash = db_text(0, "SELECT hex(randomblob(25))");
zCookie = login_gen_user_cookie_value(zUsername, zHash);
cgi_set_cookie(zCookieName, zCookie, login_cookie_path(), expires);
record_login_attempt(zUsername, zIpAddr, 1);
db_multi_exec(
- "UPDATE user SET cookie=%Q, ipaddr=%Q, "
- " cexpire=julianday('now')+%d/86400.0 WHERE uid=%d",
- zHash, zRemoteAddr, expires, uid
- );
+ "INSERT OR REPLACE INTO cookiestore.user ("
+ " repo, uid, cookie, ipaddr, cexpire, login )"
+ " VALUES ( %Q, %d, %Q, %Q, julianday('now')+%d/86400.0, %Q );",
+ zProjCode, uid, zHash, zRemoteAddr, expires, zUsername );
free(zRemoteAddr);
free(zHash);
+ free(zProjCode);
if( zDest ){
*zDest = zCookie;
}else{
free(zCookie);
}
@@ -348,17 +394,19 @@
void login_clear_login_data(){
if(!g.userUid){
return;
}else{
const char *cookie = login_cookie_name();
+ char *zProjCode = db_get("project-code",NULL);
/* To logout, change the cookie value to an empty string */
cgi_set_cookie(cookie, "",
login_cookie_path(), -86400);
- db_multi_exec("UPDATE user SET cookie=NULL, ipaddr=NULL, "
- " cexpire=0 WHERE uid=%d"
- " AND login NOT IN ('anonymous','nobody',"
- " 'developer','reader')", g.userUid);
+ attach_cookiestore();
+ db_multi_exec(
+ "DELETE FROM cookiestore.user WHERE repo=%Q AND uid=%d;",
+ zProjCode, g.userUid);
+ free(zProjCode);
cgi_replace_parameter(cookie, NULL);
cgi_replace_parameter("anon", NULL);
}
}
@@ -841,21 +889,27 @@
const char *zLogin, /* User name */
const char *zCookie, /* Login cookie value */
const char *zRemoteAddr /* Abbreviated IP address for valid login */
){
int uid;
+ char *zProjCode;
if( login_is_special(zLogin) ) return 0;
+ attach_cookiestore();
+ zProjCode = db_get("project-code",NULL);
uid = db_int(0,
- "SELECT uid FROM user"
- " WHERE login=%Q"
+ "SELECT uid FROM cookiestore.user"
+ " WHERE ( repo=%Q AND login=%Q"
" AND ipaddr=%Q"
" AND cexpire>julianday('now')"
+ " AND constant_time_cmp(cookie,%Q)=0 )"
+ " AND EXISTS ( SELECT 1 FROM user"
+ " WHERE login=%Q"
" AND length(cap)>0"
- " AND length(pw)>0"
- " AND constant_time_cmp(cookie,%Q)=0",
- zLogin, zRemoteAddr, zCookie
+ " AND length(pw)>0);",
+ zProjCode, zLogin, zRemoteAddr, zCookie, zLogin
);
+ free(zProjCode);
return uid;
}
/*
** Return true if it is appropriate to redirect login requests to HTTPS.
===================== Patch for Fossil [e08f9c04] ======================
===================== Patch for Fossil [e08f9c04] ======================
Modify the HTTP cache handlers to query "cookiestore.user.cexpire"
instead of "user.cexpire".
Index: src/etag.c
==================================================================
--- src/etag.c
+++ src/etag.c
@@ -87,12 +87,18 @@
sqlite3_snprintf(sizeof(zBuf),zBuf,"mtime: %lld\n", mtime);
md5sum_step_text(zBuf, -1);
/* Include "user.cexpire" for logged-in users in the hash */
if ( (eFlags & ETAG_CEXP)!=0 && g.zLogin ){
- char *zCExp = db_text(0, "SELECT cexpire FROM user WHERE uid=%d",
- g.userUid);
+ char *zProjCode;
+ char *zCExp;
+ attach_cookiestore();
+ zProjCode = db_get("project-code",NULL);
+ zCExp = db_text(0,
+ "SELECT cexpire FROM cookiestore.user WHERE repo=%Q AND uid=%d",
+ zProjCode,g.userUid);
+ free(zProjCode);
if ( zCExp ){
md5sum_step_text("cexp: ", -1);
md5sum_step_text(zCExp, -1);
md5sum_step_text("\n", 1);
fossil_free(zCExp);
===================== Patch for Fossil [e08f9c04] ======================
===================== Patch for Fossil [e08f9c04] ======================
Baseline: Fossil [e08f9c0423]
Omit the "PRAGMA optimize" command prior to closing the repository
database, to prevent writes caused by read-only web server access.
Note: The sqlite_statN tables created by the "ANALYZE" command can be
managed using the --analyze and --deanalyze rebuild options.
Index: src/db.c
==================================================================
--- src/db.c
+++ src/db.c
@@ -1713,13 +1713,15 @@
while( db.pAllStmt ){
db_finalize(db.pAllStmt);
}
db_end_transaction(1);
pStmt = 0;
+#if 0
g.dbIgnoreErrors++; /* Stop "database locked" warnings from PRAGMA
optimize */
sqlite3_exec(g.db, "PRAGMA optimize", 0, 0, 0);
g.dbIgnoreErrors--;
+#endif
db_close_config();
/* If the localdb has a lot of unused free space,
** then VACUUM it as we shut down.
*/
===================== Patch for Fossil [e08f9c04] ======================