b450839c514656467470338eaae47003e344b522 max Sat Sep 26 21:52:53 2026 -0700 geoMirror: send peer sync payload as a POST body, and pin the certificate check for it diff --git src/hg/lib/geoMirror.c src/hg/lib/geoMirror.c index 6fe482b9e85..31623086268 100644 --- src/hg/lib/geoMirror.c +++ src/hg/lib/geoMirror.c @@ -1,446 +1,472 @@ /* 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" +#include "https.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); } +static char *geoMirrorPostRequest(char *url, char *body) +/* POST body to url with certificate checking forced on for this call, and return the response + * body, or NULL on failure. Caller frees the result. */ +{ +/* This carries a signed proof of the caller's action to a peer node, so a forged certificate + * on the way there must not be accepted just because the site's own httpsCertCheck default is + * "log". Pin "abort" for this call the same way oauthLogin.c's httpRequest() does. */ +httpsSetCertCheck("abort"); +struct dyString *header = dyStringNew(256); +dyStringPrintf(header, "Content-Type: application/x-www-form-urlencoded\r\n"); +dyStringPrintf(header, "Content-Length: %d\r\n", (int)strlen(body)); +char *result = NULL; +struct errCatch *errCatch = errCatchNew(); +if (errCatchStart(errCatch)) + { + int sd = netOpenHttpExt(url, "POST", header->string); + mustWriteFd(sd, body, strlen(body)); + int newSd = 0; + char *newUrl = NULL; + if (!netSkipHttpHeaderLinesHandlingRedirect(sd, url, &newSd, &newUrl)) + noWarnAbort(); + if (newUrl != NULL) + { + sd = newSd; + freeMem(newUrl); + } + struct dyString *response = netSlurpFile(sd); + close(sd); + result = dyStringCannibalize(&response); + } +errCatchEnd(errCatch); +if (errCatch->gotError) + freez(&result); +errCatchFree(&errCatch); +dyStringFree(&header); +return result; +} + 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()). 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 +/* Best-effort: POST cgiVars (name=value) as the body of a request to cgiName on every other geo + * mirror 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 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, *results = NULL; for (node = nodes; node != NULL; node = node->next) { 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); + // to Germany and Japan would leave it exposed in transit besides + struct dyString *urlDy = dyStringCreate("https://%s/cgi-bin/%s", domain, cgiName); + char *url = dyStringCannibalize(&urlDy); + struct dyString *body = dyStringNew(256); struct slPair *var; for (var = cgiVars; var != NULL; var = var->next) - dyStringPrintf(url, "%s%s=%s", (var == cgiVars) ? "" : "&", var->name, + dyStringPrintf(body, "%s%s=%s", (var == cgiVars) ? "" : "&", var->name, cgiEncodeFull((char *)var->val)); - char *body = NULL; - struct errCatch *errCatch = errCatchNew(); - if (errCatchStart(errCatch)) - { - // 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) - { - // 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); + char *result = geoMirrorPostRequest(url, body->string); + if (result == NULL) + // Log the host only, never the request body or the full URL with its query. + fprintf(stderr, "geoMirrorNotifyOtherNodes: failed to reach %s (%s)\n", + node->name, domain); + slPairAdd(&results, domain, result); + dyStringFree(&body); + freez(&url); } slPairFreeValsAndList(&nodes); slReverse(&results); return results; } 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); }