d48e7626ab0b366e8ce43fee17366fe7d6ba6e67 angie Thu Jun 9 11:36:06 2016 -0700 Revert "Several revisions to login cookie-checking after helpful code review by Max:" This reverts commit 185dbcc2ba84d6eb1301163b926ebed3177cd379. diff --git src/hg/lib/wikiLink.c src/hg/lib/wikiLink.c index 6a99406..d136db0 100644 --- src/hg/lib/wikiLink.c +++ src/hg/lib/wikiLink.c @@ -1,512 +1,416 @@ /* wikiLink - interoperate with a wiki site (share user identities). */ /* Copyright (C) 2014 The Regents of the University of California * See README in this or parent directory for licensing information. */ #include "common.h" #include "hash.h" #include "htmshell.h" #include "cheapcgi.h" #include "hgConfig.h" #include "hui.h" +#include "cart.h" #include "web.h" #include "wikiLink.h" -// Flag to indicate that loginValidateCookies has been called: +// Flag to indicate that loginSystemValidateCookies has been called: static boolean alreadyAuthenticated = FALSE; -// If centralDb has table gbMemberToken, then loginValidateCookies will set this +// If centralDb has table gbMemberToken, then loginSystemValidateCookies will set this // to a random token that validates the user; otherwise if the cookie has the same // value as gbMembers.idx, this is set to that ID; otherwise it stays 0 and the user // is not logged in. static uint authToken = 0; -// If we need to change some cookies, store cookie strings here in case loginValidateCookies -// is called multiple times (e.g. validate before cookie-writing, then later write cookies) -static struct slName *cookieStrings = NULL; // If a random token in gbMemberToken is more than this many seconds old, make a new // random token and delete the old: #define TOKEN_LIFESPAN 300 char *loginSystemName() /* Return the wiki host specified in hg.conf, or NULL. Allocd here. */ { return cloneString(cfgOption(CFG_LOGIN_SYSTEM_NAME)); } boolean loginSystemEnabled() /* Return TRUE if login.systemName parameter is defined in hg.conf . */ { #ifdef USE_SSL return (cfgOption(CFG_LOGIN_SYSTEM_NAME) != NULL); #else return FALSE; #endif } -boolean wikiLinkEnabled() -/* Return TRUE if all wiki.* parameters are defined in hg.conf . */ -{ -return ((cfgOption(CFG_WIKI_HOST) != NULL) && - (cfgOption(CFG_WIKI_USER_NAME_COOKIE) != NULL) && - (cfgOption(CFG_WIKI_LOGGED_IN_COOKIE) != NULL)); -} - static char *wikiLinkLoggedInCookie() /* Return the cookie name specified in hg.conf as the wiki logged-in cookie. */ { return cfgOption(CFG_WIKI_LOGGED_IN_COOKIE); } static char *wikiLinkUserNameCookie() /* Return the cookie name specified in hg.conf as the wiki user name cookie. */ { return cfgOption(CFG_WIKI_USER_NAME_COOKIE); } -static char *loginTokenCookie() -/* Return the name of the login system random token cookie. Do not free result. */ -{ -static char defaultCookie[512]; -char *cookie = cfgOption(CFG_LOGIN_TOKEN_COOKIE); -if (isEmpty(cookie)) - { - char *centralDbPrefix = cfgOptionDefault(CFG_CENTRAL_COOKIE, "central"); - safef(defaultCookie, sizeof(defaultCookie), "%s.hgLoginToken", centralDbPrefix); - cookie = defaultCookie; - } -return cookie; -} - -static char *loginUserNameCookie() -/* Return the name of the login system user name cookie. Do not free result. */ -{ -static char defaultCookie[512]; -char *cookie = cfgOption(CFG_LOGIN_USER_NAME_COOKIE); -if (isEmpty(cookie)) - { - char *centralDbPrefix = cfgOptionDefault(CFG_CENTRAL_COOKIE, "central"); - safef(defaultCookie, sizeof(defaultCookie), "%s.hgLoginUserName", centralDbPrefix); - cookie = defaultCookie; - } -return cookie; -} - -static uint getCookieToken( - boolean *retReplaceOld // TODO: remove in July 2016 - ) +static uint getCookieToken() /* If the cookie holding the login token exists, return its uint value, else 0. */ { -char *cookieTokenStr = findCookieData(loginTokenCookie()); -if (isEmpty(cookieTokenStr) && wikiLinkEnabled()) // TODO: remove in July 2016 - { // TODO: remove in July 2016 - cookieTokenStr = findCookieData(wikiLinkLoggedInCookie()); // TODO: remove in July 2016 - if (retReplaceOld && isNotEmpty(cookieTokenStr)) // TODO: remove in July 2016 - *retReplaceOld = TRUE; // TODO: remove in July 2016 - } // TODO: remove in July 2016 +char *cookieTokenStr = findCookieData(wikiLinkLoggedInCookie()); return cookieTokenStr ? (uint)atoll(cookieTokenStr) : 0; } static uint getMemberIdx(struct sqlConnection *conn, char *userName) /* Return userName's idx value in gbMembers. Return 0 if not found. */ { char query[512]; sqlSafef(query, sizeof(query), "select idx from gbMembers where userName='%s'", userName); return (uint)sqlQuickLongLong(conn, query); } static boolean haveTokenTable(struct sqlConnection *conn) /* Return true if centralDb has table gbMemberToken. */ { return sqlTableExists(conn, "gbMemberToken"); } static boolean isValidToken(struct sqlConnection *conn, uint token, char *userName, boolean *retMakeNewToken) /* Return TRUE if gbMemberToken has an entry that maps token to userName. * If retMakeNewToken is non-NULL, set it to TRUE if the token is older than TOKEN_LIFESPAN. */ { boolean isValid = FALSE; char query[512]; sqlSafef(query, sizeof(query), "select userName, createTime from gbMemberToken where token = %u", token); struct sqlResult *sr = sqlGetResult(conn, query); char **row; if ((row = sqlNextRow(sr)) != NULL) { char *userForToken = cloneString(row[0]); if (retMakeNewToken != NULL) { long createTime = sqlDateToUnixTime(row[1]); *retMakeNewToken = (time(NULL) - createTime > TOKEN_LIFESPAN); } isValid = sameString(userForToken, userName); } sqlFreeResult(&sr); return isValid; } static void deleteToken(struct sqlConnection *conn, uint token) /* Remove token's entry from gbMemberToken. */ { char query[512]; sqlSafef(query, sizeof(query), "delete from gbMemberToken where token = %u", token); sqlUpdate(conn, query); } static void insertToken(struct sqlConnection *conn, uint token, char *userName) /* Add a new entry to gbMemberToken mapping token to userName. */ { char query[512]; sqlSafef(query, sizeof(query), "insert into gbMemberToken values (%u, '%s', now())", token, userName); sqlUpdate(conn, query); } static uint newToken() -/* Return a random nonnegative integer. In the extremely unlikely event that it is 0, - * the user will have to log in again. */ -{ -uint token = 0; -// open random system device for read-only access. -FILE *f = mustOpen("/dev/urandom", "r"); -mustRead(f, &token, 4); -carefulClose(&f); +/* Return a random nonzero positive integer. */ +{ +srand(clock1000()); +uint token = (uint)rand(); +if (token == 0) + // highly unlikely - try again. + token = (uint)rand(); return token; } -char *getCookieDomainString() -/* Get a string that will look something like " domain=.ucsc.edu;" if central.domain - * is defined, otherwise just "". Don't free result. */ +uint loginSystemLoginUser(char *userName) +/* Return a nonzero token which caller must set as the value of CFG_WIKI_LOGGED_IN_COOKIE. + * Call this when userName's password has been validated. */ { -static char domainString[256]; -char *domain = cloneString(cfgOption(CFG_CENTRAL_DOMAIN)); -if (domain != NULL && strchr(domain, '.') != NULL) - safef(domainString, sizeof(domainString), " domain=%s;", domain); +struct sqlConnection *conn = hConnectCentral(); +alreadyAuthenticated = TRUE; +if (haveTokenTable(conn)) + { + authToken = newToken(); + insertToken(conn, authToken, userName); + } else - domainString[0] = '\0'; -return domainString; + // Fall back on gbMembers.idx + authToken = getMemberIdx(conn, userName); +hDisconnectCentral(&conn); +return authToken; } static char *loginCookieDate() /* For now, don't expire (before we retire :) Consider changing this to 6 months in the * future or something like that, maybe under hg.conf control (for CIRM vs GB?). */ { return "Thu, 31-Dec-2037 23:59:59 GMT"; } -#define EXPIRED_COOKIE_DATE "Thu, 01-Jan-1970 00:00:00 GMT" - -struct slName *invalidateCookieString(char *cookieName) -/* Return a cookie string that deletes/invalidates the cookie. */ -{ -char *domain = getCookieDomainString(); -char cookieString[2048]; -safef(cookieString, sizeof(cookieString), "%s=;%s path=/; expires="EXPIRED_COOKIE_DATE, - cookieName, domain); -return slNameNew(cookieString); -} - -struct slName *loginUserNameCookieString(char *userName) -/* Return a cookie string that sets userName cookie to userName if non-empty and - * deletes/invalidates the cookie if empty. */ -{ -char *cookie = loginUserNameCookie(); -if (isNotEmpty(userName)) +static char *expiredCookieDate() +/* Return a date that passed long ago. */ { - // Send userName in cookie - char *domain = getCookieDomainString(); - char cookieString[2048]; - safef(cookieString, sizeof(cookieString), "%s=%s;%s path=/; expires=%s", - cookie, userName, domain, loginCookieDate()); - return slNameNew(cookieString); - } -else - return invalidateCookieString(cookie); +return "Thu, 01-Jan-1970 00:00:00 GMT"; } -struct slName *loginTokenCookieString(uint token) -/* Return a cookie string that sets token cookie to token if token is valid and - * deletes/invalidates the cookie if not. */ -{ -char *cookie = loginTokenCookie(); -if (token) +char *makeAuthCookieString() +/* Return a cookie string that sets cookie to authToken if token is valid and + * deletes/invalidates both cookies if not. */ { +struct dyString *dy = dyStringCreate("%s=", wikiLinkLoggedInCookie()); +if (authToken) // Validated; send new token in cookie - char *domain = getCookieDomainString(); - char cookieString[2048]; - safef(cookieString, sizeof(cookieString), "%s=%u;%s path=/; expires=%s", - cookie, token, domain, loginCookieDate()); - return slNameNew(cookieString); - } + dyStringPrintf(dy, "%u; path=/; domain=%s; expires=%s\r\n", + authToken, cgiServerName(), loginCookieDate()); else - return invalidateCookieString(cookie); -} - -struct slName *loginLoginUser(char *userName) -/* Return a nonzero token which caller must set as the value of CFG_WIKI_LOGGED_IN_COOKIE. - * Call this when userName's password has been validated. */ { -struct sqlConnection *conn = hConnectCentral(); -alreadyAuthenticated = TRUE; -if (haveTokenTable(conn)) - { - authToken = newToken(); - insertToken(conn, authToken, userName); - } -else - // Fall back on gbMembers.idx - authToken = getMemberIdx(conn, userName); -hDisconnectCentral(&conn); -slAddHead(&cookieStrings, loginTokenCookieString(authToken)); -slAddHead(&cookieStrings, loginUserNameCookieString(userName)); -return cookieStrings; + // Remove both cookies + dyStringPrintf(dy, "; path=/; domain=%s; expires=%s\r\n", + cgiServerName(), expiredCookieDate()); + dyStringPrintf(dy, "%s=; path=/; domain=%s; expires=%s\r\n", + wikiLinkUserNameCookie(), + cgiServerName(), expiredCookieDate()); } - -struct slName *loginLogoutUser() -/* If the gbMemberToken table exists, delete the user's random token. */ -{ -struct sqlConnection *conn = hConnectCentral(); -if (haveTokenTable(conn)) - deleteToken(conn, getCookieToken(NULL)); -slAddHead(&cookieStrings, loginTokenCookieString(0)); -slAddHead(&cookieStrings, loginUserNameCookieString(NULL)); -hDisconnectCentral(&conn); -return cookieStrings; +return dyStringCannibalize(&dy); } -struct slName *loginValidateCookies() -/* Return possibly empty list of cookie strings for the caller to set. - * If login cookies are present and valid, but the current token has aged out, - * the returned cookie string sets the token cookie to a new token value. - * If login cookies are present but invalid, the result deletes/expires the cookies. - * Otherwise returns NULL (no change to cookies). */ +char *loginSystemValidateCookies() +/* Return a cookie string or NULL. If login cookies are present and valid, but the current + * token has aged out, the returned cookie string sets a cookie to a new token value. + * If login cookies are present but invalid, the cookie string deletes/expires the cookies. + * Otherwise returns NULL. */ { if (alreadyAuthenticated) - return cookieStrings; + return makeAuthCookieString(); alreadyAuthenticated = TRUE; authToken = 0; -char *userName = findCookieData(loginUserNameCookie()); -boolean replaceOldCookies = FALSE; // TODO: remove in July 2016 -if (isEmpty(userName) && wikiLinkEnabled()) // TODO: remove in July 2016 - { // TODO: remove in July 2016 - userName = findCookieData(wikiLinkUserNameCookie()); // TODO: remove in July 2016 - if (isNotEmpty(userName)) // TODO: remove in July 2016 - replaceOldCookies = TRUE; // TODO: remove in July 2016 - } // TODO: remove in July 2016 -boolean deleteCookies = FALSE; // TODO: remove in July 2016 -uint cookieToken = getCookieToken(&replaceOldCookies); +char *userName = findCookieData(wikiLinkUserNameCookie()); +uint cookieToken = getCookieToken(); if (userName && cookieToken) { struct sqlConnection *conn = hConnectCentral(); uint memberIdx = getMemberIdx(conn, userName); + char *cookieString = NULL; if (haveTokenTable(conn)) { // Look up cookieToken and userName in gbMemberToken boolean makeNewToken = FALSE; boolean tokenIsValid = isValidToken(conn, cookieToken, userName, &makeNewToken); if (tokenIsValid // Also accept gbMembers.idx to smooth the transition; TODO: remove in July 2016 || (cookieToken == memberIdx)) // TODO: remove in July 2016 { if (makeNewToken || ! tokenIsValid) // TODO: remove in July 2016 { // Delete the old token, create and store a new token, and make a cookie string // with the new token. deleteToken(conn, cookieToken); authToken = newToken(); insertToken(conn, authToken, userName); - slAddHead(&cookieStrings, loginTokenCookieString(authToken)); + cookieString = makeAuthCookieString(); } else // Keep using this token, no change to cookie authToken = cookieToken; } else - { - // Invalid token; delete cookies - deleteCookies = TRUE; // TODO: remove in July 2016 - slAddHead(&cookieStrings, loginTokenCookieString(0)); - slAddHead(&cookieStrings, loginUserNameCookieString(NULL)); - } + // Invalid token; delete both cookies + cookieString = makeAuthCookieString(); } else if (cookieToken == memberIdx) // centralDb does not have gbMemberToken table -- fall back on gbMembers.idx authToken = cookieToken; hDisconnectCentral(&conn); + return cookieString; } -// Delete the cookies that we used to use and make sure the new cookies are set. remove in July '16 -if (replaceOldCookies) // TODO: remove in July 2016 - { // TODO: remove in July 2016 - if (cookieStrings == NULL) // TODO: remove in July 2016 - slAddHead(&cookieStrings, loginTokenCookieString(authToken)); // TODO: remove in July 2016 - if (! deleteCookies) // TODO: remove in July 2016 - slAddHead(&cookieStrings, loginUserNameCookieString(userName)); // TODO: remove in July 2016 - slAddHead(&cookieStrings, invalidateCookieString(wikiLinkLoggedInCookie())); // TODO: remove in July 2016 - slAddHead(&cookieStrings, invalidateCookieString(wikiLinkUserNameCookie())); // TODO: remove in July 2016 - } // TODO: remove in July 2016 -return cookieStrings; +return NULL; } char *wikiLinkHost() /* Return the wiki host specified in hg.conf, or NULL. Allocd here. * Returns hostname from http request if hg.conf entry is HTTPHOST. * */ { char *wikiHost = cfgOption(CFG_WIKI_HOST); -if (isEmpty(wikiHost) || sameString(wikiHost, "HTTPHOST")) +if ((wikiHost!=NULL) && sameString(wikiHost, "HTTPHOST")) wikiHost = hHttpHost(); return cloneString(wikiHost); } +boolean wikiLinkEnabled() +/* Return TRUE if all wiki.* parameters are defined in hg.conf . */ +{ +return ((cfgOption(CFG_WIKI_HOST) != NULL) && + (cfgOption(CFG_WIKI_USER_NAME_COOKIE) != NULL) && + (cfgOption(CFG_WIKI_LOGGED_IN_COOKIE) != NULL)); +} + char *wikiLinkUserName() /* Return the user name specified in cookies from the browser, or NULL if * the user doesn't appear to be logged in. */ { -if (loginSystemEnabled()) - { - if (! alreadyAuthenticated) - errAbort("wikiLinkUserName: loginValidateCookies must be called first."); - char *userName = findCookieData(loginUserNameCookie()); - if (isEmpty(userName) && wikiLinkEnabled()) // TODO: remove in July 2016 - userName = findCookieData(wikiLinkUserNameCookie()); // TODO: remove in July 2016 - if (authToken) - return cloneString(userName); - } -else if (wikiLinkEnabled()) +if (wikiLinkEnabled()) { char *wikiUserName = findCookieData(wikiLinkUserNameCookie()); char *wikiLoggedIn = findCookieData(wikiLinkLoggedInCookie()); + if (isNotEmpty(wikiLoggedIn) && isNotEmpty(wikiUserName)) + { + if (loginSystemEnabled()) + { + if (! alreadyAuthenticated) + errAbort("wikiLinkUserName: loginSystemValidateCookies must be called first."); + if (authToken == 0) + return NULL; + } return cloneString(wikiUserName); } + } else errAbort("wikiLinkUserName called when wiki is not enabled (specified " "in hg.conf)."); return NULL; } static char *encodedHgSessionReturnUrl(char *hgsid) /* Return a CGI-encoded hgSession URL with hgsid. Free when done. */ { char retBuf[1024]; char *cgiDir = cgiScriptDirUrl(); safef(retBuf, sizeof(retBuf), "http%s://%s%shgSession?hgsid=%s", cgiAppendSForHttps(), cgiServerNamePort(), cgiDir, hgsid); return cgiEncode(retBuf); } char *wikiLinkUserLoginUrlReturning(char *hgsid, char *returnUrl) /* Return the URL for the wiki user login page. */ { char buf[2048]; if (loginSystemEnabled()) { + if (! wikiLinkEnabled()) + errAbort("wikiLinkUserLoginUrl called when login system is not enabled " + "(specified in hg.conf)."); safef(buf, sizeof(buf), "http%s://%s/cgi-bin/hgLogin?hgLogin.do.displayLoginPage=1&returnto=%s", cgiAppendSForHttps(), wikiLinkHost(), returnUrl); } else { if (! wikiLinkEnabled()) errAbort("wikiLinkUserLoginUrl called when wiki is not enabled (specified " "in hg.conf)."); safef(buf, sizeof(buf), "http://%s/index.php?title=Special:UserloginUCSC&returnto=%s", wikiLinkHost(), returnUrl); } return(cloneString(buf)); } char *wikiLinkUserLoginUrl(char *hgsid) /* Return the URL for the wiki user login page with return going to hgSessions. */ { char *retUrl = encodedHgSessionReturnUrl(hgsid); char *result = wikiLinkUserLoginUrlReturning(hgsid, retUrl); freez(&retUrl); return result; } char *wikiLinkUserLogoutUrlReturning(char *hgsid, char *returnUrl) /* Return the URL for the wiki user logout page. */ { char buf[2048]; if (loginSystemEnabled()) { + if (! wikiLinkEnabled()) + errAbort("wikiLinkUserLogoutUrl called when login system is not enabled " + "(specified in hg.conf)."); safef(buf, sizeof(buf), "http%s://%s/cgi-bin/hgLogin?hgLogin.do.displayLogout=1&returnto=%s", cgiAppendSForHttps(), wikiLinkHost(), returnUrl); } else { if (! wikiLinkEnabled()) errAbort("wikiLinkUserLogoutUrl called when wiki is not enable (specified " "in hg.conf)."); safef(buf, sizeof(buf), "http://%s/index.php?title=Special:UserlogoutUCSC&returnto=%s", wikiLinkHost(), returnUrl); } return(cloneString(buf)); } char *wikiLinkUserLogoutUrl(char *hgsid) /* Return the URL for the wiki user logout page that returns to hgSessions. */ { char *retEnc = encodedHgSessionReturnUrl(hgsid); char *result = wikiLinkUserLogoutUrlReturning(hgsid, retEnc); freez(&retEnc); return result; } char *wikiLinkUserSignupUrl(char *hgsid) /* Return the URL for the user signup page. */ { char buf[2048]; char *retEnc = encodedHgSessionReturnUrl(hgsid); if (loginSystemEnabled()) { + if (! wikiLinkEnabled()) + errAbort("wikiLinkUserSignupUrl called when login system is not enabled " + "(specified in hg.conf)."); safef(buf, sizeof(buf), "http%s://%s/cgi-bin/hgLogin?hgLogin.do.signupPage=1&returnto=%s", cgiAppendSForHttps(), wikiLinkHost(), retEnc); } else { if (! wikiLinkEnabled()) errAbort("wikiLinkUserLogoutUrl called when wiki is not enable (specified " "in hg.conf)."); safef(buf, sizeof(buf), "http://%s/index.php?title=Special:UserlogoutUCSC&returnto=%s", wikiLinkHost(), retEnc); } freez(&retEnc); return(cloneString(buf)); } char *wikiLinkChangePasswordUrl(char *hgsid) /* Return the URL for the user change password page. */ { char buf[2048]; char *retEnc = encodedHgSessionReturnUrl(hgsid); if (loginSystemEnabled()) { + if (! wikiLinkEnabled()) + errAbort("wikiLinkChangePasswordUrl called when login system is not enabled " + "(specified in hg.conf)."); safef(buf, sizeof(buf), "http%s://%s/cgi-bin/hgLogin?hgLogin.do.changePasswordPage=1&returnto=%s", cgiAppendSForHttps(), wikiLinkHost(), retEnc); } else { if (! wikiLinkEnabled()) errAbort("wikiLinkUserLogoutUrl called when wiki is not enable (specified " "in hg.conf)."); safef(buf, sizeof(buf), "http://%s/index.php?title=Special:UserlogoutUCSC&returnto=%s", wikiLinkHost(), retEnc); } freez(&retEnc); return(cloneString(buf)); }