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,146 +1,231 @@
 /* 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]);
 ret->canPcr = sqlSigned(row[4]);
 ret->dynamic = sqlSigned(row[5]);
 }
 
 struct blatServers *blatServersLoad(char **row)
 /* Load a blatServers from row fetched with select * from blatServers
  * from database.  Dispose of this with blatServersFree(). */
 {
 struct blatServers *ret;
 
 AllocVar(ret);
 ret->db = cloneString(row[0]);
 ret->host = cloneString(row[1]);
 ret->port = sqlSigned(row[2]);
 ret->isTrans = sqlSigned(row[3]);
 ret->canPcr = sqlSigned(row[4]);
 ret->dynamic = sqlSigned(row[5]);
 return ret;
 }
 
 struct blatServers *blatServersLoadAll(char *fileName) 
 /* Load all blatServers from a whitespace-separated file.
  * Dispose of this with blatServersFreeList(). */
 {
 struct blatServers *list = NULL, *el;
 struct lineFile *lf = lineFileOpen(fileName, TRUE);
 char *row[6];
 
 while (lineFileRow(lf, row))
     {
     el = blatServersLoad(row);
     slAddHead(&list, el);
     }
 lineFileClose(&lf);
 slReverse(&list);
 return list;
 }
 
 struct blatServers *blatServersLoadAllByChar(char *fileName, char chopper) 
 /* Load all blatServers from a chopper separated file.
  * Dispose of this with blatServersFreeList(). */
 {
 struct blatServers *list = NULL, *el;
 struct lineFile *lf = lineFileOpen(fileName, TRUE);
 char *row[6];
 
 while (lineFileNextCharRow(lf, chopper, row, ArraySize(row)))
     {
     el = blatServersLoad(row);
     slAddHead(&list, el);
     }
 lineFileClose(&lf);
 slReverse(&list);
 return list;
 }
 
 struct blatServers *blatServersCommaIn(char **pS, struct blatServers *ret)
 /* Create a blatServers out of a comma separated string. 
  * This will fill in ret if non-null, otherwise will
  * return a new blatServers */
 {
 char *s = *pS;
 
 if (ret == NULL)
     AllocVar(ret);
 ret->db = sqlStringComma(&s);
 ret->host = sqlStringComma(&s);
 ret->port = sqlSignedComma(&s);
 ret->isTrans = sqlSignedComma(&s);
 ret->canPcr = sqlSignedComma(&s);
 ret->dynamic = sqlSignedComma(&s);
 *pS = s;
 return ret;
 }
 
 void blatServersFree(struct blatServers **pEl)
 /* Free a single dynamically allocated blatServers such as created
  * with blatServersLoad(). */
 {
 struct blatServers *el;
 
 if ((el = *pEl) == NULL) return;
 freeMem(el->db);
 freeMem(el->host);
 freez(pEl);
 }
 
 void blatServersFreeList(struct blatServers **pList)
 /* Free a list of dynamically allocated blatServers's */
 {
 struct blatServers *el, *next;
 
 for (el = *pList; el != NULL; el = next)
     {
     next = el->next;
     blatServersFree(&el);
     }
 *pList = NULL;
 }
 
 void blatServersOutput(struct blatServers *el, FILE *f, char sep, char lastSep) 
 /* Print out blatServers.  Separate fields with sep. Follow last field with lastSep. */
 {
 if (sep == ',') fputc('"',f);
 fprintf(f, "%s", el->db);
 if (sep == ',') fputc('"',f);
 fputc(sep,f);
 if (sep == ',') fputc('"',f);
 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;
+}
+