-
Notifications
You must be signed in to change notification settings - Fork 40
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Loading status checks…
feat: add ip deny lists to the sdk
in ip deny lists, we use an open source IP deny list aggregator as a source for blocking IP addresses automatically. We keep a status flag at redis to denote the state. When the feature is disabled, flag is set to disabled with no expiry date. Otherwise, it's either set to a value with some expiry or expired. We use these three states to define what we do: - disabled with no ttl: don't do anything. use list of all deny lists it as it is (with values entered by the user) - set with some ttl: don't do anything. use list of all deny lists it as it is (which includes user values and the ip list) - deleted: this means the ip list must be updated. The result is returned to the user and an async is process is attached to the pending field in our response Mind that the first two states are the same, but we differentiate them to explain what's going on.
Showing
13 changed files
with
750 additions
and
41 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./deny-list" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,201 @@ | ||
// test ip deny list from the highest level, using Ratelimit | ||
import { expect, test, describe, afterAll, beforeEach } from "bun:test"; | ||
import { Ratelimit } from "../index"; | ||
import { Redis } from "@upstash/redis"; | ||
import { DenyListExtension, IpDenyListKey, IpDenyListStatusKey, RatelimitResponse } from "../types"; | ||
import { disableIpDenyList } from "./ip-deny-list"; | ||
|
||
describe("should reject in deny list", async () => { | ||
|
||
const redis = Redis.fromEnv(); | ||
const prefix = `test-integration-prefix`; | ||
const statusKey = [prefix, IpDenyListStatusKey].join(":") | ||
const allDenyListsKey = [prefix, DenyListExtension, "all"].join(":"); | ||
const ipDenyListsKey = [prefix, DenyListExtension, IpDenyListKey].join(":"); | ||
|
||
const ratelimit = new Ratelimit({ | ||
redis, | ||
limiter: Ratelimit.tokenBucket(10, "5 s", 10), | ||
prefix, | ||
enableProtection: true, | ||
denyListThreshold: 8 | ||
}); | ||
|
||
beforeEach(async () => { | ||
await redis.flushdb() | ||
await redis.sadd(allDenyListsKey, "foo"); | ||
}); | ||
|
||
test("should not check deny list when enableProtection: false", async () => { | ||
const ratelimit = new Ratelimit({ | ||
redis, | ||
limiter: Ratelimit.tokenBucket(10, "5 s", 10), | ||
prefix, | ||
enableProtection: false, | ||
denyListThreshold: 8 | ||
}); | ||
|
||
const result = await ratelimit.limit("foo") | ||
expect(result.success).toBeTrue() | ||
|
||
const [status, statusTTL, allSize, ipListsize] = await Promise.all([ | ||
redis.get(statusKey), | ||
redis.ttl(statusKey), | ||
redis.scard(allDenyListsKey), | ||
redis.scard(ipDenyListsKey), | ||
]) | ||
|
||
// no status flag | ||
expect(status).toBe(null) | ||
expect(statusTTL).toBe(-2) | ||
expect(allSize).toBe(1) // foo | ||
expect(ipListsize).toBe(0) | ||
}) | ||
|
||
test("should create ip denylist when enableProtection: true and not disabled", async () => { | ||
const { pending, success } = await ratelimit.limit("foo"); | ||
expect(success).toBeFalse() | ||
await pending; | ||
|
||
const [status, statusTTL, allSize, ipListsize] = await Promise.all([ | ||
redis.get(statusKey), | ||
redis.ttl(statusKey), | ||
redis.scard(allDenyListsKey), | ||
redis.scard(ipDenyListsKey), | ||
]) | ||
|
||
// status flag exists and has ttl | ||
expect(status).toBe("valid") | ||
expect(statusTTL).toBeGreaterThan(1000) | ||
expect(allSize).toBeGreaterThan(0) | ||
expect(ipListsize).toBe(allSize-1) // foo | ||
}) | ||
|
||
test("should not create ip denylist when enableProtection: true but flag is disabled", async () => { | ||
await disableIpDenyList(redis, prefix); | ||
const { pending, success } = await ratelimit.limit("test-user-2"); | ||
expect(success).toBeTrue() | ||
await pending; | ||
|
||
const [status, statusTTL, allSize, ipListsize] = await Promise.all([ | ||
redis.get(statusKey), | ||
redis.ttl(statusKey), | ||
redis.scard(allDenyListsKey), | ||
redis.scard(ipDenyListsKey), | ||
]) | ||
|
||
// no status flag | ||
expect(status).toBe("disabled") | ||
expect(statusTTL).toBe(-1) | ||
expect(allSize).toBe(1) // foo | ||
expect(ipListsize).toBe(0) | ||
}) | ||
|
||
test("should observe that ip denylist is deleted after disabling", async () => { | ||
const { pending, success } = await ratelimit.limit("test-user-3"); | ||
expect(success).toBeTrue() | ||
await pending; | ||
|
||
const [status, statusTTL, allSize, ipListsize] = await Promise.all([ | ||
redis.get(statusKey), | ||
redis.ttl(statusKey), | ||
redis.scard(allDenyListsKey), | ||
redis.scard(ipDenyListsKey), | ||
]) | ||
|
||
// status flag exists and has ttl | ||
expect(status).toBe("valid") | ||
expect(statusTTL).toBeGreaterThan(1000) | ||
expect(allSize).toBeGreaterThan(0) | ||
expect(ipListsize).toBe(allSize-1) // foo | ||
|
||
// DISABLE: called from UI | ||
await disableIpDenyList(redis, prefix); | ||
|
||
// call again | ||
const { pending: newPending } = await ratelimit.limit("test-user"); | ||
await newPending; | ||
|
||
const [newStatus, newStatusTTL, newAllSize, newIpListsize] = await Promise.all([ | ||
redis.get(statusKey), | ||
redis.ttl(statusKey), | ||
redis.scard(allDenyListsKey), | ||
redis.scard(ipDenyListsKey), | ||
]) | ||
|
||
// status flag exists and has ttl | ||
expect(newStatus).toBe("disabled") | ||
expect(newStatusTTL).toBe(-1) | ||
expect(newAllSize).toBe(1) // foo | ||
expect(newIpListsize).toBe(0) | ||
}) | ||
|
||
test("should intialize ip list only once when called consecutively", async () => { | ||
|
||
const requests: RatelimitResponse[] = await Promise.all([ | ||
ratelimit.limit("test-user-X"), | ||
ratelimit.limit("test-user-Y") | ||
]) | ||
|
||
expect(requests[0].success).toBeTrue() | ||
expect(requests[1].success).toBeTrue() | ||
|
||
// wait for both to finish | ||
const result = await Promise.all([ | ||
requests[0].pending, | ||
requests[1].pending | ||
]) | ||
/** | ||
* Result is like this: | ||
* [ | ||
* undefined, | ||
* [ | ||
* undefined, | ||
* [ 1, 0, 74, 74, 75, "OK" ] | ||
* ] | ||
* ] | ||
* | ||
* the first is essentially: | ||
* >> Promise.resolve() | ||
* | ||
* Second one is | ||
* >> Promise.all([Promise.resolve(), updateIpDenyListPromise]) | ||
* | ||
* This means that even though the requests were consecutive, only one was | ||
* allowed to update to update the ip list! | ||
*/ | ||
|
||
// only one undefined | ||
expect(result.filter((value) => value === undefined).length).toBe(1) | ||
|
||
// other response is defined | ||
const definedResponse = result.filter((value) => value !== undefined)[0] as [undefined, any[]] | ||
expect(definedResponse[0]).toBe(undefined) | ||
expect(definedResponse[1].length).toBe(6) | ||
expect(definedResponse[1][1]).toBe(0) // deleting deny list fails because there is none | ||
expect(definedResponse[1][5]).toBe("OK") // setting TTL returns OK | ||
}) | ||
|
||
test("should block ips from ip deny list", async () => { | ||
const { pending, success } = await ratelimit.limit("test-user"); | ||
expect(success).toBeTrue() | ||
await pending; | ||
|
||
const [ip1, ip2] = await redis.srandmember(ipDenyListsKey, 2) as string[] | ||
|
||
const result = await ratelimit.limit("test-user", {ip: ip1}) | ||
expect(result.success).toBeFalse() | ||
expect(result.reason).toBe("denyList") | ||
|
||
await disableIpDenyList(redis, prefix); | ||
|
||
// first one still returns false because it is cached | ||
const newResult = await ratelimit.limit("test-user", {ip: ip1}) | ||
expect(newResult.success).toBeFalse() | ||
expect(newResult.reason).toBe("denyList") | ||
|
||
// other one returns true | ||
const otherResult = await ratelimit.limit("test-user", {ip: ip2}) | ||
expect(otherResult.success).toBeTrue() | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
import { Redis } from "@upstash/redis"; | ||
import { beforeEach, describe, expect, test } from "bun:test"; | ||
import { checkDenyList } from "./deny-list"; | ||
import { disableIpDenyList, updateIpDenyList } from "./ip-deny-list"; | ||
import { DenyListExtension, IpDenyListKey, IpDenyListStatusKey } from "../types"; | ||
|
||
describe("should update ip deny list status", async () => { | ||
const redis = Redis.fromEnv(); | ||
const prefix = `test-ip-list-prefix`; | ||
const allDenyListsKey = [prefix, DenyListExtension, "all"].join(":"); | ||
const ipDenyListsKey = [prefix, DenyListExtension, IpDenyListKey].join(":"); | ||
const statusKey = [prefix, IpDenyListStatusKey].join(":") | ||
|
||
beforeEach(async () => { | ||
await redis.flushdb() | ||
await redis.sadd( | ||
allDenyListsKey, "foo", "bar") | ||
}); | ||
|
||
test("should return invalidIpDenyList: true when empty", async () => { | ||
const { deniedValue, invalidIpDenyList } = await checkDenyList( | ||
redis, prefix, ["foo", "bar"] | ||
) | ||
|
||
expect(deniedValue).toBe("bar") | ||
expect(invalidIpDenyList).toBeTrue() | ||
}) | ||
|
||
test("should return invalidIpDenyList: false when disabled", async () => { | ||
await disableIpDenyList(redis, prefix); | ||
const { deniedValue, invalidIpDenyList } = await checkDenyList( | ||
redis, prefix, ["bar", "foo"] | ||
) | ||
|
||
expect(deniedValue).toBe("foo") | ||
expect(invalidIpDenyList).toBeFalse() | ||
}) | ||
|
||
test("should return invalidIpDenyList: false after updating", async () => { | ||
await updateIpDenyList(redis, prefix, 8); | ||
const { deniedValue, invalidIpDenyList } = await checkDenyList( | ||
redis, prefix, ["whale", "albatros"] | ||
) | ||
|
||
expect(typeof deniedValue).toBe("undefined") | ||
expect(invalidIpDenyList).toBeFalse() | ||
}) | ||
|
||
test("should return invalidIpDenyList: false after updating + disabling", async () => { | ||
|
||
// initial values | ||
expect(await redis.ttl(statusKey)).toBe(-2) | ||
const initialStatus = await redis.get(statusKey) | ||
expect(initialStatus).toBe(null) | ||
|
||
// UPDATE | ||
await updateIpDenyList(redis, prefix, 8); | ||
const { deniedValue, invalidIpDenyList } = await checkDenyList( | ||
redis, prefix, ["user"] | ||
) | ||
|
||
expect(typeof deniedValue).toBe("undefined") | ||
expect(invalidIpDenyList).toBeFalse() | ||
// positive tll on the status key | ||
expect(await redis.ttl(statusKey)).toBeGreaterThan(0) | ||
const status = await redis.get(statusKey) | ||
expect(status).toBe("valid") | ||
|
||
// DISABLE | ||
await disableIpDenyList(redis, prefix); | ||
const { | ||
deniedValue: secondDeniedValue, | ||
invalidIpDenyList: secondInvalidIpDenyList | ||
} = await checkDenyList( | ||
redis, prefix, ["foo", "bar"] | ||
) | ||
|
||
expect(secondDeniedValue).toBe("bar") | ||
expect(secondInvalidIpDenyList).toBeFalse() | ||
// -1 in the status key | ||
expect(await redis.ttl(statusKey)).toBe(-1) | ||
const secondStatus = await redis.get(statusKey) | ||
expect(secondStatus).toBe("disabled") | ||
}) | ||
|
||
test("should handle timeout correctly", async () => { | ||
|
||
await updateIpDenyList(redis, prefix, 8, 5_000); // update with 5 seconds ttl on status flag | ||
const pipeline = redis.multi() | ||
pipeline.smembers(allDenyListsKey) | ||
pipeline.smembers(ipDenyListsKey) | ||
pipeline.get(statusKey) | ||
pipeline.ttl(statusKey) | ||
|
||
const [allValues, ipDenyListValues, status, statusTTL]: [string[], string[], string | null, number] = await pipeline.exec(); | ||
expect(ipDenyListValues.length).toBeGreaterThan(0) | ||
expect(allValues.length).toBe(ipDenyListValues.length + 2) // + 2 for foo and bar | ||
expect(status).toBe("valid") | ||
expect(statusTTL).toBeGreaterThan(2) // ttl is more than 5 seconds | ||
|
||
// wait 6 seconds | ||
await new Promise((r) => setTimeout(r, 6_000)); | ||
|
||
const [newAllValues, newIpDenyListValues, newStatus, newStatusTTL]: [string[], string[], string | null, number] = await pipeline.exec(); | ||
|
||
// deny lists remain as they are | ||
expect(newIpDenyListValues.length).toBeGreaterThan(0) | ||
expect(newAllValues.length).toBe(allValues.length) | ||
expect(newIpDenyListValues.length).toBe(ipDenyListValues.length) | ||
|
||
// status flag is gone | ||
expect(newStatus).toBe(null) | ||
expect(newStatusTTL).toBe(-2) | ||
}, { timeout: 10_000 }) | ||
|
||
test("should overwrite disabled status with updateIpDenyList", async () => { | ||
await disableIpDenyList(redis, prefix); | ||
|
||
const pipeline = redis.multi() | ||
pipeline.smembers(allDenyListsKey) | ||
pipeline.smembers(ipDenyListsKey) | ||
pipeline.get(statusKey) | ||
pipeline.ttl(statusKey) | ||
|
||
const [allValues, ipDenyListValues, status, statusTTL]: [string[], string[], string | null, number] = await pipeline.exec(); | ||
expect(ipDenyListValues.length).toBe(0) | ||
expect(allValues.length).toBe(2) // + 2 for foo and bar | ||
expect(status).toBe("disabled") | ||
expect(statusTTL).toBe(-1) | ||
|
||
// update status: called from UI or from SDK when status key expires | ||
await updateIpDenyList(redis, prefix, 8); | ||
|
||
const [newAllValues, newIpDenyListValues, newStatus, newStatusTTL]: [string[], string[], string | null, number] = await pipeline.exec(); | ||
|
||
// deny lists remain as they are | ||
expect(newIpDenyListValues.length).toBeGreaterThan(0) | ||
expect(newAllValues.length).toBe(newIpDenyListValues.length + 2) | ||
expect(newStatus).toBe("valid") | ||
expect(newStatusTTL).toBeGreaterThan(1000) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
import { DenyListExtension, IpDenyListKey, IpDenyListStatusKey, Redis } from "../types" | ||
import { getIpListTTL } from "./time" | ||
|
||
const baseUrl = "https://raw.githubusercontent.com/stamparm/ipsum/master/levels" | ||
|
||
/** | ||
* Fetches the ips from the ipsum.txt at github | ||
* | ||
* @param threshold ips with less than or equal to the threshold are not included | ||
* @returns list of ips | ||
*/ | ||
const getIpDenyList = async (threshold: number) => { | ||
try { | ||
// Fetch data from the URL | ||
const response = await fetch(`${baseUrl}/${threshold}.txt`) | ||
if (!response.ok) { | ||
throw new Error(`Error fetching data: ${response.statusText}`) | ||
} | ||
const data = await response.text() | ||
|
||
// Process the data | ||
const lines = data.split("\n") | ||
return lines.filter((value) => value.length > 0) // remove empty values | ||
} catch (error) { | ||
throw new Error(`Failed to fetch ip deny list: ${error}`) | ||
} | ||
} | ||
|
||
/** | ||
* Gets the list of ips from the github source which are not in the | ||
* deny list already | ||
* | ||
* @param redis redis instance | ||
* @param prefix ratelimit prefix | ||
* @param threshold ips with less than or equal to the threshold are not included | ||
* @param ttl time to live in milliseconds for the status flag. Optional. If not | ||
* passed, ttl is infferred from current time. | ||
* @returns list of ips which are not in the deny list | ||
*/ | ||
export const updateIpDenyList = async ( | ||
redis: Redis, | ||
prefix: string, | ||
threshold: number, | ||
ttl?: number | ||
) => { | ||
const allIps = await getIpDenyList(threshold) | ||
|
||
const allDenyLists = [prefix, DenyListExtension, "all"].join(":") | ||
const ipDenyList = [prefix, DenyListExtension, IpDenyListKey].join(":") | ||
const statusKey = [prefix, IpDenyListStatusKey].join(":") | ||
|
||
const transaction = redis.multi() | ||
|
||
// remove the old ip deny list from the all set | ||
transaction.sdiffstore(allDenyLists, allDenyLists, ipDenyList) | ||
|
||
// delete the old ip deny list and create new one | ||
transaction.del(ipDenyList) | ||
transaction.sadd(ipDenyList, ...allIps) | ||
|
||
// make all deny list and ip deny list disjoint by removing duplicate | ||
// ones from ip deny list | ||
transaction.sdiffstore(ipDenyList, ipDenyList, allDenyLists) | ||
|
||
// add remaining ips to all list | ||
transaction.sunionstore(allDenyLists, allDenyLists, ipDenyList) | ||
|
||
// set status key with ttl | ||
transaction.set(statusKey, "valid", {px: ttl ?? getIpListTTL()}) | ||
|
||
return await transaction.exec() | ||
} | ||
|
||
export const disableIpDenyList = async (redis: Redis, prefix: string) => { | ||
const allDenyListsKey = [prefix, DenyListExtension, "all"].join(":") | ||
const ipDenyListKey = [prefix, DenyListExtension, IpDenyListKey].join(":") | ||
const statusKey = [prefix, IpDenyListStatusKey].join(":") | ||
|
||
const transaction = redis.multi() | ||
|
||
// remove the old ip deny list from the all set | ||
transaction.sdiffstore(allDenyListsKey, allDenyListsKey, ipDenyListKey) | ||
|
||
// delete the old ip deny list | ||
transaction.del(ipDenyListKey) | ||
|
||
// set to disabled | ||
// this way, the TTL command in checkDenyListScript will return -1. | ||
transaction.set(statusKey, "disabled") | ||
|
||
return await transaction.exec() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import { Redis } from "@upstash/redis"; | ||
import { beforeAll, beforeEach, describe, expect, test } from "bun:test"; | ||
import { DenyListExtension, IpDenyListStatusKey, IsDenied } from "../types"; | ||
import { checkDenyListScript } from "./scripts"; | ||
import { disableIpDenyList, updateIpDenyList } from "./deny-list-update"; | ||
|
||
describe("should manage state correctly", async () => { | ||
const redis = Redis.fromEnv(); | ||
const prefix = `test-script-prefix`; | ||
|
||
const allDenyListsKey = [prefix, DenyListExtension, "all"].join(":"); | ||
const ipDenyListStatusKey = [prefix, IpDenyListStatusKey].join(":"); | ||
|
||
beforeEach(async () => { | ||
await redis.flushdb() | ||
await redis.sadd( | ||
allDenyListsKey, "foo", "bar") | ||
}); | ||
|
||
test("should return status: -2 initially", async () => { | ||
const [isMember, status] = await redis.eval( | ||
checkDenyListScript, | ||
[allDenyListsKey, ipDenyListStatusKey], | ||
["whale", "foo", "bar", "zed"] | ||
) as [IsDenied[], number]; | ||
|
||
expect(isMember).toEqual([0, 1, 1, 0]) | ||
expect(status).toBe(-2) | ||
}) | ||
|
||
test("should return status: -1 when disabled", async () => { | ||
await disableIpDenyList(redis, prefix); | ||
const [isMember, status] = await redis.eval( | ||
checkDenyListScript, | ||
[allDenyListsKey, ipDenyListStatusKey], | ||
["whale", "foo", "bar", "zed"] | ||
) as [IsDenied[], number]; | ||
|
||
expect(isMember).toEqual([0, 1, 1, 0]) | ||
expect(status).toBe(-1) | ||
}) | ||
|
||
test("should return status: number after update", async () => { | ||
await updateIpDenyList(redis, prefix, 8); | ||
const [isMember, status] = await redis.eval( | ||
checkDenyListScript, | ||
[allDenyListsKey, ipDenyListStatusKey], | ||
["foo", "whale", "bar", "zed"] | ||
) as [IsDenied[], number]; | ||
|
||
expect(isMember).toEqual([1, 0, 1, 0]) | ||
expect(status).toBeGreaterThan(1000) | ||
}) | ||
|
||
test("should return status: -1 after update and disable", async () => { | ||
await updateIpDenyList(redis, prefix, 8); | ||
await disableIpDenyList(redis, prefix); | ||
const [isMember, status] = await redis.eval( | ||
checkDenyListScript, | ||
[allDenyListsKey, ipDenyListStatusKey], | ||
["foo", "whale", "bar", "zed"] | ||
) as [IsDenied[], number]; | ||
|
||
expect(isMember).toEqual([1, 0, 1, 0]) | ||
expect(status).toBe(-1) | ||
}) | ||
|
||
test("should only make one of two consecutive requests update deny list", async () => { | ||
|
||
// running the eval script consecutively when the deny list needs | ||
// to be updated. Only one will update the ip list. It will be | ||
// given 30 seconds before its turn expires. Until then, other requests | ||
// will continue using the old ip deny list | ||
const response = await Promise.all([ | ||
redis.eval( | ||
checkDenyListScript, | ||
[allDenyListsKey, ipDenyListStatusKey], | ||
["foo", "whale", "bar", "zed"] | ||
) as Promise<[IsDenied[], number]>, | ||
redis.eval( | ||
checkDenyListScript, | ||
[allDenyListsKey, ipDenyListStatusKey], | ||
["foo", "whale", "bar", "zed"] | ||
) as Promise<[IsDenied[], number]> | ||
]); | ||
|
||
// first request is told that there is no valid ip list (ttl: -2), | ||
// hence it will update the ip deny list | ||
expect(response[0]).toEqual([[1, 0, 1, 0], -2]) | ||
|
||
// second request is told that there is already a valid ip list | ||
// with ttl 30. | ||
expect(response[1]).toEqual([[1, 0, 1, 0], 30]) | ||
|
||
const state = await redis.get(ipDenyListStatusKey) | ||
expect(state).toBe("pending") | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
export const checkDenyListScript = ` | ||
local allDenyListsKey = KEYS[1] | ||
local ipDenyListStatusKey = KEYS[2] | ||
local results = redis.call('SMISMEMBER', allDenyListsKey, unpack(ARGV)) | ||
local status = redis.call('TTL', ipDenyListStatusKey) | ||
-- if status == -1, then deny list is disabled | ||
-- if status == -2, then status flag has expired | ||
if status == -2 then | ||
redis.call('SETEX', ipDenyListStatusKey, 30, "pending") | ||
end | ||
return { results, status } | ||
` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import { getIpListTTL } from './time'; | ||
import { beforeAll, beforeEach, describe, expect, test } from "bun:test"; | ||
|
||
describe('getIpListTTL', () => { | ||
test('returns correct TTL when it is before 2 AM UTC', () => { | ||
const before2AM = Date.UTC(2024, 5, 12, 1, 0, 0); // June 12, 2024, 1:00 AM UTC | ||
const expectedTTL = 1 * 60 * 60 * 1000; // 1 hour in milliseconds | ||
|
||
expect(getIpListTTL(before2AM)).toBe(expectedTTL); | ||
}); | ||
|
||
test('returns correct TTL when it is exactly 2 AM UTC', () => { | ||
const exactly2AM = Date.UTC(2024, 5, 12, 2, 0, 0); // June 12, 2024, 2:00 AM UTC | ||
const expectedTTL = 24 * 60 * 60 * 1000; // 24 hours in milliseconds | ||
|
||
expect(getIpListTTL(exactly2AM)).toBe(expectedTTL); | ||
}); | ||
|
||
test('returns correct TTL when it is after 2 AM UTC but before the next 2 AM UTC', () => { | ||
const after2AM = Date.UTC(2024, 5, 12, 3, 0, 0); // June 12, 2024, 3:00 AM UTC | ||
const expectedTTL = 23 * 60 * 60 * 1000; // 23 hours in milliseconds | ||
|
||
expect(getIpListTTL(after2AM)).toBe(expectedTTL); | ||
}); | ||
|
||
test('returns correct TTL when it is much later in the day', () => { | ||
const laterInDay = Date.UTC(2024, 5, 12, 20, 0, 0); // June 12, 2024, 8:00 PM UTC | ||
const expectedTTL = 6 * 60 * 60 * 1000; // 6 hours in milliseconds | ||
|
||
expect(getIpListTTL(laterInDay)).toBe(expectedTTL); | ||
}); | ||
|
||
test('returns correct TTL when it is exactly the next day', () => { | ||
const nextDay = Date.UTC(2024, 5, 13, 2, 0, 0); // June 13, 2024, 2:00 AM UTC | ||
const expectedTTL = 24 * 60 * 60 * 1000; // 24 hours in milliseconds | ||
|
||
expect(getIpListTTL(nextDay)).toBe(expectedTTL); | ||
}); | ||
|
||
test('returns correct TTL when no time is provided (uses current time)', () => { | ||
const now = Date.now(); | ||
const expectedTTL = getIpListTTL(now); | ||
|
||
expect(getIpListTTL()).toBe(expectedTTL); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
|
||
// Number of milliseconds in one hour | ||
const MILLISECONDS_IN_HOUR = 60 * 60 * 1000; | ||
|
||
// Number of milliseconds in one day | ||
const MILLISECONDS_IN_DAY = 24 * MILLISECONDS_IN_HOUR; | ||
|
||
// Number of milliseconds from the current time to 2 AM UTC | ||
const MILLISECONDS_TO_2AM = 2 * MILLISECONDS_IN_HOUR; | ||
|
||
export const getIpListTTL = (time?: number) => { | ||
const now = time ?? Date.now(); | ||
|
||
// Time since the last 2 AM UTC | ||
const timeSinceLast2AM = (now - MILLISECONDS_TO_2AM) % MILLISECONDS_IN_DAY; | ||
|
||
// Remaining time until the next 2 AM UTC | ||
return MILLISECONDS_IN_DAY - timeSinceLast2AM; | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters