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/blatServers.c src/hg/lib/blatServers.c
index d1c23eee321..067b9ec475b 100644
--- src/hg/lib/blatServers.c
+++ src/hg/lib/blatServers.c
@@ -1,23 +1,26 @@
 /* blatServers.c was originally generated by the autoSql program, which also 
  * generated blatServers.h and blatServers.sql.  This module links the database and
  * the RAM representation of objects. */
 
 #include "common.h"
 #include "linefile.h"
 #include "dystring.h"
 #include "jksql.h"
+#include "hdb.h"
+#include "hgConfig.h"
+#include "trackHub.h"
 #include "blatServers.h"
 
 
 
 char *blatServersCommaSepFieldNames = "db,host,port,isTrans,canPcr,dynamic";
 
 void blatServersStaticLoad(char **row, struct blatServers *ret)
 /* Load a row from blatServers table into ret.  The contents of ret will
  * be replaced at the next call to this function. */
 {
 
 ret->db = row[0];
 ret->host = row[1];
 ret->port = sqlSigned(row[2]);
 ret->isTrans = sqlSigned(row[3]);
@@ -132,15 +135,97 @@
 fprintf(f, "%s", el->host);
 if (sep == ',') fputc('"',f);
 fputc(sep,f);
 fprintf(f, "%d", el->port);
 fputc(sep,f);
 fprintf(f, "%d", el->isTrans);
 fputc(sep,f);
 fprintf(f, "%d", el->canPcr);
 fputc(sep,f);
 fprintf(f, "%d", el->dynamic);
 fputc(lastSep,f);
 }
 
 /* -------------------------------- End autoSql Generated Code -------------------------------- */
 
+struct blatServerParams *findBlatServer(char *db, boolean isTrans)
+/* Return gfServer connection parameters for the given assembly, or NULL if
+ * none is configured.  Handles both hub and non-hub assemblies. */
+{
+struct blatServerParams *st;
+AllocVar(st);
+
+if (trackHubDatabase(db))
+    {
+    char *host = NULL, *port = NULL, *genomeDataDir = NULL;
+    if (!trackHubGetBlatParams(db, isTrans, &host, &port, &genomeDataDir))
+        {
+        freez(&st);
+        return NULL;
+        }
+    st->db = cloneString(db);
+    st->genome = cloneString(hGenome(db));
+    st->host = host;
+    st->port = port;
+    st->isTrans = isTrans;
+    struct trackHubGenome *hg = trackHubGetGenome(db);
+    st->nibDir = cloneString(hg->twoBitPath);
+    char *slash = strrchr(st->nibDir, '/');
+    if (slash != NULL)
+        *slash = 0;
+    if (genomeDataDir != NULL)
+        {
+        st->isDynamic = TRUE;
+        st->genomeDataDir = cloneString(genomeDataDir);
+        }
+    return st;
+    }
+
+struct sqlConnection *conn = hConnectCentral();
+char query[512];
+char dbActualName[64];
+
+/* Accept either a db name or a description (hgBlat allows both). */
+sqlSafef(query, sizeof(query), "select name from dbDb where name = '%s'", db);
+if (!sqlExists(conn, query))
+    {
+    sqlSafef(query, sizeof(query),
+        "select name from dbDb where description = '%s'", db);
+    if (sqlQuickQuery(conn, query, dbActualName, sizeof(dbActualName)) != NULL)
+        db = dbActualName;
+    }
+
+char *blatServersTbl = cfgOptionDefault("blatServersTbl", "blatServers");
+boolean haveDynamic = sqlColumnExists(conn, blatServersTbl, "dynamic");
+sqlSafef(query, sizeof(query),
+    "select dbDb.name, dbDb.description, blatServers.host, blatServers.port, "
+    "dbDb.nibPath, %s "
+    "from dbDb, %s blatServers "
+    "where blatServers.isTrans = %d and dbDb.name = '%s' "
+    "and dbDb.name = blatServers.db",
+    (haveDynamic ? "blatServers.dynamic" : "0"),
+    blatServersTbl, isTrans, db);
+struct sqlResult *sr = sqlGetResult(conn, query);
+char **row = sqlNextRow(sr);
+if (row == NULL)
+    {
+    sqlFreeResult(&sr);
+    hDisconnectCentral(&conn);
+    freez(&st);
+    return NULL;
+    }
+st->db = cloneString(row[0]);
+st->genome = cloneString(row[1]);
+st->host = cloneString(row[2]);
+st->port = cloneString(row[3]);
+st->nibDir = hReplaceGbdbSeqDir(row[4], st->db);
+st->isTrans = isTrans;
+if (atoi(row[5]))
+    {
+    st->isDynamic = TRUE;
+    st->genomeDataDir = cloneString(st->db);
+    }
+sqlFreeResult(&sr);
+hDisconnectCentral(&conn);
+return st;
+}
+