Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add HSTSRedirection Middleware #23

Merged
merged 6 commits into from
Jan 5, 2021
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
- name: Run Bionic Tests
run: swift test --enable-test-discovery --enable-code-coverage --sanitize=thread
- name: Setup container for codecov upload
run: apt-get update && apt-get install curl
run: apt-get update && apt-get install curl -y
- name: Process coverage file
run: llvm-cov show .build/x86_64-unknown-linux-gnu/debug/VaporSecurityHeadersPackageTests.xctest -instr-profile=.build/debug/codecov/default.profdata > coverage.txt
- name: Upload code coverage
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Easily add headers to all your responses for improving the security of your site
* X-Frame-Options
* X-Content-Type-Options
* Strict-Transport-Security (HSTS)
* Redirect HTTP to HTTPS
* Server
* Referrer Policy

Expand Down Expand Up @@ -436,6 +437,16 @@ let securityHeadersFactory = SecurityHeadersFactory().with(strictTransportSecuri
strict-transport-security: max-age=31536000; includeSubDomains; preload
```

## Redirect HTTP to HTTPS

If Strict-Transport-Security is not enough to accomplish a forwarding connection to HTTPS from the browsers, you can opt to add an additional middleware who provides this redirection if clients try to reach your site with an HTTP connection.

To use the HTTPS Redirect Middleware, you can add the following line in **configure.swift** to enable the middleware. This must be done before `securityHeadersFactory.build()` to ensure HSTS works:

```swift
app.middleware.use(HTTPSRedirectMiddleware())
```

## Server

The Server header is usually hidden from responses in order to not give away what type of server you are running and what version you are using. This is to stop attackers from scanning your site and using known vulnerabilities against it easily. By default Vapor does not show the server header in responses for this reason.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Vapor

public class HTTPSRedirectMiddleware: Middleware {

public init() {}

public func respond(to request: Request, chainingTo next: Responder) -> EventLoopFuture<Response> {
if request.application.environment == .development {
return next.respond(to: request)
}

let proto = request.headers.first(name: "X-Forwarded-Proto")
?? request.url.scheme
?? "http"

guard proto == "https" else {
guard let host = request.headers.first(name: .host) else {
return request.eventLoop.makeFailedFuture(Abort(.badRequest))
}
let httpsURL = "https://" + host + "\(request.url)"
return request.redirect(to: "\(httpsURL)", type: .permanent).encodeResponse(for: request)
}
return next.respond(to: request)
}
}
86 changes: 86 additions & 0 deletions Tests/VaporSecurityHeadersTests/RedirectionTest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import XCTest

@testable import Vapor

import VaporSecurityHeaders

class RedirectionTest: XCTestCase {

// MARK: - Properties

private var application: Application!
private var eventLoopGroup: EventLoopGroup!
private var request: Request!

override func setUp() {
eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
application = Application(.testing, .shared(eventLoopGroup))
request = Request(application: application, method: .GET, on: eventLoopGroup.next())
}

override func tearDownWithError() throws {
application.shutdown()
try eventLoopGroup.syncShutdownGracefully()
}

func testWithRedirectionMiddleware() throws {
let expectedRedirectStatus: HTTPStatus = HTTPResponseStatus(statusCode: 301, reasonPhrase: "Moved permanently")
request.headers.add(name: .host, value: "localhost:8080")
let responseRedirected = try makeTestResponse(for: request, withRedirection: true)
XCTAssertEqual(expectedRedirectStatus, responseRedirected.status)
}
func testWithoutRedirectionMiddleware() throws {
let expectedNoRedirectStatus: HTTPStatus = HTTPResponseStatus(statusCode: 200, reasonPhrase: "Ok")
request.headers.add(name: .host, value: "localhost:8080")
let response = try makeTestResponse(for: request, withRedirection: false)
XCTAssertEqual(expectedNoRedirectStatus, response.status)
}

func testOnDevelopmentEnvironment() throws {
let expectedStatus: HTTPStatus = HTTPResponseStatus(statusCode: 200, reasonPhrase: "Ok")
request.headers.add(name: .host, value: "localhost:8080")
let response = try makeTestResponse(for: request, withRedirection: true, environment: .development)
XCTAssertEqual(expectedStatus, response.status)
}

func testWithoutHost() throws {
let expectedOutcome: String = "Abort.400: Bad Request"
do {
_ = try makeTestResponse(for: request, withRedirection: true)
} catch (let error) {
XCTAssertEqual(expectedOutcome, error.localizedDescription)
}
}

func testWithProtoSet() throws {
let expectedStatus: HTTPStatus = HTTPResponseStatus(statusCode: 200, reasonPhrase: "Ok")
request.headers.add(name: .xForwardedProto, value: "https")
let response = try makeTestResponse(for: request, withRedirection: true)
XCTAssertEqual(expectedStatus, response.status)
}

private func makeTestResponse(for request: Request, withRedirection: Bool, environment: Environment? = nil) throws -> Response {
application.middleware = Middlewares()
if let environment = environment {
application.environment = environment
}
if withRedirection == true {
application.middleware.use(HTTPSRedirectMiddleware())
}
try routes(application)
return try application.responder.respond(to: request).wait()
}

func routes(_ app: Application) throws {
try app.register(collection: RouteController())
}

struct RouteController: RouteCollection {
func boot(routes: RoutesBuilder) throws {
routes.get(use: testing)
}
func testing(req: Request) throws -> String {
return "Test"
}
}
}