LeafSources, LeafSource and File Sandboxing/Limiting in NIOLeafFiles
Pre-releaseThis patch was authored and released by @tdotclare.
LeafSources, LeafSource and File Sandboxing/Limiting in NIOLeafFiles
This update provides several notable changes to internal behaviors to how raw source templates for Leaf are located and read prior to parsing:
LeafSourcesstores multipleLeafSource*-adhering objects by name and maintains a default search order of which objects to attempt to read fromLeafSource(previouslyLeafFiles) represents any object with a directed behavior for interpreting a template name into its own reading space (eg, a filesystem or database)NIOLeafFilesgains initialization configuration for sandboxing and reading-limit behavior
LeafSources Usage Pattern
LeafSources publishes three public attributes:
.all: Set<String>read-only set of all registeredLeafSourceobjects.searchOrder: [String]read-only array ofLeafSources which will be checked if no specific source is specifiedregister(source key: String = "default", using source: LeafSource, searchable: Bool = true)will add thesourcebykeyname, and ifsearchableis set, will include it in the search order.
Keys must be unique, and search order cannot be changed after a source is added to prevent collisions or changes in behavior by modifying sources once LeafRenderer has been used to read any template, so register must be called in the order objects should be used.
NOTE
Hooks are not yet available for specifying a single source to use, but are intended to be added. render by default will progressively search the sources according to searchOrder, but a new method for directively using only a single named source (EG: render(template, from: source)) would force Leaf to only attempt to use that named source (failing if source can't provide it).
This will allow applications to directively secure templates so that, EG, internally called templates can be named as being in a specific source, while potentially publically written templates can be stored in another source (and prevented from accessing the secured templates by not including that source in the default search order.)
A default definition of LeafSources.singleSource(LeafSource) provides a convenience for creating a complete LeafSources object that mimics the previous behavior of LeafRenderer.files
Example usage:
// Will access templates exactly as before
let nioLeaf = NIOLeafFiles(fileio: app.fileio,
limits: [.requireExtensions],
sandboxDirectory: "/",
viewDirectory: app.directory.viewsDirectory)
let singleSource = LeafSources.singleSource(nioLeaf)
// Will search sourceOne, then SourceTwo if sourceOne can't provide requested template
// Will never search hiddenSource unless it is specifically requested via explicit request
let multipleSources = LeafSources()
try! multipleSources.register(using: sourceOne)
try! multipleSources.register(source: "sourceTwo", using: sourceTwo)
try! multipleSources.register(source: "hiddenSource", using: hiddenSource, searchable: false)This results in a LeafSource with three specific sources of templates; a request for a template will first check sourceOne, then sourceTwo. hiddenSource will not be checked unless specifically named when attempting to render a template.
LeafSource Usage Pattern
LeafSource is a renaming of the LeafFiles protocol and updates the single signature call required to reflect generic concepts of a "template name" versus a filesystem path. escape may be meaningless for a particular source object, and can be safely ignored if so.
// Required signature format
func file(template: String, escape: Bool, on eventLoop: EventLoop) throws -> EventLoopFuture<ByteBuffer>
// Deprecated format
func file(path: String, on eventLoop: EventLoop) throws -> EventLoopFuture<ByteBuffer>For objects with a concept of "escaping", an attempt to escape should throw LeafError(.illegalAccess()) so that LeafSources can fail a search entirely. EG; if the first configured source cannot provide the requested template, LeafSources will continue to the next source, but if the first throws illegal access, the entire render attempt will fail rather than going on to a potentially less-restricted source.
NOTE
template should be interpreted in a meaningful way by an adherent to LeafSource, so calls to render should generally refer to the template name as generically as possible and allow the specific LeafSource to expand or interpret it as appropriate for its internal store map; LeafRenderer no longer makes any attempt to expand or modify the template name prior to requesting it from the LeafSource:
// Previous: LeafFiles would get a request for "/#viewDirectory#/path/to/template.leaf"
render("path/to/template")
// Now: LeafSource gets a request for exactly "path/to/template"
render("path/to/template")However, default implementations bridging the older call signature for LeafFiles will mimic the old LeafRenderer behavior and expand the template path using the old behavior (this does NOT protect against escaping or guarantee non-relative path requests)
NIOLeafFiles Usage Pattern
NIOLeafFiles gains the following initialization configuration for sandboxing and reading-limit behaviors:
viewDirectory: the default directory templates will be interpreted as relative tosandboxDirectory: the highest level directory the object may read fromlimits: (NIOLeafFiles.Limit): anOptionSetof behaviors:.toSandbox: if set, files outsidesandboxDirectorycan't be read.toVisibleFiles: if set, prevents reading any file/directory starting with.(or files inside such directories).requireExtensions: only files with an extension can be read.onlyLeafExtensions: only files with.leafextensions can be read
- Default configuration of
limitsis[.toSandbox, .toVisibleFiles, .requireExtensions] viewDirectorymust be contained insidesandboxDirectory(or coincident to limit entirely toviewDirectory)
Example:
let templateFolder = "/web/app/Resources/"
NIOLeafFiles(fileio: fileio,
limits: .default, // .toSandbox, .toVisibleFiles, .requireExtensions
sandboxDirectory: templateFolder,
viewDirectory: templateFolder + "Views/")
... // setup happens for LeafRenderer
renderer.render("a") // Tries to render "/web/app/Resources/Views/a.leaf"
renderer.render("a.leaf") // Tries to render "/web/app/Resources/Views/a.leaf"
renderer.render("../a") // Tries to render "/web/app/Resources/a.leaf"
renderer.render("../../a") // Throws .illegalAccess for "/web/app/a.leaf"
renderer.render(".ssh") // Throws .illegalAccess for ""/web/app/Resources/Views/.ssh"