Skip to content

Commit

Permalink
Removed NSRegularExpression dependency from HttpServer.
Browse files Browse the repository at this point in the history
Introduced a first version of new routing mechanism ( ~Sinatra ).
  • Loading branch information
damian-kolakowski committed Dec 8, 2015
1 parent e8acc49 commit df24ced
Show file tree
Hide file tree
Showing 10 changed files with 206 additions and 125 deletions.
74 changes: 24 additions & 50 deletions Sources/Swifter/DemoServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,30 +10,41 @@ public func demoServer(publicDir: String?) -> HttpServer {
let server = HttpServer()

if let publicDir = publicDir {
server["/resources/(.+)"] = HttpHandlers.directory(publicDir)
server["/resources/:file"] = HttpHandlers.directory(publicDir)
}

server["/files/:path"] = HttpHandlers.directoryBrowser("~/")

server["/"] = { r in
var listPage = "Available services:<br><ul>"
listPage += server.routes.map({ "<li><a href=\"\($0)\">\($0)</a></li>"}).joinWithSeparator("")
return .OK(.Html(listPage))
}

server["/files(.+)"] = HttpHandlers.directoryBrowser("~/")
server["/magic"] = { .OK(.Html("You asked for " + $0.url)) }

server["/test"] = { request in
server["/test/:param1/:param2"] = { r in
var headersInfo = ""
for (name, value) in request.headers {
for (name, value) in r.headers {
headersInfo += "\(name) : \(value)<br>"
}
var queryParamsInfo = ""
for (name, value) in request.urlParams {
for (name, value) in r.urlParams {
queryParamsInfo += "\(name) : \(value)<br>"
}
return .OK(.Html("<h3>Address: \(request.address)</h3><h3>Url:</h3> \(request.url)<h3>Method: \(request.method)</h3><h3>Headers:</h3>\(headersInfo)<h3>Query:</h3>\(queryParamsInfo)"))
var pathParamsInfo = ""
for token in r.params {
pathParamsInfo += "\(token.0) : \(token.1)<br>"
}
return .OK(.Html("<h3>Address: \(r.address)</h3><h3>Url:</h3> \(r.url)<h3>Method: \(r.method)</h3><h3>Headers:</h3>\(headersInfo)<h3>Query:</h3>\(queryParamsInfo)<h3>Path params:</h3>\(pathParamsInfo)"))
}

server["/params/(.+)/(.+)"] = { request in
var capturedGroups = ""
for (index, group) in request.capturedUrlGroups.enumerate() {
capturedGroups += "Expression group \(index) : \(group)<br>"
}
return .OK(.Html("Url: \(request.url)<br>Method: \(request.method)<br>\(capturedGroups)"))
server["/demo"] = { r in
return .OK(.Html("<center><h2>Hello Swift</h2><img src=\"https://devimages.apple.com.edgekey.net/swift/images/swift-hero_2x.png\"/><br></center>"))
}

server["/raw"] = { request in
return HttpResponse.RAW(200, "OK", ["XXX-Custom-Header": "value"], [UInt8]("Sample Response".utf8))
}

server["/json"] = { request in
Expand All @@ -43,49 +54,12 @@ public func demoServer(publicDir: String?) -> HttpServer {
server["/redirect"] = { request in
return .MovedPermanently("http://www.google.com")
}

server["/long"] = { request in
var longResponse = ""
for k in 0..<1000 { longResponse += "(\(k)),->" }
return .OK(.Html(longResponse))
}

server["/demo"] = { request in
return .OK(.Html("<center><h2>Hello Swift</h2>" +
"<img src=\"https://devimages.apple.com.edgekey.net/swift/images/swift-hero_2x.png\"/><br>" +
"</center>"))
}

server["/login"] = { request in
switch request.method.uppercaseString {
case "GET":
if let rootDir = publicDir {
if let html = NSData(contentsOfFile:"\(rootDir)/login.html") {
var array = [UInt8](count: html.length, repeatedValue: 0)
html.getBytes(&array, length: html.length)
return HttpResponse.RAW(200, "OK", nil, array)
} else {
return .NotFound
}
}
case "POST":
let formFields = request.parseForm()
return HttpResponse.OK(.Html(formFields.map({ "\($0.0) = \($0.1)" }).joinWithSeparator("<br>")))
default:
return .NotFound
}
return .NotFound
}

server["/raw"] = { request in
return HttpResponse.RAW(200, "OK", ["XXX-Custom-Header": "value"], [UInt8]("Sample Response".utf8))
}

server["/"] = { request in
var listPage = "Available services:<br><ul>"
listPage += server.routes.map({ "<li><a href=\"\($0)\">\($0)</a></li>"}).joinWithSeparator("")
return .OK(.Html(listPage))
}

return server
}
22 changes: 6 additions & 16 deletions Sources/Swifter/HttpHandlers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ public class HttpHandlers {
public class func directory(dir: String) -> ( HttpRequest -> HttpResponse ) {
return { request in

guard let localPath = request.capturedUrlGroups.first else {
guard let localPath = request.params.first else {
return HttpResponse.NotFound
}

let filesPath = dir.stringByExpandingTildeInPath.stringByAppendingPathComponent(localPath)
let filesPath = dir + "/" + localPath.1

let cachedBody = cache.objectForKey(filesPath) as? NSData

Expand Down Expand Up @@ -74,17 +74,17 @@ public class HttpHandlers {
}

public class func directoryBrowser(dir: String) -> ( HttpRequest -> HttpResponse ) {
return { request in
if let pathFromUrl = request.capturedUrlGroups.first {
let filePath = dir.stringByExpandingTildeInPath.stringByAppendingPathComponent(pathFromUrl)
return { r in
if let (_, value) = r.params.first {
let filePath = dir + "/" + value
let fileManager = NSFileManager.defaultManager()
var isDir: ObjCBool = false;
if ( fileManager.fileExistsAtPath(filePath, isDirectory: &isDir) ) {
if ( isDir ) {
do {
let files = try fileManager.contentsOfDirectoryAtPath(filePath)
var response = "<h3>\(filePath)</h3></br><table>"
response += files.map({ "<tr><td><a href=\"\(request.url)/\($0)\">\($0)</a></td></tr>"}).joinWithSeparator("")
response += files.map({ "<tr><td><a href=\"\(r.url)/\($0)\">\($0)</a></td></tr>"}).joinWithSeparator("")
response += "</table>"
return HttpResponse.OK(.Html(response))
} catch {
Expand All @@ -103,13 +103,3 @@ public class HttpHandlers {
}
}
}

private extension String {
var stringByExpandingTildeInPath: String {
return (self as NSString).stringByExpandingTildeInPath
}

func stringByAppendingPathComponent(str: String) -> String {
return (self as NSString).stringByAppendingPathComponent(str)
}
}
6 changes: 3 additions & 3 deletions Sources/Swifter/HttpParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,16 @@ class HttpParser {
let headers = try readHeaders(socket)
if let contentLength = headers["content-length"], let contentLengthValue = Int(contentLength) {
let body = try readBody(socket, size: contentLengthValue)
return HttpRequest(url: path, urlParams: urlParams, method: method, headers: headers, body: body, capturedUrlGroups: [], address: nil)
return HttpRequest(url: path, urlParams: urlParams, method: method, headers: headers, body: body, address: nil, params: [:])
}
return HttpRequest(url: path, urlParams: urlParams, method: method, headers: headers, body: nil, capturedUrlGroups: [], address: nil)
return HttpRequest(url: path, urlParams: urlParams, method: method, headers: headers, body: nil, address: nil, params: [:])
}

private func extractUrlParams(url: String) -> [(String, String)] {
guard let query = url.split("?").last else {
return []
}
return query.split("&").map { (param:String) -> (String, String) in
return query.split("&").map { (param: String) -> (String, String) in
let tokens = param.split("=")
guard tokens.count >= 2 else {
return ("", "")
Expand Down
2 changes: 1 addition & 1 deletion Sources/Swifter/HttpRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ public struct HttpRequest {
public let method: String
public let headers: [String: String]
public let body: String?
public var capturedUrlGroups: [String]
public var address: String?
public var params: [String: String]

public func parseForm() -> [(String, String)] {
if let body = body {
Expand Down
55 changes: 55 additions & 0 deletions Sources/Swifter/HttpRouter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//
// HttpRouter.swift
// Swifter
// Copyright (c) 2015 Damian Kołakowski. All rights reserved.
//

import Foundation

public class HttpRouter {

private var handlers: [(pattern: [String], handler: HttpServer.Handler)] = []

public func routes() -> [String] {
return handlers.map { $0.pattern.joinWithSeparator("/") }
}

public func register(path: String, handler: HttpServer.Handler) {
handlers.append((path.split("/"), handler));
handlers.sortInPlace { $0.0.pattern.count < $0.1.pattern.count }
}

public func select(url:String) -> ([String:String], HttpServer.Handler)? {
let urlTokens = url.split("/")
for (pattern, handler) in handlers {
if let params = matchParams(pattern, valueTokens: urlTokens) {
return (params, handler)
}
}
return nil
}

public func matchParams(patternTokens: [String], valueTokens: [String]) -> [String: String]? {
var params = [String: String]()
for index in 0..<valueTokens.count {
if index >= patternTokens.count {
return nil
}
let patternToken = patternTokens[index]
let valueToken = valueTokens[index]
if patternToken.isEmpty {
if patternToken != valueToken {
return nil
}
}
if patternToken.characters.first == ":" {
params[patternToken.substringFromIndex(patternToken.characters.startIndex.successor())] = valueToken
} else {
if patternToken != valueToken {
return nil
}
}
}
return params
}
}
59 changes: 9 additions & 50 deletions Sources/Swifter/HttpServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ public class HttpServer

public typealias Handler = HttpRequest -> HttpResponse

private var handlers: [(expression: NSRegularExpression, handler: Handler)] = []
private var router = HttpRouter()

private var listenSocket: Socket = Socket(socketFileDescriptor: -1)
private var clientSockets: Set<Socket> = []
private let clientSocketsLock = 0
Expand All @@ -21,22 +22,15 @@ public class HttpServer

public subscript (path: String) -> Handler? {
set {
do {
let regex = try NSRegularExpression(pattern: path, options: self.expressionOptions)
if let newHandler = newValue {
self.handlers.append(expression: regex, handler: newHandler)
// Longer patterns will have higher priority.
self.handlers = self.handlers.sort { $0.0.pattern > $1.0.pattern }
}
} catch {
print("Could not register handler for: \(path), error: \(error)")
}
router.register(path, handler: newValue!)
}
get {
return nil
}
get { return nil }
}

public var routes:[String] {
return self.handlers.map { $0.expression.pattern }
return router.routes()
}

public func start(listenPort: in_port_t = 8080) throws {
Expand All @@ -53,9 +47,8 @@ public class HttpServer
while let request = try? httpParser.readHttpRequest(socket) {
let keepAlive = httpParser.supportsKeepAlive(request.headers)
let response: HttpResponse
if let (expression, handler) = self.findHandler(request.url) {
let capturedUrlsGroups = self.captureExpressionGroups(expression, value: request.url)
let updatedRequest = HttpRequest(url: request.url, urlParams: request.urlParams, method: request.method, headers: request.headers, body: request.body, capturedUrlGroups: capturedUrlsGroups, address: socketAddress)
if let (params, handler) = self.router.select(request.url) {
let updatedRequest = HttpRequest(url: request.url, urlParams: request.urlParams, method: request.method, headers: request.headers, body: request.body, address: socketAddress, params: params)
response = handler(updatedRequest)
} else {
response = HttpResponse.NotFound
Expand Down Expand Up @@ -88,40 +81,6 @@ public class HttpServer
}
}

private let matchingOptions = NSMatchingOptions(rawValue: 0)
private let expressionOptions = NSRegularExpressionOptions(rawValue: 0)

private func findHandler(url:String) -> (NSRegularExpression, Handler)? {
if let u = NSURL(string: url), path = u.path {
for handler in self.handlers {
if handler.expression.numberOfMatchesInString(path, options: self.matchingOptions, range: HttpServer.asciiRange(path)) > 0 {
return handler
}
}
}
return nil
}

private func captureExpressionGroups(expression: NSRegularExpression, value: String) -> [String] {
guard let u = NSURL(string: value), path = u.path else {
return []
}
var capturedGroups = [String]()
if let result = expression.firstMatchInString(path, options: matchingOptions, range: HttpServer.asciiRange(path)) {
let nsValue: NSString = path
for i in 1..<result.numberOfRanges {
if let group = nsValue.substringWithRange(result.rangeAtIndex(i)).stringByRemovingPercentEncoding {
capturedGroups.append(group)
}
}
}
return capturedGroups
}

private class func asciiRange(value: String) -> NSRange {
return NSMakeRange(0, value.lengthOfBytesUsingEncoding(NSASCIIStringEncoding))
}

private class func lock(handle: AnyObject, closure: () -> ()) {
objc_sync_enter(handle)
closure()
Expand Down
10 changes: 10 additions & 0 deletions Swifter.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
7AE893EA1C05127900A29F63 /* SwifteriOS.h in Headers */ = {isa = PBXBuildFile; fileRef = 7AE893E91C05127900A29F63 /* SwifteriOS.h */; settings = {ATTRIBUTES = (Public, ); }; };
7AE893FE1C0512C400A29F63 /* SwifterMac.h in Headers */ = {isa = PBXBuildFile; fileRef = 7AE893FD1C0512C400A29F63 /* SwifterMac.h */; settings = {ATTRIBUTES = (Public, ); }; };
7AE8940D1C05151100A29F63 /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7AE8940C1C05151100A29F63 /* Launch Screen.storyboard */; };
7C67C3471C17542E007B98E8 /* HttpRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C67C3461C17542E007B98E8 /* HttpRouter.swift */; };
7C67C3481C17542E007B98E8 /* HttpRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C67C3461C17542E007B98E8 /* HttpRouter.swift */; };
7C67C3491C17542E007B98E8 /* HttpRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C67C3461C17542E007B98E8 /* HttpRouter.swift */; };
7C67C34A1C17542E007B98E8 /* HttpRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C67C3461C17542E007B98E8 /* HttpRouter.swift */; };
7C71C5B01A1D52F800682BF0 /* login.html in CopyFiles */ = {isa = PBXBuildFile; fileRef = 98630C061A1C9A9D00478D08 /* login.html */; };
7C71C5B11A1EC49B00682BF0 /* logo.png in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7CB102DF1A17381D00CBA3B4 /* logo.png */; };
7CA4813E19A2EA8D0030B30D /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CA4813D19A2EA8D0030B30D /* main.swift */; };
Expand Down Expand Up @@ -97,6 +101,7 @@
7AE893FD1C0512C400A29F63 /* SwifterMac.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SwifterMac.h; sourceTree = "<group>"; };
7AE893FF1C0512C400A29F63 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
7AE8940C1C05151100A29F63 /* Launch Screen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = "<group>"; };
7C67C3461C17542E007B98E8 /* HttpRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HttpRouter.swift; sourceTree = "<group>"; };
7C839B6E19422CFF003A6950 /* SwifterSampleiOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwifterSampleiOS.app; sourceTree = BUILT_PRODUCTS_DIR; };
7CA4813B19A2EA8D0030B30D /* SwifterSampleOSX */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = SwifterSampleOSX; sourceTree = BUILT_PRODUCTS_DIR; };
7CA4813D19A2EA8D0030B30D /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -238,6 +243,7 @@
isa = PBXGroup;
children = (
7CEAF84C1C14B29B003252DE /* DemoServer.swift */,
7C67C3461C17542E007B98E8 /* HttpRouter.swift */,
7CEAF84D1C14B29B003252DE /* HttpHandlers.swift */,
7CEAF84E1C14B29B003252DE /* HttpParser.swift */,
7CEAF84F1C14B29B003252DE /* HttpRequest.swift */,
Expand Down Expand Up @@ -431,6 +437,7 @@
7CEAF85D1C14B29B003252DE /* HttpParser.swift in Sources */,
7CEAF8551C14B29B003252DE /* DemoServer.swift in Sources */,
7CD6DABB1C15C48500A04931 /* String+Misc.swift in Sources */,
7C67C3491C17542E007B98E8 /* HttpRouter.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -446,6 +453,7 @@
7CEAF85E1C14B29B003252DE /* HttpParser.swift in Sources */,
7CEAF8561C14B29B003252DE /* DemoServer.swift in Sources */,
7CD6DABC1C15C48500A04931 /* String+Misc.swift in Sources */,
7C67C34A1C17542E007B98E8 /* HttpRouter.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -457,6 +465,7 @@
7CEAF8531C14B29B003252DE /* DemoServer.swift in Sources */,
7CEAF8671C14B29B003252DE /* HttpServer.swift in Sources */,
7CD6DAB91C15C48500A04931 /* String+Misc.swift in Sources */,
7C67C3471C17542E007B98E8 /* HttpRouter.swift in Sources */,
7CEAF8571C14B29B003252DE /* HttpHandlers.swift in Sources */,
7CEAF85B1C14B29B003252DE /* HttpParser.swift in Sources */,
7CEAF86B1C14B29B003252DE /* Socket.swift in Sources */,
Expand All @@ -474,6 +483,7 @@
7CEAF8541C14B29B003252DE /* DemoServer.swift in Sources */,
7CEAF8681C14B29B003252DE /* HttpServer.swift in Sources */,
7CD6DABA1C15C48500A04931 /* String+Misc.swift in Sources */,
7C67C3481C17542E007B98E8 /* HttpRouter.swift in Sources */,
7CEAF8581C14B29B003252DE /* HttpHandlers.swift in Sources */,
7CEAF85C1C14B29B003252DE /* HttpParser.swift in Sources */,
7CEAF86C1C14B29B003252DE /* Socket.swift in Sources */,
Expand Down
Binary file not shown.
Loading

0 comments on commit df24ced

Please sign in to comment.