diff --git a/Sources/Vapor/HTTP/Headers/HTTPHeaders+Directive.swift b/Sources/Vapor/HTTP/Headers/HTTPHeaders+Directive.swift index 576390a0ba..c7f0eb1c75 100644 --- a/Sources/Vapor/HTTP/Headers/HTTPHeaders+Directive.swift +++ b/Sources/Vapor/HTTP/Headers/HTTPHeaders+Directive.swift @@ -177,7 +177,11 @@ extension HTTPHeaders { for directive in directives { let string: String if let parameter = directive.parameter { - string = "\(directive.value)=\(parameter)" + if self.shouldQuote(parameter) { + string = "\(directive.value)=\"\(parameter.escapingDoubleQuotes())\"" + } else { + string = "\(directive.value)=\(parameter)" + } } else { string = .init(directive.value) } @@ -188,13 +192,19 @@ extension HTTPHeaders { return main.joined(separator: ", ") } + + private func shouldQuote(_ parameter: Substring) -> Bool { + parameter.contains(where: { + $0 == .space || $0 == .doubleQuote + }) + } } } private extension Substring { /// Converts all `\"` to `"`. func unescapingDoubleQuotes() -> Substring { - return self.lazy.split(separator: "\\").reduce(into: "") { (result, part) in + self.lazy.split(separator: "\\").reduce(into: "") { (result, part) in if result.isEmpty || part.first == "\"" { result += part } else { @@ -202,6 +212,11 @@ private extension Substring { } } } + + /// Converts all `"` to `\"`. + func escapingDoubleQuotes() -> String { + self.split(separator: "\"").joined(separator: "\\\"") + } } @@ -227,6 +242,9 @@ private extension Character { static var period: Self { .init(".") } + static var space: Self { + .init(" ") + } var isDirectiveKey: Bool { self.isLetter || self.isNumber || self == .dash || self == .underscore || self == .period diff --git a/Tests/VaporTests/HTTPHeaderTests.swift b/Tests/VaporTests/HTTPHeaderTests.swift index 26c14ee0cc..42b0bade70 100644 --- a/Tests/VaporTests/HTTPHeaderTests.swift +++ b/Tests/VaporTests/HTTPHeaderTests.swift @@ -1,7 +1,7 @@ @testable import Vapor import XCTest -final class HTTPHeaderValueTests: XCTestCase { +final class HTTPHeaderTests: XCTestCase { func testValue() throws { var parser = HTTPHeaders.DirectiveParser(string: "foobar") XCTAssertEqual(parser.nextDirectives(), [.init(value: "foobar")]) @@ -196,4 +196,15 @@ final class HTTPHeaderValueTests: XCTestCase { let upper = HTTPMediaType(type: "foo", subType: "BAR") XCTAssertEqual(lower, upper) } + + // https://github.com/vapor/vapor/issues/2439 + func testContentDispositionQuotedFilename() throws { + var headers = HTTPHeaders() + headers.contentDisposition = .init(.formData, filename: "foo") + XCTAssertEqual(headers.first(name: .contentDisposition), "form-data; filename=foo") + headers.contentDisposition = .init(.formData, filename: "foo bar") + XCTAssertEqual(headers.first(name: .contentDisposition), #"form-data; filename="foo bar""#) + headers.contentDisposition = .init(.formData, filename: "foo\"bar") + XCTAssertEqual(headers.first(name: .contentDisposition), #"form-data; filename="foo\"bar""#) + } }