Skip to content
This repository was archived by the owner on Apr 20, 2024. It is now read-only.

Commit 02ec165

Browse files
Merge pull request #21 from madsodgaard/master
Add expiration to cache entry
2 parents a9671f5 + d0a8273 commit 02ec165

File tree

6 files changed

+66
-28
lines changed

6 files changed

+66
-28
lines changed

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ let package = Package(
1212
targets: ["Gatekeeper"]),
1313
],
1414
dependencies: [
15-
.package(url: "https://github.com/vapor/vapor.git", from: "4.38.0"),
15+
.package(url: "https://github.com/vapor/vapor.git", from: "4.44.0"),
1616
],
1717
targets: [
1818
.target(

Sources/Gatekeeper/Gatekeeper+Vapor/Request+Gatekeeper.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Vapor
22

3-
extension Request {
3+
public extension Request {
44
func gatekeeper(
55
config: GatekeeperConfig? = nil,
66
cache: Cache? = nil,

Sources/Gatekeeper/Gatekeeper.swift

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,29 +11,31 @@ public struct Gatekeeper {
1111
self.keyMaker = identifier
1212
}
1313

14-
public func gatekeep(on req: Request) -> EventLoopFuture<Void> {
14+
public func gatekeep(
15+
on req: Request,
16+
throwing error: Error = Abort(.tooManyRequests, reason: "Slow down. You sent too many requests.")
17+
) -> EventLoopFuture<Void> {
1518
keyMaker
1619
.make(for: req)
1720
.flatMap { cacheKey in
1821
fetchOrCreateEntry(for: cacheKey, on: req)
22+
.guard(
23+
{ $0.requestsLeft > 0 },
24+
else: error
25+
)
1926
.map(updateEntry)
2027
.flatMap { entry in
21-
cache
22-
.set(cacheKey, to: entry)
23-
.transform(to: entry)
28+
// The amount of time the entry has existed.
29+
let entryLifetime = Int(Date().timeIntervalSince1970 - entry.createdAt.timeIntervalSince1970)
30+
// Remaining time until the entry expires. The entry would be expired by cache if it was negative.
31+
let timeRemaining = Int(config.refreshInterval) - entryLifetime
32+
return cache.set(cacheKey, to: entry, expiresIn: .seconds(timeRemaining))
2433
}
2534
}
26-
.guard(
27-
{ $0.requestsLeft > 0 },
28-
else: Abort(.tooManyRequests, reason: "Slow down. You sent too many requests."))
29-
.transform(to: ())
3035
}
3136

3237
private func updateEntry(_ entry: Entry) -> Entry {
3338
var newEntry = entry
34-
if newEntry.hasExpired(within: config.refreshInterval) {
35-
newEntry.reset(remainingRequests: config.limit)
36-
}
3739
newEntry.touch()
3840
return newEntry
3941
}

Sources/Gatekeeper/GatekeeperEntry.swift

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,11 @@ extension Gatekeeper.Entry {
1414
Date().timeIntervalSince1970 - createdAt.timeIntervalSince1970 >= interval
1515
}
1616

17-
mutating func reset(remainingRequests: Int) {
18-
createdAt = Date()
19-
requestsLeft = remainingRequests
20-
}
21-
2217
mutating func touch() {
23-
requestsLeft -= 1
18+
if requestsLeft > 0 {
19+
requestsLeft -= 1
20+
} else {
21+
requestsLeft = 0
22+
}
2423
}
2524
}

Sources/Gatekeeper/GatekeeperMiddleware.swift

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,31 @@ import Vapor
33
/// Middleware used to rate-limit a single route or a group of routes.
44
public struct GatekeeperMiddleware: Middleware {
55
private let config: GatekeeperConfig?
6+
private let keyMaker: GatekeeperKeyMaker?
7+
private let error: Error?
68

7-
/// Initialize with a custom `GatekeeperConfig` instead of using the default `app.gatekeeper.config`
8-
public init(config: GatekeeperConfig? = nil) {
9+
/// Initialize a new middleware for rate-limiting routes, by optionally overriding default configurations.
10+
///
11+
/// - Parameters:
12+
/// - config: Override `GatekeeperConfig` instead of using the default `app.gatekeeper.config`
13+
/// - keyMaker: Override `GatekeeperKeyMaker` instead of using the default `app.gatekeeper.keyMaker`
14+
/// - config: Override the `Error` thrown when the user is rate-limited instead of using the default error.
15+
public init(config: GatekeeperConfig? = nil, keyMaker: GatekeeperKeyMaker? = nil, error: Error? = nil) {
916
self.config = config
17+
self.keyMaker = keyMaker
18+
self.error = error
1019
}
1120

1221
public func respond(to request: Request, chainingTo next: Responder) -> EventLoopFuture<Response> {
13-
request
14-
.gatekeeper(config: config)
15-
.gatekeep(on: request)
16-
.flatMap { next.respond(to: request) }
22+
let gatekeeper = request.gatekeeper(config: config, keyMaker: keyMaker)
23+
24+
let gatekeep: EventLoopFuture<Void>
25+
if let error = error {
26+
gatekeep = gatekeeper.gatekeep(on: request, throwing: error)
27+
} else {
28+
gatekeep = gatekeeper.gatekeep(on: request)
29+
}
30+
31+
return gatekeep.flatMap { next.respond(to: request) }
1732
}
1833
}

Tests/GatekeeperTests/GatekeeperTests.swift

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ class GatekeeperTests: XCTestCase {
1212
return .ok
1313
}
1414

15-
for i in 1...10 {
15+
for i in 1...11 {
1616
try app.test(.GET, "test", headers: ["X-Forwarded-For": "::1"], afterResponse: { res in
17-
if i == 10 {
17+
if i == 11 {
1818
XCTAssertEqual(res.status, .tooManyRequests)
1919
} else {
2020
XCTAssertEqual(res.status, .ok, "failed for request \(i) with status: \(res.status)")
@@ -51,7 +51,7 @@ class GatekeeperTests: XCTestCase {
5151
})
5252
}
5353

54-
let entryBefore = try app.gatekeeper.caches.cache .get("gatekeeper_::1", as: Gatekeeper.Entry.self).wait()
54+
let entryBefore = try app.gatekeeper.caches.cache.get("gatekeeper_::1", as: Gatekeeper.Entry.self).wait()
5555
XCTAssertEqual(entryBefore!.requestsLeft, 50)
5656

5757
Thread.sleep(forTimeInterval: 1)
@@ -63,6 +63,28 @@ class GatekeeperTests: XCTestCase {
6363
let entryAfter = try app.gatekeeper.caches.cache .get("gatekeeper_::1", as: Gatekeeper.Entry.self).wait()
6464
XCTAssertEqual(entryAfter!.requestsLeft, 99, "Requests left should've reset")
6565
}
66+
67+
func testGatekeeperCacheExpiry() throws {
68+
let app = Application(.testing)
69+
defer { app.shutdown() }
70+
app.gatekeeper.config = .init(maxRequests: 5, per: .second)
71+
app.grouped(GatekeeperMiddleware()).get("test") { req -> HTTPStatus in
72+
return .ok
73+
}
74+
75+
for _ in 1...5 {
76+
try app.test(.GET, "test", headers: ["X-Forwarded-For": "::1"], afterResponse: { res in
77+
XCTAssertEqual(res.status, .ok)
78+
})
79+
}
80+
81+
let entryBefore = try app.gatekeeper.caches.cache.get("gatekeeper_::1", as: Gatekeeper.Entry.self).wait()
82+
XCTAssertEqual(entryBefore!.requestsLeft, 0)
83+
84+
Thread.sleep(forTimeInterval: 1)
85+
86+
try XCTAssertNil(app.gatekeeper.caches.cache.get("gatekeeper_::1", as: Gatekeeper.Entry.self).wait())
87+
}
6688

6789
func testRefreshIntervalValues() {
6890
let expected: [(GatekeeperConfig.Interval, Double)] = [

0 commit comments

Comments
 (0)