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,27 +1,28 @@
 /* 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)
 
@@ -332,78 +333,103 @@
 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())
     {