14ecff0730596f8be147b255afc8acb99e9095e2 max Fri Sep 18 09:06:06 2026 -0700 hgHubConnect: an api key made on one geo mirror now works on all of them, behind syncHubApiKeys #Preview2 week - bugs introduced now will need a build patch to fix #Preview2 week - bugs introduced now will need a build patch to fix Api keys live in the hgcentral of whichever mirror issued them, so a key made on genome.ucsc.edu was rejected on genome-euro and genome-asia, and the botDelay error told the user to go and make a second one. hgHubConnect now tells the other nodes about a key as soon as it generates or revokes one: cjGenerateApiKey and cjRevokeApiKey call syncApiKeyToOtherNodes(), which posts a hgHubSyncApiKey cartJson request to each peer, and cjSyncApiKey() on the far side writes it into that mirror's own table. The peers come from hgcentral.gbNode via the new geoMirrorNotifyOtherNodes(), and the request is signed with login.cookieSalt, which all of a site's mirrors already share - it is what makes the login cookie verifiable on each of them. So neither a list of mirror addresses nor a new shared secret needs provisioning. The notify is best effort: a peer that is down is warn()ed about and skipped, never failing the local generate or revoke, which has already committed by then. All of it is off unless hg.conf says syncHubApiKeys=on. With the gate off the sender returns at once, the receiver refuses the request outright rather than merely being unreachable, and botDelay keeps printing the old server-specific wording, which off is still the truth. Registered in hgConfCatalog.py as a gate, to be flipped once this is released. refs #38323 diff --git src/hg/lib/geoMirror.c src/hg/lib/geoMirror.c index 996dc803799..05c8e660463 100644 --- src/hg/lib/geoMirror.c +++ src/hg/lib/geoMirror.c @@ -1,389 +1,426 @@ /* 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) +/* 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. + * 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 + * complete locally before this is called, since a peer being down must never fail the local + * action. */ +{ +struct slPair *nodes = geoMirrorOtherNodes(); +struct slPair *node; +for (node = nodes; node != NULL; node = node->next) + { + struct dyString *url = dyStringCreate("http://%s/cgi-bin/%s?", (char *)node->val, 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)); + struct errCatch *errCatch = errCatchNew(); + if (errCatchStart(errCatch)) + { + struct dyString *response = netSlurpUrl(url->string); + dyStringFree(&response); + } + errCatchEnd(errCatch); + if (errCatch->gotError) + warn("geoMirrorNotifyOtherNodes: failed to reach %s (%s): %s", + node->name, (char *)node->val, errCatch->message->string); + errCatchFree(&errCatch); + dyStringFree(&url); + } +slPairFreeValsAndList(&nodes); +} + char *geoMirrorMenu() /* Create customized geoMirror menu string for substitution of into * 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, "

  • \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, "
  • \n"); dyStringAppend(dy, "\"X\"\n"); if (!sameString(node, myNode)) dyStringPrintf(dy, "", domain); dyStringPrintf(dy, "%s", shortLabel); if (!sameString(node, myNode)) dyStringAppend(dy, ""); dyStringAppend(dy, "
  • \n"); } sqlFreeResult(&sr); hDisconnectCentral(&conn); } return dyStringCannibalize(&dy); }