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/hubApi/hubApi.c src/hg/hubApi/hubApi.c
index d92effe4854..81422758a95 100644
--- src/hg/hubApi/hubApi.c
+++ src/hg/hubApi/hubApi.c
@@ -1,21 +1,22 @@
 /* hubApi - access mechanism to hub data resources. */
 #include "dataApi.h"
 #include "botDelay.h"
 #include "jsHelper.h"
 #include "srcVersion.h"
 #include "asmAlias.h"
+#include "hubSpaceKeys.h"
 /* can not include bamFile.h with the liftOver business, there
  * is a conflict in a definition of the enum 'bed'
  */
 #include "bamFile.h"
 
 /*
 +------------------+------------------+------+-----+---------+-------+
 | Field            | Type             | Null | Key | Default | Extra |
 +------------------+------------------+------+-----+---------+-------+
 | hubUrl           | longblob         | NO   | PRI | NULL    |       |
 | shortLabel       | varchar(255)     | NO   |     | NULL    |       |
 | longLabel        | varchar(255)     | NO   |     | NULL    |       |
 | registrationTime | varchar(255)     | NO   |     | NULL    |       |
 | dbCount          | int(10) unsigned | NO   |     | NULL    |       |
 | dbList           | blob             | YES  |     | NULL    |       |
@@ -50,30 +51,31 @@
 char *argListPublicHubs[] = { NULL };
 char *argListUcscGenomes[] = { argGenome, NULL };
 char *argListGenarkGenomes[] = { argMaxItemsOutput, argGenome, NULL };
 char *argListHubGenomes[] = { argHubUrl, NULL };
 char *argListTracks[] = { argGenome, argHubUrl, argTrackLeavesOnly, NULL };
 char *argListChromosomes[] = { argGenome, argHubUrl, argTrack, NULL };
 char *argListSchema[] = { argGenome, argHubUrl, argTrack, NULL };
 char *argListFiles[] = { argGenome, argMaxItemsOutput, argFormat, argSkipContext, argFileType, NULL };
 char *argGetDataTrack[] = { argGenome, argHubUrl, argTrack, argChrom, argStart, argEnd, argMaxItemsOutput, argJsonOutputArrays, NULL };
 char *argGetDataSequence[] = { argGenome, argHubUrl, argTrack, argChrom, argStart, argEnd, argRevComp, NULL };
 char *argSearch[] = {argSearchTerm, argGenome, argHubUrl, argCategories, NULL};
 char *argFindGenome[] = {argQ, argMaxItemsOutput, argJsonOutputArrays, argStatsOnly, argBrowser, argYear, argCategory, argStatus, argLevel, argLiftable, NULL};
 char *argLiftOver[] = {argFromGenome, argToGenome, argChrom, argStart, argEnd, argFilter, argMaxItemsOutput, NULL};
 char *argLiftRequest[] = {argFromGenome, argToGenome, argEmail, argComment, NULL};
 char *argAssemblyRequest[] = {argAsmId, argName, argEmail, argBetterName, argComment, NULL};
+char *argBlat[] = {argGenome, argHubUrl, argUserSeq, argFormat, argMaxItemsOutput, argJsonOutputArrays, argApiKey, NULL};
 
 /* Global only to this one source file */
 static struct cart *cart;             /* CGI and other variables */
 static struct hash *oldVars = NULL;
 static struct hash *trackCounter = NULL;
 static long totalTracks = 0;
 static boolean allTrackSettings = FALSE;	/* checkbox setting */
 static char **shortLabels = NULL;	/* public hub short labels in array */
 static int publicHubCount = 0;
 static char *defaultHub = "Synonymous Constraint";
 static char *defaultDb = "ce11";
 long enteredMainTime = 0;	/* will become = clock1000() on entry */
 		/* to allow calculation of when to bail out, taking too long */
 static long timeOutSeconds = 100;
 static boolean timedOut = FALSE;
@@ -991,30 +993,31 @@
 
 static void setupFunctionHash()
 /* initialize the apiFunctionHash */
 {
 if (apiFunctionHash)
     return;	/* already done */
 
 apiFunctionHash = hashNew(0);
 hashAdd(apiFunctionHash, "list", &apiList);
 hashAdd(apiFunctionHash, "getData", &apiGetData);
 hashAdd(apiFunctionHash, "search", &apiSearch);
 hashAdd(apiFunctionHash, "findGenome", &apiFindGenome);
 hashAdd(apiFunctionHash, "liftOver", &apiLiftOver);
 hashAdd(apiFunctionHash, "liftRequest", &apiLiftRequest);
 hashAdd(apiFunctionHash, "assemblyRequest", &apiAssemblyRequest);
+hashAdd(apiFunctionHash, "blat", &apiBlat);
 }
 
 static struct hashEl *parsePathInfo(char *pathInfo, char *words[MAX_PATH_INFO])
 /* given a pathInfo string: /command/subCommand/etc...
  *  parse that and return a function pointer and the parsed words
  * Returns NULL if not recognized
  */
 {
 char *tmp = cloneString(pathInfo);
 /* skip the first leading slash to simplify chopByChar parsing */
 tmp += 1;
 int wordCount = chopByChar(tmp, '/', words, MAX_PATH_INFO);
 if (wordCount < 1 || wordCount > 2)
     return NULL;	/* only 2 words allowed */
 
@@ -1630,30 +1633,45 @@
 hPrintf("<head>\n");
 hPrintf("<meta http-equiv='Refresh' content='0; url=/goldenPath/help/api.html' />\n");
 hPrintf("</head>\n");
 }
 
 /* Null terminated list of CGI Variables we don't want to save
  * permanently. */
 static char *excludeVars[] = {"Submit", "submit", "sourceSelected", "selectRadio", "ucscGenome", "publicHubs", "clade", NULL,};
 
 int main(int argc, char *argv[])
 /* Process command line. */
 {
 enteredMainTime = clock1000();
 cgiSpoof(&argc, argv);
 verboseTimeInit();
+/* Pre-validate apiKey before the global bot-check.  Without this,
+ * hgBotDelayTimeFrac->getBotCheckString in hg/lib/botDelay.c would
+ * hUserAbort() on an invalid key, which apache renders as a 500 because
+ * it emits plain HTML rather than the JSON the API contract promises. */
+char *earlyApiKey = cgiOptionalString(argApiKey);
+if (isNotEmpty(earlyApiKey))
+    {
+    struct sqlConnection *centralConn = hConnectCentralNoCache();
+    char *userName = hubSpaceUserNameForApiKey(centralConn, earlyApiKey);
+    sqlDisconnect(&centralConn);
+    if (isEmpty(userName))
+        apiErrAbort(err403, err403Msg,
+            "invalid '%s' provided. Make sure the apiKey is valid, or contact us.",
+            argApiKey);
+    }
 /* similar delay system as in DAS server */
 botDelay = hgBotDelayTimeFrac(delayFraction);
 if (botDelay > 0)
     {
     if (botDelay > 2000)
         {
 	sleep1000(botDelay);
 	hogExit();
         return 0;
         }
     sleep1000(botDelay);
     }
 
 setGlobalCgiVars();