Skip to content

Commit

Permalink
Merge pull request #84 from d-exclaimation/websocket-security-improve…
Browse files Browse the repository at this point in the history
…ments

HTTP Encoding and WebSocket Security Improvement
  • Loading branch information
d-exclaimation authored Sep 17, 2022
2 parents fab31b7 + 5e013e3 commit f7e4345
Show file tree
Hide file tree
Showing 31 changed files with 300 additions and 88 deletions.
2 changes: 1 addition & 1 deletion Documentation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ You can add Pioneer into any existing Vapor application with any GraphQL schema
Add this line to add Pioneer as one of your dependencies.

```swift
.package(url: "https://github.com/d-exclaimation/pioneer", from: "0.9.4")
.package(url: "https://github.com/d-exclaimation/pioneer", from: "0.10.0")
```

Go to the `main.swift` or any Swift file where you apply your Vapor routing like your `routes.swift` file.
Expand Down
2 changes: 1 addition & 1 deletion Documentation/features/async-await.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ import NIO

extension EventLoop {
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
func makeFutureWithTask<Value>(_ body: @escaping @Sendable () async throws -> Value) -> EventLoopFuture<Value> {
func makeFutureWithTask<Value>(_ body: @Sendable @escaping () async throws -> Value) -> EventLoopFuture<Value> {
let promise = eventLoop.makePromise(of: Value.self)
promise.completeWithTask(body)
return promise.futureResult
Expand Down
4 changes: 4 additions & 0 deletions Documentation/features/graphql-over-http.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,10 @@ Pioneer also provide handler to manually setting routes for WebSocket
[!ref Manual WebSocket Routing](/features/graphql-over-websocket/#manual-websocket-routing)
!!!

!!!success Custom ContentEncoder
Since `v0.10.0`, There is `.httpHandler(req:using:)` method that can take a custom `ContentEncoder`
!!!

```swift
let app = try Application(.detect())
let server = try Pioneer(...)
Expand Down
25 changes: 25 additions & 0 deletions Documentation/guides/advanced/context.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,3 +209,28 @@ struct Resolver {
```

[!ref GraphQLRequest API References](/references/structs/#graphqlrequest)

## WebSocket Initialisation Hook and Authorization

There might be times where you want to authorize any incoming WebSocket connection before any operation done, and thus before the context builder is executed.

Since `v0.10.0`, Pioneer now provide a way to run custom code during the GraphQL over WebSocket initialisation phase that can deny a WebSocket connection by throwing an error.

```swift
let server = Pioneer(
schema: schema,
resolver: Resolver(),
contextBuilder: getContext,
websocketContextBuilder: getWebsocketContext,
websocketOnInit: { payload in
guard .some(.string(let token)) = payload?["Authorization"] {
throw Abort(.unauthorized)
}

// do something with the Authorization token
},
websocketProtocol: .graphqlWs,
introspection: true,
playground: .graphiql
)
```
2 changes: 1 addition & 1 deletion Documentation/guides/getting-started/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ let package = Package(
dependencies: [
.package(url: "https://github.com/GraphQLSwift/Graphiti.git", from: "1.0.0"),
.package(url: "https://github.com/vapor/vapor.git", from: "4.61.1"),
.package(url: "https://github.com/d-exclaimation/pioneer", from: "0.9.4")
.package(url: "https://github.com/d-exclaimation/pioneer", from: "0.10.0")
],
targets: [
.target(
Expand Down
36 changes: 36 additions & 0 deletions Documentation/references/pioneer.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ let server = Pioneer(
| `playground` | [!badge variant="primary" text="IDE"] | Allowing playground <br/> **Default**: `.graphiql` |
| `validationRules` | [!badge variant="primary" text="Validations"] | Validation rules to be applied before operation <br/> **Default**: `.none` |
| `keepAlive` | [!badge variant="warning" text="UInt64?"] | Keep alive internal in nanosecond, `nil` for disabling <br/> **Default**: 12.5 seconds |
| `timeout` | [!badge variant="warning" text="UInt64?"] | Timeout interval in nanosecond, `nil` for disabling <br/> **Default**: 5 seconds |

===

Expand Down Expand Up @@ -88,6 +89,7 @@ let server = Pioneer(
| `playground` | [!badge variant="primary" text="IDE"] | Allowing playground <br/> **Default**: `.graphiql` |
| `validationRules` | [!badge variant="primary" text="Validations"] | Validation rules to be applied before operation <br/> **Default**: `.none` |
| `keepAlive` | [!badge variant="warning" text="UInt64?"] | Keep alive internal in nanosecond, `nil` for disabling <br/> **Default**: 12.5 seconds |
| `timeout` | [!badge variant="warning" text="UInt64?"] | Timeout interval in nanosecond, `nil` for disabling <br/> **Default**: 5 seconds |

===

Expand Down Expand Up @@ -124,6 +126,7 @@ let server = Pioneer(
| `playground` | [!badge variant="primary" text="IDE"] | Allowing playground <br/> **Default**: `.graphiql` |
| `validationRules` | [!badge variant="primary" text="Validations"] | Validation rules to be applied before operation <br/> **Default**: `.none` |
| `keepAlive` | [!badge variant="warning" text="UInt64?"] | Keep alive internal in nanosecond, `nil` for disabling <br/> **Default**: 12.5 seconds |
| `timeout` | [!badge variant="warning" text="UInt64?"] | Timeout interval in nanosecond, `nil` for disabling <br/> **Default**: 5 seconds |

===

Expand Down Expand Up @@ -164,6 +167,7 @@ let server = try Pioneer(
| `playground` | [!badge variant="primary" text="IDE"] | Allowing playground <br/> **Default**: `.graphiql` |
| `validationRules` | [!badge variant="primary" text="Validations"] | Validation rules to be applied before operation <br/> **Default**: `.none` |
| `keepAlive` | [!badge variant="warning" text="UInt64?"] | Keep alive internal in nanosecond, `nil` for disabling <br/> **Default**: 12.5 seconds |
| `timeout` | [!badge variant="warning" text="UInt64?"] | Timeout interval in nanosecond, `nil` for disabling <br/> **Default**: 5 seconds |

===

Expand Down Expand Up @@ -212,6 +216,7 @@ let server = try Pioneer(
| `playground` | [!badge variant="primary" text="IDE"] | Allowing playground <br/> **Default**: `.graphiql` |
| `validationRules` | [!badge variant="primary" text="Validations"] | Validation rules to be applied before operation <br/> **Default**: `.none` |
| `keepAlive` | [!badge variant="warning" text="UInt64?"] | Keep alive internal in nanosecond, `nil` for disabling <br/> **Default**: 12.5 seconds |
| `timeout` | [!badge variant="warning" text="UInt64?"] | Timeout interval in nanosecond, `nil` for disabling <br/> **Default**: 5 seconds |

===

Expand Down Expand Up @@ -284,6 +289,37 @@ app.post("/manual") { req async throws in

---

### `httpHandler`

Common Handler for GraphQL through HTTP using custom `ContentEncoder`

!!!info Manually handling request
If you use [`applyMiddleware`](#applymiddleware), this function is already in use and does not need to be called.

However, you can opt out of [`applyMiddleware`](#applymiddleware), manually set your HTTP routes, and use this method to handle GraphQL request
!!!

=== Example

```swift
app.post("/manual") { req async throws in
try await server.httpHandler(req: req, using: JSONEncoder())
}
```

===

==- Options

| Name | Type | Description |
| ----- | ----------------------------------------- | --------------------------- |
| `req` | [!badge variant="primary" text="Request"] | The HTTP request being made |
| `encoder` | [!badge variant="warning" text="ContentEncoder"] | The custom content encoder |

===

---

### `webSocketHandler`

Upgrade Handler for all GraphQL through Websocket
Expand Down
13 changes: 11 additions & 2 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,15 @@
"version": "1.0.0"
}
},
{
"package": "swift-atomics",
"repositoryURL": "https://github.com/apple/swift-atomics.git",
"state": {
"branch": null,
"revision": "919eb1d83e02121cdb434c7bfc1f0c66ef17febe",
"version": "1.0.2"
}
},
{
"package": "swift-backtrace",
"repositoryURL": "https://github.com/swift-server/swift-backtrace.git",
Expand Down Expand Up @@ -195,8 +204,8 @@
"repositoryURL": "https://github.com/vapor/websocket-kit.git",
"state": {
"branch": null,
"revision": "09212f4c2b9ebdef00f04b913b57f5d77bc4ea62",
"version": "2.4.1"
"revision": "2d9d2188a08eef4a869d368daab21b3c08510991",
"version": "2.6.1"
}
}
]
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Pioneer is an open-source Swift GraphQL server for [Vapor](https://github.com/va
## Setup

```swift
.package(url: "https://github.com/d-exclaimation/pioneer", from: "0.9.4")
.package(url: "https://github.com/d-exclaimation/pioneer", from: "0.10.0")
```

## Swift for GraphQL
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ extension Actor {
/// - Parameters:
/// - future: EventLoopFuture value being awaited
/// - to: Transforming callback to for the result from the Future.
public func pipeToSelf<U>(future: EventLoopFuture<U>, to callback: @escaping @Sendable (Self, Result<U, Error>) async -> Void) {
public func pipeToSelf<U>(future: EventLoopFuture<U>, to callback: @Sendable @escaping (Self, Result<U, Error>) async -> Void) {
Task {
do {
let res = try await future.get()
Expand Down
9 changes: 6 additions & 3 deletions Sources/Pioneer/Extensions/Pioneer+Default.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,18 @@ public extension Pioneer {
/// - playground: Allowing playground
/// - validationRules: Validation rules to be applied before operation
/// - keepAlive: Keep alive internal in nanosecond, default to 12.5 sec, nil for disable
/// - timeout: Timeout interval in nanosecond, default to 5 sec, nil for disable
init(
schema: GraphQLSchema,
resolver: Resolver,
contextBuilder: @escaping @Sendable (Request, Response) async throws -> Context,
contextBuilder: @Sendable @escaping (Request, Response) async throws -> Context,
httpStrategy: HTTPStrategy = .queryOnlyGet,
websocketProtocol: WebsocketProtocol = .graphqlWs,
introspection: Bool = true,
playground: IDE = .graphiql,
validationRules: Validations = .none,
keepAlive: UInt64? = 12_500_000_000
keepAlive: UInt64? = 12_500_000_000,
timeout: UInt64? = 5_000_000_000
) {
self.init(
schema: schema,
Expand All @@ -46,7 +48,8 @@ public extension Pioneer {
introspection: introspection,
playground: playground,
validationRules: validationRules,
keepAlive: keepAlive
keepAlive: keepAlive,
timeout: timeout
)
}
}
15 changes: 10 additions & 5 deletions Sources/Pioneer/Extensions/Pioneer+Graphiti.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public extension Pioneer {
init(
schema: Schema<Resolver, Context>,
resolver: Resolver,
contextBuilder: @escaping @Sendable (Request, Response) async throws -> Context,
contextBuilder: @Sendable @escaping (Request, Response) async throws -> Context,
httpStrategy: HTTPStrategy = .queryOnlyGet,
websocketProtocol: WebsocketProtocol = .graphqlWs,
introspection: Bool = true,
Expand Down Expand Up @@ -57,22 +57,26 @@ public extension Pioneer {
/// - contextBuilder: Context builder from request
/// - httpStrategy: HTTP strategy
/// - websocketContextBuilder: Context builder for the websocket
/// - websocketOnInit: Function to intercept websocket connection during the initialization phase
/// - websocketProtocol: Websocket sub-protocol
/// - introspection: Allowing introspection
/// - playground: Allowing playground
/// - validationRules: Validation rules to be applied before operation
/// - keepAlive: Keep alive internal in nanosecond, default to 12.5 sec, nil for disable
/// - timeout: Timeout interval in nanosecond, default to 5 sec, nil for disable
init(
schema: Schema<Resolver, Context>,
resolver: Resolver,
contextBuilder: @escaping @Sendable (Request, Response) async throws -> Context,
contextBuilder: @Sendable @escaping (Request, Response) async throws -> Context,
httpStrategy: HTTPStrategy = .queryOnlyGet,
websocketContextBuilder: @escaping @Sendable (Request, ConnectionParams, GraphQLRequest) async throws -> Context,
websocketContextBuilder: @Sendable @escaping (Request, ConnectionParams, GraphQLRequest) async throws -> Context,
websocketOnInit: @Sendable @escaping (ConnectionParams) async throws -> Void = { _ in },
websocketProtocol: WebsocketProtocol = .graphqlWs,
introspection: Bool = true,
playground: IDE = .graphiql,
validationRules: Validations = .none,
keepAlive: UInt64? = 12_500_000_000
keepAlive: UInt64? = 12_500_000_000,
timeout: UInt64? = 5_000_000_000
) {
self.init(
schema: schema.schema,
Expand All @@ -84,7 +88,8 @@ public extension Pioneer {
introspection: introspection,
playground: playground,
validationRules: validationRules,
keepAlive: keepAlive
keepAlive: keepAlive,
timeout: timeout
)
}
}
7 changes: 5 additions & 2 deletions Sources/Pioneer/Extensions/Pioneer+RequestContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public extension Pioneer where Context == Void {
/// - playground: Allowing playground
/// - validationRules: Validation rules to be applied before operation
/// - keepAlive: Keep alive internal in nanosecond, default to 12.5 sec, nil for disable
/// - timeout: Timeout interval in nanosecond, default to 5 sec, nil for disable
init(
schema: Schema<Resolver, Void>,
resolver: Resolver,
Expand All @@ -25,7 +26,8 @@ public extension Pioneer where Context == Void {
introspection: Bool = true,
playground: IDE = .graphiql,
validationRules: Validations = .none,
keepAlive: UInt64? = 12_500_000_000
keepAlive: UInt64? = 12_500_000_000,
timeout: UInt64? = 5_000_000_000
) {
self.init(
schema: schema.schema,
Expand All @@ -37,7 +39,8 @@ public extension Pioneer where Context == Void {
introspection: introspection,
playground: playground,
validationRules: validationRules,
keepAlive: keepAlive
keepAlive: keepAlive,
timeout: timeout
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public extension Request {
func defaultWebsocketContextBuilder<Context>(
payload: ConnectionParams,
gql: GraphQLRequest,
contextBuilder: @escaping @Sendable (Request, Response) async throws -> Context
contextBuilder: @Sendable @escaping (Request, Response) async throws -> Context
) async throws -> Context {
let uri = URI(
scheme: url.scheme,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//
// GraphQLJSONEncoder+ContentEncoder.swift
// pioneer
//
// Created by d-exclaimation on 11:30.
//

import Vapor
import class GraphQL.GraphQLJSONEncoder

extension GraphQLJSONEncoder: ContentEncoder {
public func encode<E>(_ encodable: E, to body: inout NIOCore.ByteBuffer, headers: inout NIOHTTP1.HTTPHeaders) throws where E : Encodable {
headers.contentType = .json
try body.writeBytes(self.encode(encodable))
}
}
16 changes: 13 additions & 3 deletions Sources/Pioneer/Http/Pioneer+Http.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Vapor
import enum GraphQL.OperationType
import enum GraphQL.Map
import struct GraphQL.GraphQLError
import class GraphQL.GraphQLJSONEncoder

extension Pioneer {
/// Apply middleware for `POST`
Expand All @@ -32,6 +33,15 @@ extension Pioneer {
/// - Parameter req: The HTTP request being made
/// - Returns: A response from the GraphQL operation execution properly formatted
public func httpHandler(req: Request) async throws -> Response {
try await httpHandler(req: req, using: GraphQLJSONEncoder())
}

/// Common Handler for GraphQL through HTTP
/// - Parameters:
/// - req: The HTTP request being made
/// - using: The custom content encoder
/// - Returns: A response from the GraphQL operation execution properly formatted
public func httpHandler(req: Request, using encoder: ContentEncoder) async throws -> Response {
// Check for CSRF Prevention
guard isCSRFProtected(isActive: httpStrategy == .csrfPrevention, on: req) else {
return try GraphQLError(
Expand All @@ -42,7 +52,7 @@ extension Pioneer {
}
do {
let gql = try req.graphql
return try await handle(req: req, from: gql, allowing: httpStrategy.allowed(for: req.method))
return try await handle(req: req, from: gql, allowing: httpStrategy.allowed(for: req.method), using: encoder)
} catch let error as Abort {
return try GraphQLError(message: error.reason).response(with: error.status)
} catch {
Expand All @@ -56,7 +66,7 @@ extension Pioneer {
/// - gql: The GraphQL request for the operation
/// - allowing: The allowed operation type
/// - Returns: A response with proper http status code and a well formatted body
internal func handle(req: Request, from gql: GraphQLRequest, allowing: [OperationType]) async throws -> Response {
internal func handle(req: Request, from gql: GraphQLRequest, allowing: [OperationType], using encoder: ContentEncoder) async throws -> Response {
guard allowed(from: gql, allowing: allowing) else {
return try GraphQLError(message: "Operation of this type is not allowed and has been blocked")
.response(with: .badRequest)
Expand All @@ -70,7 +80,7 @@ extension Pioneer {
do {
let context = try await contextBuilder(req, res)
let result = await executeOperation(for: gql, with: context, using: req.eventLoop)
try res.content.encode(result)
try res.content.encode(result, using: encoder)
return res
} catch let error as AbortError {
return try error.response(using: res)
Expand Down
Loading

0 comments on commit f7e4345

Please sign in to comment.