33eb806d3707661d8a4d9af1e0673f90578d86bc max Mon Sep 21 06:12:21 2026 -0700 hgHubConnect: the api key sync has to answer before the cart, and its signature needed a timestamp, refs #38323 Six things, all in the syncHubApiKeys path, which is still off everywhere. The sync never reached its handler on a real mirror. It was registered as a cartJson command, so building the cart ran forceUserIdOrCaptcha() first; a peer's request carries no hguid cookie and no apiKey= variable, so any site with cloudFlareSiteKey set -- which is every site that needs api keys in the first place -- answered it with a captcha page. Only hgwdev, where the captcha is commented out, ever ran the handler, which is why this was not caught earlier. doApiKeySyncIfRequested() now answers the request in main() before any cart exists. That also stops each sync leaving a junk userDb and sessionDb row behind: measured, 15 syncs now create none. The signature is HMAC-SHA256 over the salt instead of md5(salt-user-key), so it does not rest on md5 resisting length extension, and it is compared in constant time. It now covers a timestamp too, and a peer refuses anything signed more than HUB_APIKEY_SYNC_WINDOW (300s) either side of now, so a captured sync cannot be replayed later to reinstate a key its owner has since revoked. The fields are joined with newlines and a newline in a userName or apiKey is refused, so one signature cannot cover two different splits of the same string. syncApiKeyToOtherNodes() is called inside an errCatch of its own now. It runs after the local key is already written and in the response, so an errAbort there (login.cookieSalt unset) used to reach cartJsonExecute's outer catch, which threw the response away -- the user saw a failure over a key that was sitting in the database. geoMirrorNotifyOtherNodes() returns each peer's response instead of discarding it, and uses netUrlMustOpenPastHeader, which errAborts on anything but a 200 and hands back the body alone. A peer answering 'bad signature' or 'not enabled on this site' is no longer indistinguishable from success; it is logged to error_log, not warn()ed at the person who clicked the button. Verified against the built CGI run as Apache runs it, with the captcha turned on: a signed sync is accepted and writes the row, a signed revoke removes it, and a replay, a swapped userName, a forged signature and malformed json are all refused. hubSpaceKeysTester covers the signature rules; it still cannot run its database half until hgcentralregress gets the UPDATE and DELETE grant. diff --git src/hg/lib/geoMirror.c src/hg/lib/geoMirror.c index 2da6a2eb62f..6fe482b9e85 100644 --- src/hg/lib/geoMirror.c +++ src/hg/lib/geoMirror.c @@ -1,428 +1,446 @@ /* geoMirror - support geographic based mirroring (e.g. euro and asia nodes) */ /* Copyright (C) 2014 The Regents of the University of California * See kent/LICENSE or http://genome.ucsc.edu/license/ for licensing information. */ #include "common.h" #include "geoMirror.h" #include "hgConfig.h" #include "internet.h" #include "net.h" #include "cheapcgi.h" #include "errCatch.h" /* geographic server (mirror) support Customize 'Mirrors' drop-down menu based on current server. If the server is a UCSC-sponsored site (USA/Ca or European), show this in drop-down menu with a checkmark, and allow user to change server. Based on design notes here: http://genomewiki.ucsc.edu/genecats/index.php/Euronode NOTE: Uses hgcentral.gbNode table to populate menu items and locate redirects. This implementation uses the spec from above wiki page: browser.node=1 -> US server (genome.ucsc.edu) browser.node=2 -> European server (genome-euro.ucsc.edu) browser.node=3 -> Asian server (genome-asia.ucsc.edu) For Testing, use browser.geoSuffix: e.g. browser.geoSuffix=Test will cause it to use in hgcentral the tables gbNodeTest and geoIpNodeTest instead. See the genomewiki link above for more documentation on testing. */ boolean geoMirrorEnabled() { // return TRUE if this site has geographic mirroring turned on return geoMirrorNode() != NULL; } char *geoMirrorNode() { // return which geo mirror node this is (or NULL if geo mirroring is turned off) return cfgOption("browser.node"); } int geoMirrorDefaultNode4(struct sqlConnection *centralConn, char *ipStr) // return default node for given IP { char query[1024]; bits32 ip = 0; int defaultNode = 1; if (isIpv6Address(ipStr)) // ipv6 which we do not handle yet. TODO return defaultNode; // at least tolerate IPV6 remote addr internetDottedQuadToIp(ipStr, &ip); char *geoSuffix = cfgOptionDefault("browser.geoSuffix",""); // We (sort-of) assume no overlaps in geoIpNode table, so we can use limit 1 to make query very efficient; // we do accomodate a range that is completely contained in another (to accomodate the hgroaming entry for testing); // this is accomplished by "<= ipEnd" in the sql query. // TODO The hgroaming thing is probably obsolete and testing is done with browser.geoSuffix= instead. // If so, we may wish to remove the loop below since that was added by Larry and reformulate // it as it was originally done by Galt. However it does not seem to affect performance so we can leave it for now. sqlSafef(query, sizeof query, "select ipStart, ipEnd, node from geoIpNode%s where %u >= ipStart and %u <= ipEnd order by ipStart desc limit 1" , geoSuffix, ip, ip); char **row; struct sqlResult *sr = sqlGetResult(centralConn, query); if ((row = sqlNextRow(sr)) != NULL) { uint ipStart = sqlUnsigned(row[0]); uint ipEnd = sqlUnsigned(row[1]); if (ipStart <= ip && ipEnd >= ip) { defaultNode = sqlSigned(row[2]); } } sqlFreeResult(&sr); return defaultNode; } int geoMirrorDefaultNode6(struct sqlConnection *centralConn, char *ipStr) // return default node for given IP { char query[1024]; char newIpStr[NI_MAXHOST]; struct in6_addr ip; ZeroVar(&ip); char ipHex[33]; int defaultNode = 1; if (isIpv6Address(ipStr)) safef(newIpStr, sizeof newIpStr, "%s", ipStr); else if (isIpv4Address(ipStr)) safef(newIpStr, sizeof newIpStr, "%s%s", IPV4MAPPED_PREFIX, ipStr); // "::ffff:" else { warn("Unexpected strange ip address string: %s", ipStr); return defaultNode; } if (!internetIpStringToIp6(newIpStr, &ip)) errAbort("internetIpStringToIp6 failed for %s", ipStr); ip6AddrToHexStr(&ip, ipHex, sizeof ipHex); char *geoSuffix = cfgOptionDefault("browser.geoSuffix",""); // We (sort-of) assume no overlaps in geoIpNode table, so we can use limit 1 to make query very efficient; // we do accomodate a range that is completely contained in another (to accomodate the hgroaming entry for testing); // this is accomplished by "<= ipEnd" in the sql query. // TODO The hgroaming thing is probably obsolete and testing is done with browser.geoSuffix= instead. // If so, we may wish to remove the loop below since that was added by Larry and reformulate // it as it was originally done by Galt. However it does not seem to affect performance so we can leave it for now. sqlSafef(query, sizeof query, "select ipStart, ipEnd, node from geoIpNode6%s where unhex('%s') >= ipStart and unhex('%s') <= ipEnd order by ipStart desc limit 1" , geoSuffix, ipHex, ipHex); char **row; struct sqlResult *sr = sqlGetResult(centralConn, query); if ((row = sqlNextRow(sr)) != NULL) { struct in6_addr ipStart; ip6AddrCopy((struct in6_addr *)row[0], &ipStart); struct in6_addr ipEnd; ip6AddrCopy((struct in6_addr *)row[1], &ipEnd ); if ( (ip6AddrCmpBits(&ipStart, &ip) <= 0) && (ip6AddrCmpBits(&ipEnd , &ip) >= 0) ) { defaultNode = sqlSigned(row[2]); } } sqlFreeResult(&sr); return defaultNode; } int geoMirrorDefaultNode(struct sqlConnection *centralConn, char *ipStr) // return default node for given IP { char *geoSuffix = cfgOptionDefault("browser.geoSuffix",""); char fullTableName[1024]; safef(fullTableName, sizeof fullTableName, "%s%s", "geoIpNode6", geoSuffix); int defaultNode = 1; if (sqlTableExists(centralConn, fullTableName)) { defaultNode = geoMirrorDefaultNode6(centralConn, ipStr); } else { defaultNode = geoMirrorDefaultNode4(centralConn, ipStr); } return defaultNode; } char *geoMirrorCountry6(struct sqlConnection *centralConn, char *ipStr) /* Return 2 letter country code for given IP. user has already checked table geoIpCountry6 exists. * Return error string otherwise. Free the response string. */ { char query[1024]; char newIpStr[NI_MAXHOST]; struct in6_addr ip; ZeroVar(&ip); char ipHex[33]; char response[256]; safef(response, sizeof response, "not found"); if (isIpv6Address(ipStr)) safef(newIpStr, sizeof newIpStr, "%s", ipStr); else if (isIpv4Address(ipStr)) safef(newIpStr, sizeof newIpStr, "%s%s", IPV4MAPPED_PREFIX, ipStr); // "::ffff:" else { warn("Unexpected strange ip address string: %s", ipStr); safef(response, sizeof response, "Unexpected strange ip address string: %s", ipStr); return cloneString(response); } if (!internetIpStringToIp6(newIpStr, &ip)) errAbort("internetIpStringToIp6 failed for %s", ipStr); ip6AddrToHexStr(&ip, ipHex, sizeof ipHex); char *geoSuffix = cfgOptionDefault("browser.geoSuffix",""); // We (sort-of) assume no overlaps in geoIpCountry6 table, so we can use limit 1 to make query very efficient; // we do accomodate a range that is completely contained in another (to accomodate the hgroaming entry for testing); // this is accomplished by "<= ipEnd" in the sql query. // TODO The hgroaming thing is probably obsolete and testing is done with browser.geoSuffix= instead. // If so, we may wish to remove the loop below since that was added by Larry and reformulate // it as it was originally done by Galt. However it does not seem to affect performance so we can leave it for now. sqlSafef(query, sizeof query, "select ipStart, ipEnd, countryId from geoIpCountry6%s where unhex('%s') >= ipStart and unhex('%s') <= ipEnd order by ipStart desc limit 1" , geoSuffix, ipHex, ipHex); char **row; struct sqlResult *sr = sqlGetResult(centralConn, query); if ((row = sqlNextRow(sr)) != NULL) { struct in6_addr ipStart; ip6AddrCopy((struct in6_addr *)row[0], &ipStart); struct in6_addr ipEnd; ip6AddrCopy((struct in6_addr *)row[1], &ipEnd ); if ( (ip6AddrCmpBits(&ipStart, &ip) <= 0) && (ip6AddrCmpBits(&ipEnd , &ip) >= 0) ) { safef(response, sizeof response, "%s", row[2]); } } sqlFreeResult(&sr); return cloneString(response); } struct geoNode /* One row of hgcentral gbNode. */ { struct geoNode *next; char *node; /* the browser.node number, as text */ char *domain; /* e.g. genome-euro.ucsc.edu */ char *shortLabel; /* e.g. European Server */ }; static char *geoMirrorThisHost() /* The host name this request came in under, without any port, or NULL when there is none (the * command line). Not freed: it comes from the environment. */ { static char host[256]; char *httpHost = getenv("HTTP_HOST"); if (isEmpty(httpHost)) return NULL; safecpy(host, sizeof host, httpHost); char *colon = strchr(host, ':'); // HTTP_HOST carries the port when it is not 80/443 if (colon != NULL) *colon = '\0'; return host; } static struct geoNode *geoMirrorSelf(struct geoNode *nodeList) /* Which of the gbNode rows is the server answering this request? The host the visitor typed * decides it whenever that host is one of the nodes, so a machine serving a node other than the * one browser.node names -- a sandbox, or two nodes behind one apache -- does not take itself for * its own peer. browser.node is the fallback, for a host that is in no gbNode row at all * (hgwdev.gi.ucsc.edu rather than genome-test.gi.ucsc.edu, a bare IP, the command line). */ { struct geoNode *node; char *myHost = geoMirrorThisHost(); if (isNotEmpty(myHost)) for (node = nodeList; node != NULL; node = node->next) if (sameWord(myHost, node->domain)) return node; char *myNode = geoMirrorNode(); for (node = nodeList; node != NULL; node = node->next) if (sameString(node->node, myNode)) return node; return NULL; } static void geoNodeFreeList(struct geoNode **pList) /* Free a list of geoNode. */ { struct geoNode *node, *next; for (node = *pList; node != NULL; node = next) { next = node->next; freeMem(node->node); freeMem(node->domain); freeMem(node->shortLabel); freeMem(node); } *pList = NULL; } static struct slPair *geoMirrorNodeList(boolean wantSelf) /* Return gbNode as pairs of name=shortLabel, val=domain, ordered by node: either every node but * this one (wantSelf FALSE) or only this one (wantSelf TRUE). */ { if (!geoMirrorEnabled()) return NULL; char *geoSuffix = cfgOptionDefault("browser.geoSuffix",""); char query[256]; sqlSafef(query, sizeof query, "SELECT node, domain, shortLabel from gbNode%s order by node", geoSuffix); struct sqlConnection *conn = hConnectCentral(); struct sqlResult *sr = sqlGetResult(conn, query); struct geoNode *nodeList = NULL, *node; char **row = NULL; while ((row = sqlNextRow(sr)) != NULL) { AllocVar(node); node->node = cloneString(row[0]); node->domain = cloneString(row[1]); node->shortLabel = cloneString(row[2]); slAddHead(&nodeList, node); } sqlFreeResult(&sr); hDisconnectCentral(&conn); slReverse(&nodeList); struct geoNode *self = geoMirrorSelf(nodeList); struct slPair *nodes = NULL; for (node = nodeList; node != NULL; node = node->next) if ((node == self) == wantSelf) slPairAdd(&nodes, node->shortLabel, cloneString(node->domain)); geoNodeFreeList(&nodeList); slReverse(&nodes); return nodes; } struct slPair *geoMirrorThisNode() /* Return this node (browser.node) as a single pair of name=shortLabel, val=domain, or NULL when * geo mirroring is off or gbNode has no row for it. slPairFreeValsAndList when done. */ { return geoMirrorNodeList(TRUE); } struct slPair *geoMirrorOtherNodes() /* Return the other geo mirror nodes, as pairs of name=shortLabel, val=domain, ordered by node. * The node this CGI is running on (browser.node) is left out. Returns NULL when geo mirroring * is off or this is the only node. slPairFreeValsAndList when done. */ { return geoMirrorNodeList(FALSE); } -void geoMirrorNotifyOtherNodes(char *cgiName, struct slPair *cgiVars) +struct slPair *geoMirrorNotifyOtherNodes(char *cgiName, struct slPair *cgiVars) /* Best-effort: fire cgiVars (name=value) as a GET request at cgiName on every other geo mirror - * node (per geoMirrorOtherNodes()). No-ops if geo mirroring is off or this is the only node. + * node (per geoMirrorOtherNodes()). Returns one pair per node attempted, name=node domain and + * val=the response body, or val=NULL for a node that could not be reached or answered anything + * but a 200 -- the caller is expected to look at what came back, since a peer that refuses the + * request answers with a body, not with a connection failure. Returns NULL when geo mirroring + * is off or this is the only node. slPairFreeValsAndList when done. * Adds no authentication of its own -- callers must put their own signed proof into cgiVars, * since the receiving CGI runs with no session/cart tying the request to a user. A slow or - * unreachable peer is logged with warn() and skipped; the caller's own action must already be + * unreachable peer is logged to stderr and skipped; the caller's own action must already be * complete locally before this is called, since a peer being down must never fail the local * action. */ { struct slPair *nodes = geoMirrorOtherNodes(); -struct slPair *node; +struct slPair *node, *results = NULL; for (node = nodes; node != NULL; node = node->next) { - // https, not http: the mirrors redirect http to https and netSlurpUrl does not follow - // redirects, so an http request never reaches the CGI at all - struct dyString *url = dyStringCreate("https://%s/cgi-bin/%s?", (char *)node->val, cgiName); + char *domain = (char *)node->val; + // https, not http: the mirrors redirect http to https, and sending a secret in the clear + // to Germany and Japan would leave it in each peer's access log besides + struct dyString *url = dyStringCreate("https://%s/cgi-bin/%s?", domain, cgiName); struct slPair *var; for (var = cgiVars; var != NULL; var = var->next) dyStringPrintf(url, "%s%s=%s", (var == cgiVars) ? "" : "&", var->name, cgiEncodeFull((char *)var->val)); + char *body = NULL; struct errCatch *errCatch = errCatchNew(); if (errCatchStart(errCatch)) { - struct dyString *response = netSlurpUrl(url->string); - dyStringFree(&response); + // MustOpenPastHeader, not netSlurpUrl: it errAborts on anything but a 200 and hands + // back the body alone, so the caller does not have to pick it out of the headers + int sd = netUrlMustOpenPastHeader(url->string); + struct dyString *response = netSlurpFile(sd); + close(sd); + body = dyStringCannibalize(&response); } errCatchEnd(errCatch); if (errCatch->gotError) - warn("geoMirrorNotifyOtherNodes: failed to reach %s (%s): %s", - node->name, (char *)node->val, errCatch->message->string); + { + // stderr, not warn(): this is between two servers, and the person who clicked the + // button in the browser can do nothing about a peer being down + fprintf(stderr, "geoMirrorNotifyOtherNodes: failed to reach %s (%s): %s\n", + node->name, domain, errCatch->message->string); + freez(&body); + } errCatchFree(&errCatch); + slPairAdd(&results, domain, body); dyStringFree(&url); } slPairFreeValsAndList(&nodes); +slReverse(&results); +return results; } char *geoMirrorMenu() /* Create customized geoMirror menu string for substitution of into * <!-- OPTIONAL_MIRROR_MENU --> in htdocs/inc/globalNavBar.inc * Reads hgcentral geo tables and hg.conf settings. * Free the returned string when done. */ { struct dyString *dy = dyStringNew(0); // by default replacment is just an empty string. if (geoMirrorEnabled()) { dyStringAppend(dy, "<li id=\"geoMirrorMenu\" class=\"noHighlight\"><hr></li>\n"); // Geo mirror functionality (e.g. in nav bar) char *myNode = geoMirrorNode(); /* hgcentral.gbNode table so UI can share w/ browser GEO mirror redirect code */ char **row = NULL; struct sqlConnection *conn = hConnectCentral(); // after hClade since it access hgcentral too // get the gbNode table char *geoSuffix = cfgOptionDefault("browser.geoSuffix",""); char query[256]; sqlSafef(query, sizeof query, "SELECT node, domain, shortLabel from gbNode%s order by node", geoSuffix); struct sqlResult *sr = sqlGetResult(conn, query); while ((row = sqlNextRow(sr)) != NULL) { char *node = row[0]; char *domain = row[1]; char *shortLabel = row[2]; dyStringPrintf(dy, "<li id=\"server%s\"", node); if (sameString(node, myNode)) dyStringAppend(dy, " class=\"noHighlight\""); dyStringAppend(dy, ">\n"); dyStringAppend(dy, "<img alt=\"X\" width=\"16\" height=\"16\" style=\"float:left;"); if (!sameString(node, myNode)) dyStringAppend(dy, "visibility:hidden;"); dyStringAppend(dy, "\" src=\"../images/greenChecksmCtr.png\">\n"); if (!sameString(node, myNode)) dyStringPrintf(dy, "<a href=\"https://%s/cgi-bin/hgGateway?redirect=manual\">", domain); dyStringPrintf(dy, "%s", shortLabel); if (!sameString(node, myNode)) dyStringAppend(dy, "</a>"); dyStringAppend(dy, "</li>\n"); } sqlFreeResult(&sr); hDisconnectCentral(&conn); } return dyStringCannibalize(&dy); }