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
  * <!-- 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);
 }