Skip to content

Commit

Permalink
Serve internal resources using FileResourceServer ensuring path is st…
Browse files Browse the repository at this point in the history
…andardized. (#1273)
  • Loading branch information
kilnerm authored and ianpartridge committed May 22, 2018
1 parent aa53322 commit 5979edd
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 134 deletions.
115 changes: 115 additions & 0 deletions Sources/Kitura/FileResourceServer.swift
@@ -0,0 +1,115 @@
/*
* Copyright IBM Corporation 2016, 2017
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import Foundation
import LoggerAPI

class FileResourceServer {

/// if file found - send it in response
func sendIfFound(resource: String, usingResponse response: RouterResponse) {
guard let resourceFileName = getFilePath(for: resource) else {
do {
try response.send("Cannot find resource: \(resource)").status(.notFound).end()
} catch {
Log.error("failed to send not found response for resource: \(resource)")
}
return
}

do {
try response.send(fileName: resourceFileName)
try response.status(.OK).end()
} catch {
Log.error("failed to send response with resource \(resourceFileName)")
}
}

private func getFilePath(for resource: String) -> String? {
var candidatePath: String? = nil
var basePath: String? = nil
let fileManager = FileManager.default
var potentialResource = getResourcePathBasedOnSourceLocation(for: resource)

if potentialResource.hasSuffix("/") {
potentialResource += "index.html"
}

let fileExists = fileManager.fileExists(atPath: potentialResource)
if fileExists {
candidatePath = potentialResource
basePath = getResourcePathBasedOnSourceLocation(for: "")
} else {
candidatePath = getResourcePathBasedOnCurrentDirectory(for: resource, withFileManager: fileManager)
basePath = getResourcePathBasedOnCurrentDirectory(for: "", withFileManager: fileManager)
}
// We need to ensure we are only serving kitura resources so need to decode the file path and then check it has Sources/Kitura/resources before the last element
guard isValidPath(resourcePath: candidatePath, basePath: basePath) else {
return nil
}
return candidatePath
}

func isValidPath(resourcePath: String?, basePath: String?) -> Bool {
guard let resource = resourcePath,
let base = basePath,
let absoluteBasePath = NSURL(fileURLWithPath: base).standardizingPath?.absoluteString,
let standardisedPath = NSURL(fileURLWithPath: resource).standardizingPath?.absoluteString else {
return false
}
return standardisedPath.hasPrefix(absoluteBasePath)
}

private func getResourcePathBasedOnSourceLocation(for resource: String) -> String {
let fileName = NSString(string: #file)
let resourceFilePrefixRange: NSRange
let lastSlash = fileName.range(of: "/", options: .backwards)
if lastSlash.location != NSNotFound {
resourceFilePrefixRange = NSRange(location: 0, length: lastSlash.location+1)
} else {
resourceFilePrefixRange = NSRange(location: 0, length: fileName.length)
}
return fileName.substring(with: resourceFilePrefixRange) + "resources/" + resource
}

private func getResourcePathBasedOnCurrentDirectory(for resource: String, withFileManager fileManager: FileManager) -> String? {
for suffix in ["/Packages", "/.build/checkouts"] {
let packagePath: String
#if os(iOS)
guard let resourcePath = Bundle.main.resourcePath else {
continue
}
packagePath = resourcePath + suffix
#else
packagePath = fileManager.currentDirectoryPath + suffix
#endif

do {
let packages = try fileManager.contentsOfDirectory(atPath: packagePath)
for package in packages {
let potentialResource = "\(packagePath)/\(package)/Sources/Kitura/resources/\(resource)"
let resourceExists = fileManager.fileExists(atPath: potentialResource)
if resourceExists {
return potentialResource
}
}
} catch {
Log.error("No packages found in \(packagePath)")
}
}
return nil
}
}
46 changes: 21 additions & 25 deletions Sources/Kitura/Router.swift
Expand Up @@ -50,8 +50,11 @@ public class Router {
}
}

/// Prefix for special page resources
fileprivate let kituraResourcePrefix = "/@@Kitura-router@@/"

/// Helper for serving file resources
private let fileResourceServer: StaticFileServer?
fileprivate let fileResourceServer = FileResourceServer()

/// Flag to enable/disable access to parent router's params
private let mergeParameters: Bool
Expand All @@ -67,23 +70,10 @@ public class Router {
/// matched in its parent router. Defaults to `false`.
public init(mergeParameters: Bool = false) {
self.mergeParameters = mergeParameters
guard let resourcePath = Router.getKituraResourcePath() else {
self.fileResourceServer = nil
return
}
self.fileResourceServer = StaticFileServer(path: resourcePath)

Log.verbose("Router initialized")
}

fileprivate static func getKituraResourcePath() -> String? {
guard let currentFilePath = NSURL(string: #file),
let folderPath = currentFilePath.deletingLastPathComponent else {
return nil
}
return folderPath.appendingPathComponent("resources").absoluteString
}

func routingHelper(_ method: RouterMethod, pattern: String?, handler: [RouterHandler]) -> Router {
elements.append(RouterElement(method: method,
pattern: pattern,
Expand Down Expand Up @@ -373,16 +363,23 @@ extension Router : ServerDelegate {
/// - Parameter callback: The closure to invoke to cause the router to inspect the
/// path in the list of paths.
fileprivate func process(request: RouterRequest, response: RouterResponse, callback: @escaping () -> Void) {
if let fileServer = fileResourceServer,
fileServer.serveKituraResource(request: request, response: response) {
guard let urlPath = request.parsedURLPath.path else {
Log.error("request.parsedURLPath.path is nil. Failed to process request")
return
}
let looper = RouterElementWalker(elements: self.elements,
parameterHandlers: self.parameterHandlers,
request: request,
response: response,
callback: callback)
looper.next()

if urlPath.hasPrefix(kituraResourcePrefix) {
let resource = String(urlPath[kituraResourcePrefix.endIndex...])
fileResourceServer.sendIfFound(resource: resource, usingResponse: response)
} else {
let looper = RouterElementWalker(elements: self.elements,
parameterHandlers: self.parameterHandlers,
request: request,
response: response,
callback: callback)

looper.next()
}
}

/// Send default index.html file and its resources if appropriate, otherwise send
Expand All @@ -393,9 +390,8 @@ extension Router : ServerDelegate {
/// - Parameter response: The `RouterResponse` object used to send responses
/// to the HTTP request.
private func sendDefaultResponse(request: RouterRequest, response: RouterResponse) {
if let fileServer = fileResourceServer,
request.parsedURLPath.path == "/" {
fileServer.handle(request: request, response: response, next: {})
if request.parsedURLPath.path == "/" {
fileResourceServer.sendIfFound(resource: "index.html", usingResponse: response)
} else {
do {
let errorMessage = "Cannot \(request.method) \(request.parsedURLPath.path ?? "")."
Expand Down

0 comments on commit 5979edd

Please sign in to comment.