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(¢ralConn); + 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();