Skip to content

Commit 88704d3

Browse files
committed
improve request validation
motivation: safer handling of request validation and mutation changes: * fix schema logic to be non-case sensitive * made request version, method and url immutable * made request scheme and host internal * adjusted redirect ahndler implementation to stricter request immutabllity * adjust and add tests
1 parent 8fa6c6f commit 88704d3

File tree

3 files changed

+55
-38
lines changed

3 files changed

+55
-38
lines changed

Sources/AsyncHTTPClient/HTTPHandler.swift

Lines changed: 32 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,14 @@ extension HTTPClient {
6060
}
6161

6262
public struct Request {
63-
public var version: HTTPVersion
64-
public var method: HTTPMethod
65-
public var url: URL
66-
public var scheme: String
67-
public var host: String
63+
public let version: HTTPVersion
64+
public let method: HTTPMethod
65+
public let url: URL
6866
public var headers: HTTPHeaders
6967
public var body: Body?
68+
69+
internal let scheme: String
70+
internal let host: String
7071

7172
public init(url: String, version: HTTPVersion = HTTPVersion(major: 1, minor: 1), method: HTTPMethod = .GET, headers: HTTPHeaders = HTTPHeaders(), body: Body? = nil) throws {
7273
guard let url = URL(string: url) else {
@@ -77,7 +78,7 @@ extension HTTPClient {
7778
}
7879

7980
public init(url: URL, version: HTTPVersion = HTTPVersion(major: 1, minor: 1), method: HTTPMethod = .GET, headers: HTTPHeaders = HTTPHeaders(), body: Body? = nil) throws {
80-
guard let scheme = url.scheme else {
81+
guard let scheme = url.scheme?.lowercased() else {
8182
throw HTTPClientError.emptyScheme
8283
}
8384

@@ -99,14 +100,14 @@ extension HTTPClient {
99100
}
100101

101102
public var useTLS: Bool {
102-
return self.url.scheme == "https"
103+
return self.scheme == "https"
103104
}
104105

105106
public var port: Int {
106107
return self.url.port ?? (self.useTLS ? 443 : 80)
107108
}
108109

109-
static func isSchemeSupported(scheme: String?) -> Bool {
110+
static func isSchemeSupported(scheme: String) -> Bool {
110111
return scheme == "http" || scheme == "https"
111112
}
112113
}
@@ -514,7 +515,7 @@ internal struct RedirectHandler<T> {
514515
return nil
515516
}
516517

517-
guard HTTPClient.Request.isSchemeSupported(scheme: url.scheme) else {
518+
guard HTTPClient.Request.isSchemeSupported(scheme: request.scheme) else {
518519
return nil
519520
}
520521

@@ -526,44 +527,38 @@ internal struct RedirectHandler<T> {
526527
}
527528

528529
func redirect(status: HTTPResponseStatus, to redirectURL: URL, promise: EventLoopPromise<T>) {
529-
let originalURL = self.request.url
530-
531-
var request = self.request
532-
request.url = redirectURL
533-
534-
if let redirectHost = redirectURL.host {
535-
request.host = redirectHost
536-
} else {
537-
preconditionFailure("redirectURL doesn't contain a host")
538-
}
539-
540-
if let redirectScheme = redirectURL.scheme {
541-
request.scheme = redirectScheme
542-
} else {
543-
preconditionFailure("redirectURL doesn't contain a scheme")
544-
}
530+
let originalRequest = self.request
545531

546532
var convertToGet = false
547533
if status == .seeOther, request.method != .HEAD {
548534
convertToGet = true
549535
} else if status == .movedPermanently || status == .found, request.method == .POST {
550536
convertToGet = true
551537
}
552-
538+
539+
var method = originalRequest.method
540+
var headers = originalRequest.headers
541+
var body = originalRequest.body
542+
553543
if convertToGet {
554-
request.method = .GET
555-
request.body = nil
556-
request.headers.remove(name: "Content-Length")
557-
request.headers.remove(name: "Content-Type")
544+
method = .GET
545+
body = nil
546+
headers.remove(name: "Content-Length")
547+
headers.remove(name: "Content-Type")
558548
}
559549

560-
if !originalURL.hasTheSameOrigin(as: redirectURL) {
561-
request.headers.remove(name: "Origin")
562-
request.headers.remove(name: "Cookie")
563-
request.headers.remove(name: "Authorization")
564-
request.headers.remove(name: "Proxy-Authorization")
550+
if !originalRequest.url.hasTheSameOrigin(as: redirectURL) {
551+
headers.remove(name: "Origin")
552+
headers.remove(name: "Cookie")
553+
headers.remove(name: "Authorization")
554+
headers.remove(name: "Proxy-Authorization")
555+
}
556+
557+
do {
558+
let request = try HTTPClient.Request(url: redirectURL, version: originalRequest.version, method: method, headers: headers, body: body)
559+
return self.execute(request).futureResult.cascade(to: promise)
560+
} catch {
561+
return promise.fail(error)
565562
}
566-
567-
return self.execute(request).futureResult.cascade(to: promise)
568563
}
569564
}

Tests/AsyncHTTPClientTests/HTTPClientTests+XCTest.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ extension HTTPClientTests {
2626
static var allTests: [(String, (HTTPClientTests) -> () throws -> Void)] {
2727
return [
2828
("testRequestURI", testRequestURI),
29+
("testBadRequestURI", testBadRequestURI),
30+
("testSchemaCasing", testSchemaCasing),
2931
("testGet", testGet),
3032
("testGetWithSharedEventLoopGroup", testGetWithSharedEventLoopGroup),
3133
("testPost", testPost),

Tests/AsyncHTTPClientTests/HTTPClientTests.swift

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15-
import AsyncHTTPClient
15+
@testable import AsyncHTTPClient
1616
import NIO
1717
import NIOFoundationCompat
1818
import NIOHTTP1
@@ -33,6 +33,26 @@ class HTTPClientTests: XCTestCase {
3333
XCTAssertEqual(request2.url.path, "")
3434
}
3535

36+
func testBadRequestURI() throws {
37+
XCTAssertThrowsError(try Request(url: "some/path"), "should throw") { error in
38+
XCTAssertEqual(error as! HTTPClientError, HTTPClientError.emptyScheme)
39+
}
40+
XCTAssertThrowsError(try Request(url: "file://somewhere/some/path?foo=bar"), "should throw") { error in
41+
XCTAssertEqual(error as! HTTPClientError, HTTPClientError.unsupportedScheme("file"))
42+
}
43+
XCTAssertThrowsError(try Request(url: "https:/foo"), "should throw") { error in
44+
XCTAssertEqual(error as! HTTPClientError, HTTPClientError.emptyHost)
45+
}
46+
}
47+
48+
func testSchemaCasing() throws {
49+
let request1 = try Request(url: "https://someserver.com:8888/some/path?foo=bar")
50+
XCTAssertNotNil(request1)
51+
52+
let request2 = try Request(url: "hTTpS://someserver.com:8888/some/path?foo=bar")
53+
XCTAssertNotNil(request2)
54+
}
55+
3656
func testGet() throws {
3757
let httpBin = HttpBin()
3858
let httpClient = HTTPClient(eventLoopGroupProvider: .createNew)

0 commit comments

Comments
 (0)