diff --git a/README.md b/README.md index 937d47db..9d2e79b3 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,13 @@ server["/websocket-echo"] = websocket(text: { session, text in }) server.start() ``` +### How to TLS/SSL? +Currently only supported on Darwin OS +```swift +let server = HttpServer() +server.sslCertificate = TLS.loadP12Certificate(certificateData, certificatePassword) +server.start() +``` ### CocoaPods? Yes. ```ruby use_frameworks! diff --git a/XCode/Sources/Errno.swift b/XCode/Sources/Errno.swift index 3657b95e..113dbdbd 100644 --- a/XCode/Sources/Errno.swift +++ b/XCode/Sources/Errno.swift @@ -13,4 +13,24 @@ public class Errno { // https://forums.developer.apple.com/thread/113919 return String(cString: strerror(errno)) } + + #if !os(Linux) + public class func sslError(from status: OSStatus) -> Error { + guard let msg = getMessage(from: status) else { + return SocketError.tlsSessionFailed("<\(status): message is not provided>") + } + return SocketError.tlsSessionFailed(msg) + } + + private class func getMessage(from status: OSStatus) -> String? { + if #available(iOS 11.3, tvOS 11.3, *) { + guard let msg = SecCopyErrorMessageString(status, nil) else { + return nil + } + return msg as String + } else { + return "SSL error (\(status))" + } + } + #endif } diff --git a/XCode/Sources/HttpServerIO.swift b/XCode/Sources/HttpServerIO.swift index 339b235a..3cc90fdf 100644 --- a/XCode/Sources/HttpServerIO.swift +++ b/XCode/Sources/HttpServerIO.swift @@ -53,6 +53,11 @@ public class HttpServerIO { /// Otherwise, `listenAddressIPv4` will be used. public var listenAddressIPv6: String? + #if !os(Linux) + /// SSL certificate to use in TLS session + public var sslCertificate: CFArray? + #endif + private let queue = DispatchQueue(label: "swifter.httpserverio.clientsockets") public func port() throws -> Int { @@ -116,6 +121,19 @@ public class HttpServerIO { } private func handleConnection(_ socket: Socket) { + defer { + socket.close() + } + #if !os(Linux) + if let cert = sslCertificate { + do { + try socket.startTlsSession(with: cert) + } catch { + print("Failed to start TLS session: \(error)") + return + } + } + #endif let parser = HttpParser() while self.operating, let request = try? parser.readHttpRequest(socket) { let request = request @@ -139,7 +157,6 @@ public class HttpServerIO { } if !keepConnection { break } } - socket.close() } private struct InnerWriteContext: HttpResponseBodyWriter { diff --git a/XCode/Sources/Socket.swift b/XCode/Sources/Socket.swift index 590360c7..45896857 100644 --- a/XCode/Sources/Socket.swift +++ b/XCode/Sources/Socket.swift @@ -10,6 +10,9 @@ import Foundation public enum SocketError: Error { case socketCreationFailed(String) case socketSettingReUseAddrFailed(String) + #if !os(Linux) + case tlsSessionFailed(String) + #endif case bindFailed(String) case listenFailed(String) case writeFailed(String) @@ -25,6 +28,9 @@ public enum SocketError: Error { open class Socket: Hashable, Equatable { let socketFileDescriptor: Int32 + #if !os(Linux) + private var tls: TlsSession? + #endif private var shutdown = false public init(socketFileDescriptor: Int32) { @@ -43,10 +49,20 @@ open class Socket: Hashable, Equatable { if shutdown { return } + #if !os(Linux) + tls?.close() + #endif shutdown = true Socket.close(self.socketFileDescriptor) } + #if !os(Linux) + public func startTlsSession(with certificate: CFArray) throws { + tls = try TlsSession(fd: socketFileDescriptor, certificate: certificate) + try tls?.handshake() + } + #endif + public func port() throws -> in_port_t { var addr = sockaddr_in() return try withUnsafePointer(to: &addr) { pointer in @@ -110,6 +126,13 @@ open class Socket: Hashable, Equatable { private func writeBuffer(_ pointer: UnsafeRawPointer, length: Int) throws { var sent = 0 while sent < length { + #if !os(Linux) + if let ssl = tls { + sent += try ssl.writeBuffer(pointer + sent, length: Int(length - sent)) + continue + } + #endif + #if os(Linux) let result = send(self.socketFileDescriptor, pointer + sent, Int(length - sent), Int32(MSG_NOSIGNAL)) #else @@ -131,6 +154,13 @@ open class Socket: Hashable, Equatable { open func read() throws -> UInt8 { var byte: UInt8 = 0 + #if !os(Linux) + if let ssl = tls { + try ssl.readByte(&byte) + return byte + } + #endif + #if os(Linux) let count = Glibc.read(self.socketFileDescriptor as Int32, &byte, 1) #else @@ -174,6 +204,13 @@ open class Socket: Hashable, Equatable { // Compute next read length in bytes. The bytes read is never more than kBufferLength at once. let readLength = offset + Socket.kBufferLength < length ? Socket.kBufferLength : length - offset + #if !os(Linux) + if let ssl = tls { + offset += try ssl.read(into: baseAddress + offset, length: readLength) + continue + } + #endif + #if os(Linux) let bytesRead = Glibc.read(self.socketFileDescriptor as Int32, baseAddress + offset, readLength) #else diff --git a/XCode/Sources/TlsSession.swift b/XCode/Sources/TlsSession.swift new file mode 100644 index 00000000..8a01e0cd --- /dev/null +++ b/XCode/Sources/TlsSession.swift @@ -0,0 +1,148 @@ +// +// HttpRouter.swift +// Swifter +// +// Copyright (c) 2014-2016 Damian KoĊ‚akowski. All rights reserved. +// + +import Foundation + +#if !os(Linux) +private func ensureNoErr(_ status: OSStatus) throws { + guard status == noErr else { + throw Errno.sslError(from: status) + } +} + +public enum TLS { + /// Imports .p12 certificate file constructing structure to be used in TLS session. + /// + /// See [SecPKCS12Import](https://developer.apple.com/documentation/security/1396915-secpkcs12import). + /// Apple docs contain a misleading information that it does not import items to Keychain even though + /// it does. + /// + /// - Parameter data: .p12 certificate file content + /// - Parameter password: password used when importing certificate + public static func loadP12Certificate(_ data: Data, _ password: String) throws -> CFArray { + let options = [kSecImportExportPassphrase as String: password] + var items: CFArray? + try ensureNoErr(SecPKCS12Import(data as CFData, options as NSDictionary, &items)) + guard + let dictionary = (items as? [[String: Any]])?.first, + let chain = dictionary[kSecImportItemCertChain as String] as? [SecCertificate] + else { + throw SocketError.tlsSessionFailed("Could not retrieve p12 data from given certificate") + } + // must be force casted, will be fixed in swift 5 https://bugs.swift.org/browse/SR-7015 + // swiftlint:disable force_cast + let secIdentity = dictionary[kSecImportItemIdentity as String] as! SecIdentity + // swiftlint:enable force_cast + let chainWithoutIdentity = chain.dropFirst() + let certs = [secIdentity] + chainWithoutIdentity.map { $0 as Any } + return certs as CFArray + } +} + +open class TlsSession { + + private let context: SSLContext + private var fdPtr = UnsafeMutablePointer.allocate(capacity: 1) + + init(fd: Int32, certificate: CFArray) throws { + guard let newContext = SSLCreateContext(nil, .serverSide, .streamType) else { + throw SocketError.tlsSessionFailed("Could not create new SSL context") + } + context = newContext + fdPtr.pointee = fd + try ensureNoErr(SSLSetIOFuncs(context, sslRead, sslWrite)) + try ensureNoErr(SSLSetConnection(context, fdPtr)) + try ensureNoErr(SSLSetCertificate(context, certificate)) + } + + open func close() { + SSLClose(context) + fdPtr.deallocate() + } + + open func handshake() throws { + var status: OSStatus = -1 + repeat { + status = SSLHandshake(context) + } while status == errSSLWouldBlock + try ensureNoErr(status) + } + + /// Write up to `length` bytes to TLS session from a buffer `pointer` points to. + /// + /// - Returns: The number of bytes written + /// - Throws: SocketError.tlsSessionFailed if unable to write to the session + open func writeBuffer(_ pointer: UnsafeRawPointer, length: Int) throws -> Int { + var written = 0 + try ensureNoErr(SSLWrite(context, pointer, length, &written)) + return written + } + + /// Read a single byte off the TLS session. + /// + /// - Throws: SocketError.tlsSessionFailed if unable to read from the session + open func readByte(_ byte: UnsafeMutablePointer) throws { + _ = try read(into: byte, length: 1) + } + + /// Read up to `length` bytes from TLS session into an existing buffer + /// + /// - Parameter into: The buffer to read into (must be at least length bytes in size) + /// - Returns: The number of bytes read + /// - Throws: SocketError.tlsSessionFailed if unable to read from the session + open func read(into buffer: UnsafeMutablePointer, length: Int) throws -> Int { + var received = 0 + try ensureNoErr(SSLRead(context, buffer, length, &received)) + return received + } +} + +private func sslWrite(connection: SSLConnectionRef, data: UnsafeRawPointer, dataLength: UnsafeMutablePointer) -> OSStatus { + let fd = connection.assumingMemoryBound(to: Int32.self).pointee + let bytesToWrite = dataLength.pointee + + let written = Darwin.write(fd, data, bytesToWrite) + + dataLength.pointee = written + if written > 0 { + return written < bytesToWrite ? errSSLWouldBlock : noErr + } + if written == 0 { + return errSSLClosedGraceful + } + + dataLength.pointee = 0 + return errno == EAGAIN ? errSSLWouldBlock : errSecIO +} + +private func sslRead(connection: SSLConnectionRef, data: UnsafeMutableRawPointer, dataLength: UnsafeMutablePointer) -> OSStatus { + let fd = connection.assumingMemoryBound(to: Int32.self).pointee + let bytesToRead = dataLength.pointee + let read = recv(fd, data, bytesToRead, 0) + + dataLength.pointee = read + if read > 0 { + return read < bytesToRead ? errSSLWouldBlock : noErr + } + + if read == 0 { + return errSSLClosedGraceful + } + + dataLength.pointee = 0 + switch errno { + case ENOENT: + return errSSLClosedGraceful + case EAGAIN: + return errSSLWouldBlock + case ECONNRESET: + return errSSLClosedAbort + default: + return errSecIO + } +} +#endif diff --git a/XCode/Swifter.xcodeproj/project.pbxproj b/XCode/Swifter.xcodeproj/project.pbxproj index ee198128..3ef4d172 100644 --- a/XCode/Swifter.xcodeproj/project.pbxproj +++ b/XCode/Swifter.xcodeproj/project.pbxproj @@ -7,6 +7,9 @@ objects = { /* Begin PBXBuildFile section */ + 039CE04222F32BA600C9788F /* TlsSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039CE04122F32BA600C9788F /* TlsSession.swift */; }; + 039CE04322F32BA600C9788F /* TlsSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039CE04122F32BA600C9788F /* TlsSession.swift */; }; + 039CE04422F32BA600C9788F /* TlsSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039CE04122F32BA600C9788F /* TlsSession.swift */; }; 043660C721FED34100497989 /* Swifter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7AE893FB1C0512C400A29F63 /* Swifter.framework */; }; 043660CD21FED35200497989 /* SwifterTestsHttpRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CCB8C5F1D97B8CC008B9712 /* SwifterTestsHttpRouter.swift */; }; 043660CE21FED35500497989 /* SwifterTestsHttpParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CCD876D1C660B250068099B /* SwifterTestsHttpParser.swift */; }; @@ -164,6 +167,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 039CE04122F32BA600C9788F /* TlsSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TlsSession.swift; sourceTree = ""; }; 043660C221FED34100497989 /* SwiftermacOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SwiftermacOSTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 043660C621FED34100497989 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 043660DA21FED3A300497989 /* SwiftertvOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SwiftertvOSTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -353,6 +357,7 @@ 7C76B2A11D369C9D00D35BFB /* Errno.swift */, 7C377E161D964B6A009C6148 /* String+File.swift */, 6AE2FF702048011A00302EC4 /* MimeTypes.swift */, + 039CE04122F32BA600C9788F /* TlsSession.swift */, ); path = Sources; sourceTree = ""; @@ -818,6 +823,7 @@ 269B47901D3AAAE20042D137 /* DemoServer.swift in Sources */, 269B47921D3AAAE20042D137 /* Socket+File.swift in Sources */, 269B47931D3AAAE20042D137 /* Socket.swift in Sources */, + 039CE04422F32BA600C9788F /* TlsSession.swift in Sources */, 269B47941D3AAAE20042D137 /* HttpServerIO.swift in Sources */, 269B47951D3AAAE20042D137 /* Files.swift in Sources */, 2659FC1A1DADC077003F3930 /* String+File.swift in Sources */, @@ -845,6 +851,7 @@ 7C76B7151D2C45760030FC98 /* HttpRequest.swift in Sources */, 7C76B70D1D2C456A0030FC98 /* DemoServer.swift in Sources */, 7C76B29F1D369BEC00D35BFB /* Socket+File.swift in Sources */, + 039CE04222F32BA600C9788F /* TlsSession.swift in Sources */, 7C76B7231D2C45890030FC98 /* Socket.swift in Sources */, 7C76B71D1D2C45820030FC98 /* HttpServerIO.swift in Sources */, 7C76B7111D2C45710030FC98 /* Files.swift in Sources */, @@ -872,6 +879,7 @@ 7C76B7161D2C45760030FC98 /* HttpRequest.swift in Sources */, 7C76B70E1D2C456B0030FC98 /* DemoServer.swift in Sources */, 7C76B2A01D369BEC00D35BFB /* Socket+File.swift in Sources */, + 039CE04322F32BA600C9788F /* TlsSession.swift in Sources */, 7C76B7241D2C458A0030FC98 /* Socket.swift in Sources */, 7C76B71E1D2C45820030FC98 /* HttpServerIO.swift in Sources */, 7C76B7121D2C45710030FC98 /* Files.swift in Sources */, diff --git a/XCode/SwifterSampleOSX/main.swift b/XCode/SwifterSampleOSX/main.swift index 76d74524..b57331c8 100644 --- a/XCode/SwifterSampleOSX/main.swift +++ b/XCode/SwifterSampleOSX/main.swift @@ -7,6 +7,23 @@ import Foundation import Swifter +/// For demo purposes certificate is expected to be at location +/// ~/.swifter/localhost.p12 +/// +/// the easiest way to create certificate for localhost is using `mkcert` tool +private func certificateData() -> Data? { + guard let homePath = ProcessInfo.processInfo.environment["HOME"] else { + return nil + } + guard let homeUrl = URL(string: homePath) else { + return nil + } + let certPath = homeUrl + .appendingPathComponent(".swifter", isDirectory: true) + .appendingPathComponent("localhost.p12", isDirectory: false) + return FileManager.default.contents(atPath: certPath.absoluteString) +} + do { let server = demoServer(try String.File.currentWorkingDirectory()) server["/testAfterBaseRoute"] = { request in @@ -14,6 +31,10 @@ do { } if #available(OSXApplicationExtension 10.10, *) { + if let certData = certificateData() { + server.sslCertificate = try TLS.loadP12Certificate(certData, "changeit") + print("SSL certificate loaded") + } try server.start(9080, forceIPv4: true) } else { // Fallback on earlier versions