Percent decode the URI for the static resource requests (#491)
This commit is contained in:
parent
3e08f7d793
commit
a18bcb18b9
|
@ -31,6 +31,10 @@ final class ServerHTTPHandler: ChannelInboundHandler, RemovableChannelHandler {
|
|||
let serverName: String
|
||||
}
|
||||
|
||||
struct ServerError: Error, CustomStringConvertible {
|
||||
let description: String
|
||||
}
|
||||
|
||||
let configuration: Configuration
|
||||
private var responseBody: ByteBuffer!
|
||||
|
||||
|
@ -52,22 +56,33 @@ final class ServerHTTPHandler: ChannelInboundHandler, RemovableChannelHandler {
|
|||
guard case .head(let head) = reqPart else {
|
||||
return
|
||||
}
|
||||
|
||||
// GETs only.
|
||||
guard case .GET = head.method else {
|
||||
self.respond405(context: context)
|
||||
let constructBody: (StaticResponse) throws -> ByteBuffer
|
||||
// GET or HEAD only.
|
||||
switch head.method {
|
||||
case .GET:
|
||||
constructBody = { response in
|
||||
try response.readBody()
|
||||
}
|
||||
case .HEAD:
|
||||
constructBody = { _ in ByteBuffer() }
|
||||
default:
|
||||
self.respondEmpty(context: context, status: .methodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
let configuration = self.configuration
|
||||
configuration.logger.info("\(head.method) \(head.uri)")
|
||||
|
||||
let response: StaticResponse
|
||||
let body: ByteBuffer
|
||||
do {
|
||||
switch head.uri {
|
||||
case "/":
|
||||
response = try respondIndexPage(context: context)
|
||||
case "/main.wasm":
|
||||
let contentSize = try localFileSystem.getFileInfo(configuration.mainWasmPath).size
|
||||
response = StaticResponse(
|
||||
contentType: "application/wasm",
|
||||
contentType: "application/wasm", contentSize: Int(contentSize),
|
||||
body: try context.channel.allocator.buffer(
|
||||
bytes: localFileSystem.readFileContents(configuration.mainWasmPath).contents
|
||||
)
|
||||
|
@ -75,35 +90,34 @@ final class ServerHTTPHandler: ChannelInboundHandler, RemovableChannelHandler {
|
|||
case "/" + configuration.entrypoint.fileName:
|
||||
response = StaticResponse(
|
||||
contentType: "application/javascript",
|
||||
contentSize: configuration.entrypoint.content.count,
|
||||
body: ByteBuffer(bytes: configuration.entrypoint.content.contents)
|
||||
)
|
||||
default:
|
||||
guard let staticResponse = try self.respond(context: context, head: head) else {
|
||||
self.respond404(context: context)
|
||||
self.respondEmpty(context: context, status: .notFound)
|
||||
return
|
||||
}
|
||||
response = staticResponse
|
||||
}
|
||||
body = try constructBody(response)
|
||||
} catch {
|
||||
configuration.logger.error("Failed to respond to \(head.uri): \(error)")
|
||||
response = StaticResponse(
|
||||
contentType: "text/plain",
|
||||
body: context.channel.allocator.buffer(string: "Internal server error")
|
||||
)
|
||||
self.respondEmpty(context: context, status: .internalServerError)
|
||||
return
|
||||
}
|
||||
self.responseBody = response.body
|
||||
|
||||
var headers = HTTPHeaders()
|
||||
headers.add(name: "Server", value: configuration.serverName)
|
||||
headers.add(name: "Content-Type", value: response.contentType)
|
||||
headers.add(name: "Content-Length", value: String(response.body.readableBytes))
|
||||
headers.add(name: "Content-Length", value: String(response.contentSize))
|
||||
headers.add(name: "Connection", value: "close")
|
||||
let responseHead = HTTPResponseHead(
|
||||
version: .init(major: 1, minor: 1),
|
||||
version: .http1_1,
|
||||
status: .ok,
|
||||
headers: headers)
|
||||
context.write(self.wrapOutboundOut(.head(responseHead)), promise: nil)
|
||||
context.write(self.wrapOutboundOut(.body(.byteBuffer(response.body))), promise: nil)
|
||||
context.write(self.wrapOutboundOut(.body(.byteBuffer(body))), promise: nil)
|
||||
context.write(self.wrapOutboundOut(.end(nil))).whenComplete { (_: Result<Void, Error>) in
|
||||
context.close(promise: nil)
|
||||
}
|
||||
|
@ -112,7 +126,18 @@ final class ServerHTTPHandler: ChannelInboundHandler, RemovableChannelHandler {
|
|||
|
||||
struct StaticResponse {
|
||||
let contentType: String
|
||||
let body: ByteBuffer
|
||||
let contentSize: Int
|
||||
private let _body: () throws -> ByteBuffer
|
||||
|
||||
init(contentType: String, contentSize: Int, body: @autoclosure @escaping () throws -> ByteBuffer) {
|
||||
self.contentType = contentType
|
||||
self.contentSize = contentSize
|
||||
self._body = body
|
||||
}
|
||||
|
||||
func readBody() throws -> ByteBuffer {
|
||||
return try self._body()
|
||||
}
|
||||
}
|
||||
|
||||
private func respond(context: ChannelHandlerContext, head: HTTPRequestHead) throws
|
||||
|
@ -141,8 +166,12 @@ final class ServerHTTPHandler: ChannelInboundHandler, RemovableChannelHandler {
|
|||
self.makeStaticResourcesResponder(baseDirectory: URL(fileURLWithPath: mainResourcesPath)))
|
||||
}
|
||||
|
||||
guard let uri = head.uri.removingPercentEncoding else {
|
||||
configuration.logger.error("Failed to percent decode uri: \(head.uri)")
|
||||
return nil
|
||||
}
|
||||
for responder in responders {
|
||||
if let response = try responder(context, head.uri) {
|
||||
if let response = try responder(context, uri) {
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
@ -162,9 +191,13 @@ final class ServerHTTPHandler: ChannelInboundHandler, RemovableChannelHandler {
|
|||
return nil
|
||||
}
|
||||
let contentType = contentType(of: fileURL) ?? "application/octet-stream"
|
||||
let attributes = try FileManager.default.attributesOfItem(atPath: fileURL.path)
|
||||
guard let contentSize = (attributes[.size] as? NSNumber)?.intValue else {
|
||||
throw ServerError(description: "Failed to get content size of \(fileURL)")
|
||||
}
|
||||
|
||||
return StaticResponse(
|
||||
contentType: contentType,
|
||||
contentType: contentType, contentSize: contentSize,
|
||||
body: try context.channel.allocator.buffer(bytes: Data(contentsOf: fileURL))
|
||||
)
|
||||
}
|
||||
|
@ -180,18 +213,18 @@ final class ServerHTTPHandler: ChannelInboundHandler, RemovableChannelHandler {
|
|||
entrypointName: configuration.entrypoint.fileName
|
||||
)
|
||||
return StaticResponse(
|
||||
contentType: "text/html",
|
||||
contentType: "text/html", contentSize: htmlContent.utf8.count,
|
||||
body: context.channel.allocator.buffer(string: htmlContent)
|
||||
)
|
||||
}
|
||||
|
||||
private func respond405(context: ChannelHandlerContext) {
|
||||
private func respondEmpty(context: ChannelHandlerContext, status: HTTPResponseStatus) {
|
||||
var headers = HTTPHeaders()
|
||||
headers.add(name: "Connection", value: "close")
|
||||
headers.add(name: "Content-Length", value: "0")
|
||||
let head = HTTPResponseHead(
|
||||
version: .http1_1,
|
||||
status: .methodNotAllowed,
|
||||
status: status,
|
||||
headers: headers)
|
||||
context.write(self.wrapOutboundOut(.head(head)), promise: nil)
|
||||
context.write(self.wrapOutboundOut(.end(nil))).whenComplete { (_: Result<Void, Error>) in
|
||||
|
|
|
@ -138,6 +138,18 @@ func fetchWebContent(at url: URL, timeout: Duration) async throws -> (response:
|
|||
return (response: response, body: body)
|
||||
}
|
||||
|
||||
func fetchHead(at url: URL, timeout: Duration) async throws -> HTTPURLResponse {
|
||||
let session = URLSession.shared
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "HEAD"
|
||||
let (_, response) = try await URLSession.shared.data(for: request)
|
||||
guard let httpResponse = response as? HTTPURLResponse else {
|
||||
throw CommandTestError("Response from \(url.absoluteString) is not HTTPURLResponse")
|
||||
}
|
||||
|
||||
return httpResponse
|
||||
}
|
||||
|
||||
func checkServerNameField(response: HTTPURLResponse, expectedPID: Int32) throws {
|
||||
guard let string = response.value(forHTTPHeaderField: "Server") else {
|
||||
throw CommandTestError("no Server header")
|
||||
|
|
|
@ -59,7 +59,7 @@ struct DevServerClient {
|
|||
at url: URL,
|
||||
file: StaticString = #file, line: UInt = #line
|
||||
) async throws -> String {
|
||||
let data = try await fetchBinary(at: url)
|
||||
let data = try await fetchBinary(at: url, file: file, line: line)
|
||||
|
||||
guard let string = String(data: data, encoding: .utf8) else {
|
||||
throw CommandTestError("not UTF-8 string content")
|
||||
|
@ -67,6 +67,14 @@ struct DevServerClient {
|
|||
|
||||
return string
|
||||
}
|
||||
|
||||
func fetchContentSize(
|
||||
at url: URL, file: StaticString = #file, line: UInt = #line
|
||||
) async throws -> Int {
|
||||
let httpResponse = try await fetchHead(at: url, timeout: .seconds(10))
|
||||
let contentLength = try XCTUnwrap(httpResponse.allHeaderFields["Content-Length"] as? String)
|
||||
return Int(contentLength)!
|
||||
}
|
||||
}
|
||||
|
||||
final class FrontendDevServerTests: XCTestCase {
|
||||
|
@ -132,25 +140,34 @@ final class FrontendDevServerTests: XCTestCase {
|
|||
</html>
|
||||
"""
|
||||
)
|
||||
let contentSize = try await cl.fetchContentSize(at: host)
|
||||
XCTAssertEqual(contentSize, indexHtml.utf8.count)
|
||||
}
|
||||
|
||||
do {
|
||||
let devJs = try await cl.fetchString(at: host.appendingPathComponent("dev.js"))
|
||||
let url = host.appendingPathComponent("dev.js")
|
||||
let devJs = try await cl.fetchString(at: url)
|
||||
let expected = try XCTUnwrap(String(data: StaticResource.dev, encoding: .utf8))
|
||||
XCTAssertEqual(devJs, expected)
|
||||
let contentSize = try await cl.fetchContentSize(at: url)
|
||||
XCTAssertEqual(contentSize, expected.utf8.count)
|
||||
}
|
||||
|
||||
do {
|
||||
let mainWasm = try await cl.fetchBinary(at: host.appendingPathComponent("main.wasm"))
|
||||
let url = host.appendingPathComponent("main.wasm")
|
||||
let mainWasm = try await cl.fetchBinary(at: url)
|
||||
let expected = try Data(contentsOf: wasmFile.asURL)
|
||||
XCTAssertEqual(mainWasm, expected)
|
||||
let contentSize = try await cl.fetchContentSize(at: url)
|
||||
XCTAssertEqual(contentSize, expected.count)
|
||||
}
|
||||
|
||||
do {
|
||||
let name = "style.css"
|
||||
for name in ["style.css", "space separated.txt"] {
|
||||
let styleCss = try await cl.fetchString(at: host.appendingPathComponent(name))
|
||||
let expected = try String(contentsOf: resourcesDir.appending(component: name).asURL)
|
||||
XCTAssertEqual(styleCss, expected)
|
||||
let contentSize = try await cl.fetchContentSize(at: host.appendingPathComponent(name))
|
||||
XCTAssertEqual(contentSize, expected.utf8.count)
|
||||
}
|
||||
|
||||
let webDriver = try await WebDriverServices.find(terminal: terminal)
|
||||
|
|
|
@ -14,7 +14,8 @@ let package = Package(
|
|||
.target(
|
||||
name: "app",
|
||||
resources: [
|
||||
.copy("style.css")
|
||||
.copy("style.css"),
|
||||
.copy("space separated.txt"),
|
||||
]
|
||||
)
|
||||
]
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
hello
|
Loading…
Reference in New Issue