Просмотр исходного кода

server chat + minor changes

add beginning of server chat
some minor changes
nonn 10 лет назад
Родитель
Сommit
b83233e084

+ 1 - 1
Common/DemoServer.swift

@@ -75,7 +75,7 @@ func demoServer(publicDir: String?) -> HttpServer {
     }
     server["/"] = { request in
         var listPage = "Available services:<br><ul>"
-        for item in server.routes() {
+        for item in server.routes {
             listPage += "<li><a href=\"\(item)\">\(item)</a></li>"
         }
         listPage += "</ul>"

+ 3 - 2
Common/HttpHandlers.swift

@@ -16,6 +16,7 @@ public class HttpHandlers {
                     return HttpResponse.RAW(200, "OK", nil, fileBody)
                 }
             }
+            
             return HttpResponse.NotFound
         }
     }
@@ -26,8 +27,8 @@ public class HttpHandlers {
                 let filePath = dir.stringByExpandingTildeInPath.stringByAppendingPathComponent(pathFromUrl)
                 let fileManager = NSFileManager.defaultManager()
                 var isDir: ObjCBool = false;
-                if ( fileManager.fileExistsAtPath(filePath, isDirectory: &isDir) ) {
-                    if ( isDir ) {
+                if fileManager.fileExistsAtPath(filePath, isDirectory: &isDir) {
+                    if isDir {
                         do {
                             let files = try fileManager.contentsOfDirectoryAtPath(filePath)
                             var response = "<h3>\(filePath)</h3></br><table>"

+ 51 - 23
Common/HttpParser.swift

@@ -33,13 +33,13 @@ class HttpParser {
             let statusLine = try nextLine(socket)
             let statusTokens = statusLine.componentsSeparatedByString(" ")
             print(statusTokens)
-            if (statusTokens.count < 3) {
+            if statusTokens.count < 3 {
                 throw HttpParserError.InvalidStatusLine(statusLine)
             }
             let method = statusTokens[0]
             let path = statusTokens[1]
-            let urlParams = extractUrlParams(path)
-            // TODO extract query parameters
+            let urlParams = self.extractUrlParams(path)
+            
             let headers = try nextHeaders(socket)
             // TODO detect content-type and handle:
             // 'application/x-www-form-urlencoded' -> Dictionary
@@ -48,36 +48,64 @@ class HttpParser {
             if let contentLengthString = headers["content-length"],
                 let contentLength = Int(contentLengthString) {
                     body = try nextBody(socket, size: contentLength)
+                    
+//                    self.extractBodyParams(body!)
             } else {
                 body = nil
             }
+            
             return HttpRequest(url: path, urlParams: urlParams, method: method, headers: headers, body: body, capturedUrlGroups: [], address: nil)
         } catch {
             throw error
         }
     }
     
-    private func extractUrlParams(url: String) -> [(String, String)] {
-        if let query = url.componentsSeparatedByString("?").last {
-            return query.componentsSeparatedByString("&").map { (param:String) -> (String, String) in
-                let tokens = param.componentsSeparatedByString("=")
-                if tokens.count >= 2 {
-                    let key = tokens[0].stringByRemovingPercentEncoding
-                    let value = tokens[1].stringByRemovingPercentEncoding
-                    if key != nil && value != nil { return (key!, value!) }
+    private func extractBodyParams(body: String) -> [String: Parameter] {
+        let params = body.componentsSeparatedByString("&").map { (param:String) -> (String, String) in
+            let tokens = param.componentsSeparatedByString("=")
+            if tokens.count >= 2 {
+                let key = tokens[0].stringByRemovingPercentEncoding
+                let value = tokens[1].stringByRemovingPercentEncoding
+                if key != nil && value != nil { return (key!, value!) }
+            }
+            return ("","")
+        }
+        
+        var result = [String: Parameter]()
+        
+        for (key, value) in params {
+            if let val = result[key] {
+                switch val {
+                case .StringValue(let stringValue):
+                    result[key] = Parameter.ArrayString([stringValue, value])
+                    
+                case .ArrayString(var arrayValue):
+                    arrayValue.append(value)
+                    result[key] = Parameter.ArrayString(arrayValue)
                 }
-                return ("","")
+            } else {
+                result[key] = Parameter.StringValue(value)
             }
         }
-        return []
+        
+        return result
+
+    }
+    
+    private func extractUrlParams(url: String) -> [String : Parameter] {
+        guard let query = url.componentsSeparatedByString("?").last else {
+            return [:]
+        }
+        
+        return self.extractBodyParams(query)
     }
     
     private func nextBody(socket: CInt, size: Int) throws -> String {
         var body = ""
         var counter = 0;
-        while (counter < size) {
+        while counter < size {
             let c = nextInt8(socket)
-            if (c < 0) {
+            if c < 0 {
                 throw HttpParserError.ReadBodyFailed
             }
             body.append(UnicodeScalar(c))
@@ -86,20 +114,20 @@ class HttpParser {
         return body
     }
     
-    private func nextHeaders(socket: CInt) throws -> Dictionary<String, String> {
-        var headers = Dictionary<String, String>()
+    private func nextHeaders(socket: CInt) throws -> [String: String] {
+        var headers = [String: String]()
         while let headerLine = try? nextLine(socket) {
-            if ( headerLine.isEmpty ) {
+            if headerLine.isEmpty {
                 return headers
             }
             let headerTokens = headerLine.componentsSeparatedByString(":")
-            if ( headerTokens.count >= 2 ) {
+            if headerTokens.count >= 2 {
                 // RFC 2616 - "Hypertext Transfer Protocol -- HTTP/1.1", paragraph 4.2, "Message Headers":
                 // "Each header field consists of a name followed by a colon (":") and the field value. Field names are case-insensitive."
                 // We can keep lower case version.
                 let headerName = headerTokens[0].lowercaseString
                 let headerValue = headerTokens[1].stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet())
-                if ( !headerName.isEmpty && !headerValue.isEmpty ) {
+                if !headerName.isEmpty && !headerValue.isEmpty {
                     headers.updateValue(headerValue, forKey: headerName)
                 }
             }
@@ -120,14 +148,14 @@ class HttpParser {
         repeat {
             n = nextInt8(socket)
             if ( n > 13 /* CR */ ) { characters.append(Character(UnicodeScalar(n))) }
-        } while ( n > 0 && n != 10 /* NL */)
-        if ( n == -1 && characters.isEmpty ) {
+        } while n > 0 && n != 10 /* NL */
+        if n == -1 && characters.isEmpty {
             throw HttpParserError.RecvFailed
         }
         return characters
     }
     
-    func supportsKeepAlive(headers: Dictionary<String, String>) -> Bool {
+    func supportsKeepAlive(headers: [String: String]) -> Bool {
         if let value = headers["connection"] {
             return "keep-alive" == value.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet()).lowercaseString
         }

+ 6 - 1
Common/HttpRequest.swift

@@ -8,10 +8,15 @@ import Foundation
 
 public struct HttpRequest {
     public let url: String
-    public let urlParams: [(String, String)] // http://stackoverflow.com/questions/1746507/authoritative-position-of-duplicate-http-get-query-keys
+    public let urlParams: [String: Parameter] // http://stackoverflow.com/questions/1746507/authoritative-position-of-duplicate-http-get-query-keys
     public let method: String
     public let headers: [String: String]
 	public let body: String?
     public var capturedUrlGroups: [String]
     public var address: String?
 }
+
+public enum Parameter {
+    case StringValue(String)
+    case ArrayString([String])
+}

+ 8 - 4
Common/HttpResponse.swift

@@ -16,6 +16,7 @@ public enum HttpResponseBody {
     
     func data() -> String? {
         switch self {
+            
         case .JSON(let object):
             if NSJSONSerialization.isValidJSONObject(object) {
                 do {
@@ -28,8 +29,10 @@ public enum HttpResponseBody {
                 }
             }
             return "Invalid object to serialise."
+            
         case .XML(_):
             return "XML serialization not supported."
+            
         case .PLIST(let object):
             let format = NSPropertyListFormat.XMLFormat_v1_0
             if NSPropertyListSerialization.propertyList(object, isValidForFormat: format) {
@@ -43,8 +46,10 @@ public enum HttpResponseBody {
                 }
             }
             return "Invalid object to serialise."
+            
         case .STRING(let body):
             return body
+            
         case .HTML(let body):
             return "<html><body>\(body)</body></html>"
         }
@@ -96,15 +101,14 @@ public enum HttpResponse {
 		case .OK(let body):
             switch body {
                 case .JSON(_)   : headers["Content-Type"] = "application/json"
-                case .PLIST(_)  : headers["Content-Type"] = "application/xml"
-                case .XML(_)    : headers["Content-Type"] = "application/xml"
+                case .PLIST(_), .XML(_)  : headers["Content-Type"] = "application/xml"
                 // 'application/xml' vs 'text/xml'
                 // From RFC: http://www.rfc-editor.org/rfc/rfc3023.txt - "If an XML document -- that is, the unprocessed, source XML document -- is readable by casual users,
                 // text/xml is preferable to application/xml. MIME user agents (and web user agents) that do not have explicit 
                 // support for text/xml will treat it as text/plain, for example, by displaying the XML MIME entity as plain text. 
                 // Application/xml is preferable when the XML MIME entity is unreadable by casual users."
                 case .HTML(_)   : headers["Content-Type"] = "text/html"
-                default:[]
+                default:break
             }
         case .MovedPermanently(let location): headers["Location"] = location
         case .RAW(_,_, let rawHeaders,_):
@@ -113,7 +117,7 @@ public enum HttpResponse {
                     headers.updateValue(v, forKey: k)
                 }
             }
-        default:[]
+        default:break
         }
         return headers
     }

+ 25 - 18
Common/HttpServer.swift

@@ -12,10 +12,10 @@ public class HttpServer
     
     public typealias Handler = HttpRequest -> HttpResponse
     
-    var handlers: [(expression: NSRegularExpression, handler: Handler)] = []
-    var clientSockets: Set<CInt> = []
+    private(set) var handlers: [(expression: NSRegularExpression, handler: Handler)] = []
+    private(set) var clientSockets: Set<CInt> = []
     let clientSocketsLock = 0
-    var acceptSocket: CInt = -1
+    private(set) var acceptSocket: CInt = -1
     
     let matchingOptions = NSMatchingOptions(rawValue: 0)
     let expressionOptions = NSRegularExpressionOptions(rawValue: 0)
@@ -30,15 +30,17 @@ public class HttpServer
         set {
             if let newHandler = newValue,
                let regex = try? NSRegularExpression(pattern: path, options: expressionOptions){
-                handlers.append(expression: regex, handler: newHandler)
+                self.handlers.append(expression: regex, handler: newHandler)
             }
         }
     }
     
-    public func routes() -> [String] { return handlers.map { $0.0.pattern } }
+    public var routes:[String] {
+        return self.handlers.map { $0.0.pattern }
+    }
     
     public func start(listenPort: in_port_t = 8080) throws {
-        stop()
+        self.stop()
         do {
             let socket = try Socket.tcpForListen(listenPort)
             self.acceptSocket = socket
@@ -80,36 +82,40 @@ public class HttpServer
     }
     
     func findHandler(url:String) -> (NSRegularExpression, Handler)? {
-		let u = NSURL(string: url)!
-		let path = u.path!
-		for handler in self.handlers {
-			let regex = handler.0
-            let matches = regex.numberOfMatchesInString(path, options: self.matchingOptions, range: HttpServer.asciiRange(path)) > 0
-			if matches {
-				return 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
     }
     
     func captureExpressionGroups(expression: NSRegularExpression, value: String) -> [String] {
-		let u = NSURL(string: value)!
-		let path = u.path!
         var capturedGroups = [String]()
+        
+        guard let u = NSURL(string: value),
+                  path = u.path else {
+                return capturedGroups
+        }
+        
         if let result = expression.firstMatchInString(path, options: matchingOptions, range: HttpServer.asciiRange(path)) {
             let nsValue: NSString = path
-            for var i = 1 ; i < result.numberOfRanges ; ++i {
+            for i in 1..<result.numberOfRanges {
                 if let group = nsValue.substringWithRange(result.rangeAtIndex(i)).stringByRemovingPercentEncoding {
                     capturedGroups.append(group)
                 }
             }
+//            for var i = 1 ; i < result.numberOfRanges ; ++i {
         }
         return capturedGroups
     }
     
     public func stop() {
         Socket.release(acceptSocket)
-        acceptSocket = -1
+        self.acceptSocket = -1
         HttpServer.lock(self.clientSocketsLock) {
             for clientSocket in self.clientSockets {
                 Socket.release(clientSocket)
@@ -147,6 +153,7 @@ public class HttpServer
             }
         } catch {
             // TODO: handle error
+            print("error responding to client")
         }
         
     }

+ 10 - 9
Common/Socket.swift

@@ -49,14 +49,15 @@ enum SocketError: ErrorType {
 
 let maxPendingConnection: Int32 = 20
 
-struct Socket {    
+struct Socket {
     static func tcpForListen(port: in_port_t = 8080) throws -> CInt {
         let s = socket(AF_INET, SOCK_STREAM, 0)
-        if ( s == -1 ) {
+        if s == -1 {
             throw SocketError.SocketInitializationFailed
         }
-        var value: Int32 = 1;
-        if ( setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &value, socklen_t(sizeof(Int32))) == -1 ) {
+        var value: Int32 = 1
+        
+        if setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &value, socklen_t(sizeof(Int32))) == -1 {
             release(s)
             throw SocketError.SocketOptionInitializationFailed
         }
@@ -69,11 +70,11 @@ struct Socket {
         
         var sock_addr = sockaddr(sa_len: 0, sa_family: 0, sa_data: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0))
         memcpy(&sock_addr, &addr, Int(sizeof(sockaddr_in)))
-        if ( bind(s, &sock_addr, socklen_t(sizeof(sockaddr_in))) == -1 ) {
+        if bind(s, &sock_addr, socklen_t(sizeof(sockaddr_in))) == -1 {
             release(s)
             throw SocketError.BindFailed
         }
-        if ( listen(s, maxPendingConnection ) == -1 ) {
+        if listen(s, maxPendingConnection ) == -1 {
             release(s)
             throw SocketError.ListenFailed
         }
@@ -103,9 +104,9 @@ struct Socket {
     static func writeData(socket: CInt, data: NSData) throws {
         var sent = 0
         let unsafePointer = UnsafePointer<UInt8>(data.bytes)
-        while ( sent < data.length ) {
+        while sent < data.length {
             let s = write(socket, unsafePointer + sent, Int(data.length - sent))
-            if ( s <= 0 ) {
+            if s <= 0 {
                 throw SocketError.WriteFailed
             }
             sent += s
@@ -116,7 +117,7 @@ struct Socket {
         var addr = sockaddr(sa_len: 0, sa_family: 0, sa_data: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0))
         var len: socklen_t = 0
         let clientSocket = accept(socket, &addr, &len)
-        if ( clientSocket == -1 ) {
+        if clientSocket == -1 {
             throw SocketError.AcceptFailed
         }
         Socket.nosigpipe(clientSocket)

+ 25 - 0
Common/TestSocket.swift

@@ -0,0 +1,25 @@
+//
+//  TestSocket.swift
+//  Swifter
+//
+//  Created by Clément Nonn on 16/10/2015.
+//  Copyright © 2015 Damian Kołakowski. All rights reserved.
+//
+
+import Foundation
+//http://socket.io/get-started/chat/
+func testSocket(publicDir: String) -> HttpServer {
+    let server = HttpServer()
+
+    server["/resources/(.+)"] = HttpHandlers.directory(publicDir)
+    
+    server["/"] = { request in
+        if let html = NSData(contentsOfFile:"\(publicDir)/index.html") {
+            return HttpResponse.RAW(200, "OK", nil, html)
+        } else {
+            return .NotFound
+        }
+    }
+    
+    return server
+}

+ 22 - 0
Resources/index.html

@@ -0,0 +1,22 @@
+<!doctype html>
+<html>
+    <head>
+        <title>SwiftyIO chat</title>
+        <style>
+            * { margin: 0; padding: 0; box-sizing: border-box; }
+            body { font: 13px Helvetica, Arial; }
+            form { background: #000; padding: 3px; position: fixed; bottom: 0; width: 100%; }
+            form input { border: 0; padding: 10px; width: 90%; margin-right: .5%; }
+            form button { width: 9%; background: rgb(130, 224, 255); border: none; padding: 10px; }
+            #messages { list-style-type: none; margin: 0; padding: 0; }
+            #messages li { padding: 5px 10px; }
+            #messages li:nth-child(odd) { background: #eee; }
+            </style>
+    </head>
+    <body>
+        <ul id="messages"></ul>
+        <form action="">
+            <input id="m" autocomplete="off" /><button>Send</button>
+        </form>
+    </body>
+</html>

+ 12 - 0
Swifter.xcodeproj/project.pbxproj

@@ -7,6 +7,10 @@
 	objects = {
 
 /* Begin PBXBuildFile section */
+		18447C3F1BD14CF50011C707 /* TestSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18447C3E1BD14CF50011C707 /* TestSocket.swift */; settings = {ASSET_TAGS = (); }; };
+		18447C411BD14E400011C707 /* index.html in Resources */ = {isa = PBXBuildFile; fileRef = 18447C401BD14E400011C707 /* index.html */; settings = {ASSET_TAGS = (); }; };
+		187F2CB71BD1501700AC8474 /* TestSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18447C3E1BD14CF50011C707 /* TestSocket.swift */; settings = {ASSET_TAGS = (); }; };
+		187F2CB81BD1511500AC8474 /* index.html in CopyFiles */ = {isa = PBXBuildFile; fileRef = 18447C401BD14E400011C707 /* index.html */; };
 		7C71C5B01A1D52F800682BF0 /* login.html in CopyFiles */ = {isa = PBXBuildFile; fileRef = 98630C061A1C9A9D00478D08 /* login.html */; };
 		7C71C5B11A1EC49B00682BF0 /* logo.png in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7CB102DF1A17381D00CBA3B4 /* logo.png */; };
 		7C839B7419422CFF003A6950 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C839B7319422CFF003A6950 /* AppDelegate.swift */; };
@@ -42,6 +46,7 @@
 			dstSubfolderSpec = 7;
 			files = (
 				7C71C5B11A1EC49B00682BF0 /* logo.png in CopyFiles */,
+				187F2CB81BD1511500AC8474 /* index.html in CopyFiles */,
 				7C71C5B01A1D52F800682BF0 /* login.html in CopyFiles */,
 				7CA4815919A2EF560030B30D /* test.json in CopyFiles */,
 			);
@@ -50,6 +55,8 @@
 /* End PBXCopyFilesBuildPhase section */
 
 /* Begin PBXFileReference section */
+		18447C3E1BD14CF50011C707 /* TestSocket.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestSocket.swift; sourceTree = "<group>"; };
+		18447C401BD14E400011C707 /* index.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = index.html; sourceTree = "<group>"; };
 		7C839B6E19422CFF003A6950 /* Swifter.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Swifter.app; sourceTree = BUILT_PRODUCTS_DIR; };
 		7C839B7219422CFF003A6950 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
 		7C839B7319422CFF003A6950 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
@@ -146,6 +153,7 @@
 				7CA4814C19A2EED00030B30D /* HttpServer.swift */,
 				7CA4814D19A2EED00030B30D /* Socket.swift */,
 				7CB102DC1A167FFA00CBA3B4 /* DemoServer.swift */,
+				18447C3E1BD14CF50011C707 /* TestSocket.swift */,
 			);
 			path = Common;
 			sourceTree = "<group>";
@@ -156,6 +164,7 @@
 				98630C061A1C9A9D00478D08 /* login.html */,
 				7CB102DF1A17381D00CBA3B4 /* logo.png */,
 				7CA4815719A2EF2B0030B30D /* test.json */,
+				18447C401BD14E400011C707 /* index.html */,
 			);
 			path = Resources;
 			sourceTree = "<group>";
@@ -242,6 +251,7 @@
 				7CB102E01A17381D00CBA3B4 /* logo.png in Resources */,
 				7C839B7919422CFF003A6950 /* Main.storyboard in Resources */,
 				98630C071A1C9A9D00478D08 /* login.html in Resources */,
+				18447C411BD14E400011C707 /* index.html in Resources */,
 				7C839B7B19422CFF003A6950 /* Images.xcassets in Resources */,
 				7CA4815819A2EF2B0030B30D /* test.json in Resources */,
 			);
@@ -258,6 +268,7 @@
 				7CB102DA1A1664B200CBA3B4 /* HttpHandlers.swift in Sources */,
 				7CB102DD1A167FFA00CBA3B4 /* DemoServer.swift in Sources */,
 				7C839B7619422CFF003A6950 /* ViewController.swift in Sources */,
+				18447C3F1BD14CF50011C707 /* TestSocket.swift in Sources */,
 				7CA4815419A2EED00030B30D /* Socket.swift in Sources */,
 				7CA4815219A2EED00030B30D /* HttpServer.swift in Sources */,
 				7CA4814E19A2EED00030B30D /* HttpParser.swift in Sources */,
@@ -276,6 +287,7 @@
 				7CA4815519A2EED00030B30D /* Socket.swift in Sources */,
 				7CA4815319A2EED00030B30D /* HttpServer.swift in Sources */,
 				7CA4813E19A2EA8D0030B30D /* main.swift in Sources */,
+				187F2CB71BD1501700AC8474 /* TestSocket.swift in Sources */,
 				7CB102DB1A16657200CBA3B4 /* HttpHandlers.swift in Sources */,
 				7CA4815C19A2F6A60030B30D /* HttpRequest.swift in Sources */,
 			);

+ 1 - 1
SwifterOSX/main.swift

@@ -7,7 +7,7 @@
 
 import Foundation
 
-let server = demoServer(NSBundle.mainBundle().resourcePath)
+let server = testSocket(NSBundle.mainBundle().resourcePath!)
 do {
     try server.start(9080)
     print("Server started. Try a connection now...")