diff --git a/Package.swift b/Package.swift index 89e2189..c02c8c7 100644 --- a/Package.swift +++ b/Package.swift @@ -10,8 +10,8 @@ let package = Package( .library(name: "Leaf", targets: ["Leaf"]), ], dependencies: [ - .package(url: "https://github.com/vapor/leaf-kit.git", from: "1.0.0-rc.1.2"), - .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0-rc.1"), + .package(url: "https://github.com/vapor/leaf-kit.git", from: "1.0.0-rc.1.11"), + .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0"), ], targets: [ .target(name: "Leaf", dependencies: [ diff --git a/Sources/Leaf/Application+Leaf.swift b/Sources/Leaf/Application+Leaf.swift index b118db8..97ce069 100644 --- a/Sources/Leaf/Application+Leaf.swift +++ b/Sources/Leaf/Application+Leaf.swift @@ -25,7 +25,7 @@ extension Application { return .init( configuration: self.configuration, cache: self.cache, - files: self.files, + sources: self.sources, eventLoop: self.application.eventLoopGroup.next(), userInfo: userInfo ) @@ -51,12 +51,16 @@ extension Application { } } - public var files: LeafFiles { + public var sources: LeafSources { get { - self.storage.files ?? NIOLeafFiles(fileio: self.application.fileio) + self.storage.sources ?? LeafSources.singleSource( + NIOLeafFiles(fileio: self.application.fileio, + limits: .default, + sandboxDirectory: self.configuration.rootDirectory, + viewDirectory: self.configuration.rootDirectory)) } nonmutating set { - self.storage.files = newValue + self.storage.sources = newValue } } @@ -95,7 +99,7 @@ extension Application { final class Storage { var cache: LeafCache var configuration: LeafConfiguration? - var files: LeafFiles? + var sources: LeafSources? var tags: [String: LeafTag] var userInfo: [AnyHashable: Any] diff --git a/Sources/Leaf/Deprecated.swift b/Sources/Leaf/Deprecated.swift new file mode 100644 index 0000000..d57da92 --- /dev/null +++ b/Sources/Leaf/Deprecated.swift @@ -0,0 +1,15 @@ +import Vapor + + +extension Application.Leaf { + /// Deprecated in Leaf-Kit 1.0.0rc-1.?? + @available(*, deprecated, message: "Use .sources instead of .files") + public var files: LeafSource { + get { + fatalError("Unavailable") + } + nonmutating set { + self.storage.sources = .singleSource(newValue) + } + } +} diff --git a/Sources/Leaf/Request+Leaf.swift b/Sources/Leaf/Request+Leaf.swift index 3ceb891..997ed31 100644 --- a/Sources/Leaf/Request+Leaf.swift +++ b/Sources/Leaf/Request+Leaf.swift @@ -10,7 +10,7 @@ extension Request { configuration: self.application.leaf.configuration, tags: self.application.leaf.tags, cache: self.application.leaf.cache, - files: self.application.leaf.files, + sources: self.application.leaf.sources, eventLoop: self.eventLoop, userInfo: userInfo ) diff --git a/Tests/LeafTests/LeafTests.swift b/Tests/LeafTests/LeafTests.swift index f582d9e..621a9ef 100644 --- a/Tests/LeafTests/LeafTests.swift +++ b/Tests/LeafTests/LeafTests.swift @@ -7,10 +7,11 @@ class LeafTests: XCTestCase { defer { app.shutdown() } app.views.use(.leaf) + app.leaf.configuration.rootDirectory = projectFolder app.leaf.cache.isEnabled = false app.get("test-file") { req in - req.view.render(#file, ["foo": "bar"]) + req.view.render("Tests/LeafTests/LeafTests.swift", ["foo": "bar"]) } try app.test(.GET, "test-file") { res in @@ -20,6 +21,46 @@ class LeafTests: XCTestCase { XCTAssertContains(res.body.string, "test: bar") } } + + func testSandboxing() throws { + let app = Application(.testing) + defer { app.shutdown() } + + app.views.use(.leaf) + app.leaf.configuration.rootDirectory = templateFolder + app.leaf.sources = .singleSource(NIOLeafFiles(fileio: app.fileio, + limits: .default, + sandboxDirectory: projectFolder, + viewDirectory: templateFolder)) + + app.get("hello") { req in + req.view.render("hello") + } + + app.get("allowed") { req in + req.view.render("../hello") + } + + app.get("sandboxed") { req in + req.view.render("../../hello") + } + + try app.test(.GET, "hello") { res in + XCTAssertEqual(res.status, .ok) + XCTAssertEqual(res.headers.contentType, .html) + XCTAssertEqual(res.body.string, "Hello, world!\n") + } + + try app.test(.GET, "allowed") { res in + XCTAssertEqual(res.status, .internalServerError) + XCTAssert(res.body.string.contains("noTemplateExists")) + } + + try app.test(.GET, "sandboxed") { res in + XCTAssertEqual(res.status, .internalServerError) + XCTAssert(res.body.string.contains("Attempted to escape sandbox")) + } + } func testContextRequest() throws { var test = TestFiles() @@ -40,7 +81,7 @@ class LeafTests: XCTestCase { app.leaf.configuration.rootDirectory = "/" app.leaf.cache.isEnabled = false app.leaf.tags["path"] = RequestPathTag() - app.leaf.files = test + app.leaf.sources = .singleSource(test) app.get("test-file") { req in req.view.render("foo", [ @@ -77,7 +118,7 @@ class LeafTests: XCTestCase { app.leaf.configuration.rootDirectory = "/" app.leaf.cache.isEnabled = false app.leaf.tags["custom"] = CustomTag() - app.leaf.files = test + app.leaf.sources = .singleSource(test) app.leaf.userInfo["info"] = "World" app.get("test-file") { req in @@ -94,20 +135,39 @@ class LeafTests: XCTestCase { } } -struct TestFiles: LeafFiles { +/// Helper `LeafFiles` struct providing an in-memory thread-safe map of "file names" to "file data" +internal struct TestFiles: LeafSource { var files: [String: String] - + var lock: Lock + init() { files = [:] + lock = .init() } - - func file(path: String, on eventLoop: EventLoop) -> EventLoopFuture { + + public func file(template: String, escape: Bool = false, on eventLoop: EventLoop) -> EventLoopFuture { + var path = template + if path.split(separator: "/").last?.split(separator: ".").count ?? 1 < 2, + !path.hasSuffix(".leaf") { path += ".leaf" } + if !path.hasPrefix("/") { path = "/" + path } + + self.lock.lock() + defer { self.lock.unlock() } if let file = self.files[path] { - var buffer = ByteBufferAllocator().buffer(capacity: 0) + var buffer = ByteBufferAllocator().buffer(capacity: file.count) buffer.writeString(file) return eventLoop.makeSucceededFuture(buffer) } else { - return eventLoop.makeFailedFuture("no test file: \(path)") + return eventLoop.makeFailedFuture(LeafError(.noTemplateExists(template))) } } } + +internal var templateFolder: String { + return projectFolder + "Views/" +} + +internal var projectFolder: String { + let folder = #file.split(separator: "/").dropLast(3).joined(separator: "/") + return "/" + folder + "/" +}