Skip to content
This repository was archived by the owner on Apr 20, 2024. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 8 additions & 13 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,24 +1,19 @@
// swift-tools-version:4.2
// swift-tools-version:5.1
import PackageDescription

let package = Package(
name: "Gatekeeper",
platforms: [
.macOS(.v10_14)
],
products: [
.library(
name: "Gatekeeper",
targets: ["Gatekeeper"]),
.library(name: "Gatekeeper", targets: ["Gatekeeper"]),
],
dependencies: [
.package(url: "https://github.com/vapor/vapor.git", from: "3.0.0"),
.package(url: "https://github.com/vapor/vapor.git", from: "4.0.0-beta")
],
targets: [
.target(
name: "Gatekeeper",
dependencies: [
"Vapor"
]),
.testTarget(
name: "GatekeeperTests",
dependencies: ["Gatekeeper"]),
.target(name: "Gatekeeper", dependencies: ["Vapor"]),
.testTarget(name: "GatekeeperTests", dependencies: ["Gatekeeper"]),
]
)
48 changes: 15 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,59 +18,41 @@ It works by adding the clients IP address to the cache and count how many reques
Update your `Package.swift` dependencies:

```swift
.package(url: "https://github.com/nodes-vapor/gatekeeper.git", from: "3.0.0"),
.package(url: "https://github.com/nodes-vapor/gatekeeper.git", from: "4.0.0"),
```

as well as to your target (e.g. "App"):

```swift
targets: [
.target(name: "App", dependencies: [..., "Gatekeeper", ...]),
// ...
targets: [
.target(name: "App", dependencies: [..., "Gatekeeper", ...]),
// ...
]
```

## Getting started 🚀

### Configuration

in configure.swift:
```swift
import Gatekeeper

// [...]

// Register providers first
try services.register(
GatekeeperProvider(
config: GatekeeperConfig(maxRequests: 10, per: .second),
cacheFactory: { container -> KeyedCache in
return try container.make()
}
)
)
```
**Cache**

### Add to routes
You must implement the protocol GateKeeperCache and register it with the application before using GateKeeper

You can add the `GatekeeperMiddleware` to specific routes or to all.

**Specific routes**
in routes.swift:
```swift
let protectedRoutes = router.grouped(GatekeeperMiddleware.self)
protectedRoutes.get("protected/hello") { req in
return "Protected Hello, World!"
}
app.register(GateKeeperCache.self) { (app: Application) -> GateKeeperCache in
return MyGateKeeperCache()
}
```


**For all requests**
in configure.swift:
```swift
// Register middleware
var middlewares = MiddlewareConfig() // Create _empty_ middleware config
middlewares.use(GatekeeperMiddleware.self)
services.register(middlewares)

// Register providers first
let gateKeeperConfig = GatekeeperConfig(maxRequests: 10, per: .second)
app.provider(GatekeeperProvider(config: gateKeeperConfig)(

```

## Credits 🏆
Expand Down
20 changes: 20 additions & 0 deletions Sources/Gatekeeper/GateKeeperCache.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// File.swift
//
//
// Created by Tommy Hinrichsen on 02/12/2019.
//

import Foundation
import Vapor

public protocol GateKeeperCache {

/// Gets key as a decodable type.
func get<D>(_ key: String, as type: D.Type) -> EventLoopFuture<D?> where D: Decodable

/// Sets key to an encodable item.
func set<E>(_ key: String, to entity: E) -> EventLoopFuture<Void> where E: Encodable
}


13 changes: 13 additions & 0 deletions Sources/Gatekeeper/GateKeeperError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// File.swift
//
//
// Created by Tommy Hinrichsen on 04/12/2019.
//

import Foundation

enum GateKeeperError: Swift.Error {
case forbidden
case tooManyRequests
}
63 changes: 22 additions & 41 deletions Sources/Gatekeeper/Gatekeeper.swift
Original file line number Diff line number Diff line change
@@ -1,45 +1,32 @@
import Vapor

public struct Gatekeeper: Service {
public struct Gatekeeper {

internal let config: GatekeeperConfig
internal let cacheFactory: ((Container) throws -> KeyedCache)
internal let cache: GateKeeperCache

public init(
config: GatekeeperConfig,
cacheFactory: @escaping ((Container) throws -> KeyedCache) = { container in try container.make() }
) {
public init(config: GatekeeperConfig, cache: GateKeeperCache) {
self.config = config
self.cacheFactory = cacheFactory
self.cache = cache
}

public func accessEndpoint(
on request: Request
) throws -> Future<Gatekeeper.Entry> {
internal func accessEndpoint(on request: Request) throws -> EventLoopFuture<Gatekeeper.Entry> {

guard let peerHostName = request.http.remotePeer.hostname else {
throw Abort(
.forbidden,
reason: "Unable to verify peer"
)
guard let ipAddress = request.remoteAddress?.ipAddress else {
return request.eventLoop.makeFailedFuture(GateKeeperError.forbidden)
}

let peerCacheKey = cacheKey(for: peerHostName)
let cache = try cacheFactory(request)
let peerCacheKey = self.cacheKey(for: ipAddress)

return cache.get(peerCacheKey, as: Entry.self)
.map(to: Entry.self) { entry in
return self.cache.get(peerCacheKey, as: Entry.self)
.map({ entry -> Gatekeeper.Entry in
if let entry = entry {
return entry
} else {
return Entry(
peerHostname: peerHostName,
createdAt: Date(),
requestsLeft: self.config.limit
)
return Entry(ipAddress: ipAddress, createdAt: Date(), requestsLeft: self.config.limit)
}
}
.map(to: Entry.self) { entry in
})
.map({ entry -> Gatekeeper.Entry in

let now = Date()
var mutableEntry = entry
Expand All @@ -49,28 +36,22 @@ public struct Gatekeeper: Service {
}
mutableEntry.requestsLeft -= 1
return mutableEntry
}.then { entry in
return cache.set(peerCacheKey, to: entry).transform(to: entry)
}.map(to: Entry.self) { entry in

if entry.requestsLeft < 0 {
throw Abort(
.tooManyRequests,
reason: "Slow down. You sent too many requests."
)
}
})
.flatMap( { entry -> EventLoopFuture<Gatekeeper.Entry> in
return self.cache.set(peerCacheKey, to: entry).map { entry }
})
.flatMapThrowing({ entry in
if entry.requestsLeft < 0 { throw GateKeeperError.tooManyRequests }
return entry
}
})
}

private func cacheKey(for hostname: String) -> String {
return "gatekeeper_\(hostname)"
}
private func cacheKey(for hostname: String) -> String { return "gatekeeper_\(hostname)" }
}

extension Gatekeeper {
public struct Entry: Codable {
let peerHostname: String
let ipAddress: String
var createdAt: Date
var requestsLeft: Int
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/Gatekeeper/GatekeeperConfig.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Vapor

public struct GatekeeperConfig: Service {
public struct GatekeeperConfig {

public enum Interval {
case second
Expand Down
19 changes: 6 additions & 13 deletions Sources/Gatekeeper/GatekeeperMiddleware.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,12 @@ public struct GatekeeperMiddleware {
}

extension GatekeeperMiddleware: Middleware {
public func respond(
to request: Request,
chainingTo next: Responder
) throws -> Future<Response> {

return try gatekeeper.accessEndpoint(on: request).flatMap { _ in
return try next.respond(to: request)
public func respond(to request: Request, chainingTo next: Responder) -> EventLoopFuture<Response> {
do {
let response = try gatekeeper.accessEndpoint(on: request).flatMap { _ in return next.respond(to: request) }
return response
} catch {
return request.eventLoop.makeFailedFuture(error)
}
}
}

extension GatekeeperMiddleware: ServiceType {
public static func makeService(for container: Container) throws -> GatekeeperMiddleware {
return try .init(gatekeeper: container.make())
}
}
30 changes: 11 additions & 19 deletions Sources/Gatekeeper/GatekeeperProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,23 @@ import Vapor
public final class GatekeeperProvider {

internal let config: GatekeeperConfig
internal let cacheFactory: ((Container) throws -> KeyedCache)

public init(
config: GatekeeperConfig,
cacheFactory: @escaping ((Container) throws -> KeyedCache) = { container in try container.make() }
) {
public init(config: GatekeeperConfig = GatekeeperConfig(maxRequests: 10, per: .second)) {
self.config = config
self.cacheFactory = cacheFactory
}
}

extension GatekeeperProvider: Provider {
public func register(_ services: inout Services) throws {
services.register(config)
services.register(
Gatekeeper(
config: config,
cacheFactory: cacheFactory
),
as: Gatekeeper.self
)
services.register(GatekeeperMiddleware.self)
}

public func didBoot(_ container: Container) throws -> EventLoopFuture<Void> {
return .done(on: container)
public func register(_ app: Application) {

app.register(extension: MiddlewareConfiguration.self) { (configuration: inout MiddlewareConfiguration, app: Application) in

let cache: GateKeeperCache = app.make()
let gateKeeper = Gatekeeper(config: self.config, cache: cache)
let middleware = GatekeeperMiddleware(gatekeeper: gateKeeper)
configuration.use(middleware)
}
}

}
Loading