19e534539738dc80225bff27bda1f1835dc5b3d3
hiram
  Mon May 11 15:09:10 2026 -0700
rate limit the lifrt requests to "liftDailyLimi" setting from hg.conf refs #31811

diff --git src/hg/hubApi/liftOver.c src/hg/hubApi/liftOver.c
index c39da3f64db..04edc9e3aad 100644
--- src/hg/hubApi/liftOver.c
+++ src/hg/hubApi/liftOver.c
@@ -71,35 +71,43 @@
 
 static void listExisting()
 /* output the fromDb,toDb from liftOverChain.hgcentral SQL table */
 {
 char *filter = cgiOptionalString(argFilter);
 char *fromDb = cgiOptionalString(argFromGenome);
 char *toDb = cgiOptionalString(argToGenome);
 
 struct sqlConnection *conn = hConnectCentral();
 char *tableName = cloneString(liftOverChainTable());
 struct dyString *query = newDyString(0);
 sqlDyStringPrintf(query, "SELECT count(*) FROM %s", tableName);
 long long totalRows = sqlQuickLongLong(conn, dyStringContents(query));
 dyStringClear(query);
 
-if (isNotEmpty(fromDb) || isNotEmpty(toDb))
+if (isNotEmpty(fromDb) && isNotEmpty(toDb))
+    {
+    /* match a chain recorded in either direction */
+    sqlDyStringPrintf(query, "SELECT * FROM %s WHERE "
+        "(LOWER(fromDb) = LOWER('%s') AND LOWER(toDb) = LOWER('%s')) "
+        "OR (LOWER(fromDb) = LOWER('%s') AND LOWER(toDb) = LOWER('%s'))",
+        tableName, fromDb, toDb, toDb, fromDb);
+    }
+else if (isNotEmpty(fromDb) || isNotEmpty(toDb))
     {
     sqlDyStringPrintf(query, "SELECT * FROM %s WHERE ", tableName);
     if (isNotEmpty(fromDb))
-        sqlDyStringPrintf(query, "LOWER(fromDb) = LOWER('%s') %s ", fromDb, isNotEmpty(toDb) ? "AND" : "");
+        sqlDyStringPrintf(query, "LOWER(fromDb) = LOWER('%s') ", fromDb);
     if (isNotEmpty(toDb))
         sqlDyStringPrintf(query, "LOWER(toDb) = LOWER('%s') ", toDb);
     }
 else if (isNotEmpty(filter))
     {
     sqlDyStringPrintf(query, "SELECT * FROM %s WHERE LOWER(fromDb) = LOWER('%s') OR LOWER(toDb) = LOWER('%s')", tableName, filter, filter);
     }
 else
     {
     sqlDyStringPrintf(query, "SELECT * FROM %s", tableName);
     }
 sqlDyStringPrintf(query, " LIMIT %d;", maxItemsOutput);
 
 char *dataTime = sqlTableUpdate(conn, tableName);
 time_t dataTimeStamp = sqlDateToUnixTime(dataTime);
@@ -289,30 +297,60 @@
 char *toGenome = cgiOptionalString(argToGenome);
 char *email = cgiOptionalString(argEmail);
 char *comment = cgiOptionalString(argComment);
 
 /* probably want a silent exit here */
 if (isEmpty(fromGenome) || isEmpty(toGenome) || isEmpty(email) || isEmpty(comment))
     apiErrAbort(err400, err400Msg, "must have all arguments: %s, %s, %s, %s for endpoint '/liftRequest", argFromGenome, argToGenome, argEmail, argComment);
 
 /* Require a session cookie.  Robots that have not
  *   passed the challenge will not have one. */
 char *cookieName = hUserCookie();
 char *userId = findCookieData(cookieName);
 if (isEmpty(userId))
     apiErrAbort(err400, err400Msg, "can not find required inputs for endpoint '/liftRequest");
 
+/* per-email daily rate limit, per requestType, calendar-day server time */
+char *limitStr = cfgOption("liftDailyLimit");
+int dailyLimit = isNotEmpty(limitStr) ? atoi(limitStr) : 0;
+if (dailyLimit > 0)
+    {
+    char *limitOttoTable = cfgOption("ottoTable");
+    if (isNotEmpty(limitOttoTable))
+        {
+        struct sqlConnection *conn = hConnectCentral();
+        if (sqlTableExists(conn, limitOttoTable))
+            {
+            struct dyString *q = newDyString(0);
+            sqlDyStringPrintf(q,
+                "SELECT COUNT(*) FROM %s "
+                "WHERE requestType='liftOver' AND email='%s' "
+                "AND DATE(requestTime) = CURDATE()",
+                limitOttoTable, email);
+            int todayCount = sqlQuickNum(conn, dyStringCannibalize(&q));
+            hDisconnectCentral(&conn);
+            if (todayCount >= dailyLimit)
+                apiErrAbort(err429, err429Msg,
+                    "Daily limit reached: %d liftOver requests per day. "
+                    " Please try again tomorrow.",
+                    dailyLimit);
+            }
+        else
+            hDisconnectCentral(&conn);
+        }
+    }
+
 char *toAddr = cfgOption("chainFileRequestEmail");
 char *fromAddr = cfgOption("apiFromEmail");
 
 if (isNotEmpty(toAddr) && isNotEmpty(fromAddr))
     {
     char nowTime[256];
     time_t seconds = clock1();
     struct tm *timeNow = localtime(&seconds);
     strftime(nowTime, sizeof nowTime, "%Y-%m-%d %H:%M:%S", timeNow);
 
     struct dyString *msg = newDyString(0);
     /* may need to encode these inputs to make them safe */
     dyStringPrintf(msg, "%s\nLift over request\nfrom: %s\nto: %s\nemail '%s'\ncomment: '%s'", nowTime, fromGenome, toGenome, email, comment);
     /* Even if the mailViaPipe returned a relevant return code, and I'm not
     *    sure it would, there isn't much we can do about it from here.