2197f6d5208aff4c48ccbe42e61a116d988ac392
max
  Tue May 19 08:23:54 2026 -0700
hubApi: add /blat endpoint with apiKey gating, format=hgblat, and known-agent bypass

New src/hg/hubApi/blat.c implements /blat/<type> (dna, protein, transRna,
transDna, guess) backed by the same gfServer logic as hgBlat.  Key details:

- Requires an apiKey for rate-limiting; botException() and
botExceptionUserAgent() exempt IPs/user-agents in hg.conf (same
policy as captcha bypass elsewhere in the browser stack).
- Invalid apiKey returns a clean JSON 403 rather than an HTML 500
(pre-validated in hubApi.c main() before hgBotDelayTimeFrac runs).
- Extra bot-delay fraction (default 0.3, 10x hubApi default) is
configurable via hubApi.blatDelayFraction in hg.conf.
- format=text/psl  -> PSL text; format=hgblat -> byte-for-byte
hgBlat?output=json shape; jsonOutputArrays=1 -> hubApi envelope
with arrays (parallel to getData behaviour); default -> objects.
- botExceptionUserAgent() carved out of cart.c's static
isUserAgentException() into botDelay.c so non-cart callers can use it.
- Cross-reference comments added in hgBlat.c and blat.c noting the
shared logic so fixes get applied to both.

refs #36315

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

diff --git src/hg/lib/botDelay.c src/hg/lib/botDelay.c
index 0b0f7fa85f2..c4c5ff2ffa6 100644
--- src/hg/lib/botDelay.c
+++ src/hg/lib/botDelay.c
@@ -282,30 +282,53 @@
 	    if (e)
 		*e = 0;
 	    if (sameString(remoteAddr, s))
 		found = TRUE;
 	    if (e)
 		*e++ = ' ';
 	    s = e;
 	    }
 	if (found)
 	    return TRUE;
 	}
     }
 return FALSE;
 }
 
+boolean botExceptionUserAgent()
+/* Return TRUE if the HTTP_USER_AGENT matches a noCaptchaAgent. pattern in hg.conf.
+ * Mirrors cart.c's isUserAgentException(); kept here so non-cart callers
+ * (e.g. hubApi/blat.c) can reach it without pulling in the full cart library. */
+{
+char *agent = cgiUserAgent();
+if (!agent)
+    return FALSE;
+struct slName *excStrs = cfgValsWithPrefix("noCaptchaAgent.");
+if (!excStrs)
+    return FALSE;
+struct slName *sl;
+for (sl = excStrs; sl != NULL; sl = sl->next)
+    {
+    if (regexMatch(agent, sl->name))
+        {
+        fprintf(stderr, "CAPTCHAPASS %s matches %s\n", agent, sl->name);
+        return TRUE;
+        }
+    }
+return FALSE;
+}
+
 int hgBotDelayTime()
 {
 return hgBotDelayTimeFrac(defaultDelayFrac);
 }
 
 int hgBotDelayTimeFrac(double fraction)
 /* Get suggested delay time from cgi using the standard penalty. */
 {
 char *ip = getenv("REMOTE_ADDR");
 char *host = cfgOption("bottleneck.host");
 char *port = cfgOption("bottleneck.port");
 
 int delay = 0;
 if (host != NULL && port != NULL && ip != NULL)
     {