Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SSL support #427

Open
wants to merge 10 commits into
base: stable
Choose a base branch
from
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down
20 changes: 20 additions & 0 deletions XCode/Sources/Errno.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
19 changes: 18 additions & 1 deletion XCode/Sources/HttpServerIO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -139,7 +157,6 @@ public class HttpServerIO {
}
if !keepConnection { break }
}
socket.close()
}

private struct InnerWriteContext: HttpResponseBodyWriter {
Expand Down
37 changes: 37 additions & 0 deletions XCode/Sources/Socket.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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) {
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
148 changes: 148 additions & 0 deletions XCode/Sources/TlsSession.swift
Original file line number Diff line number Diff line change
@@ -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<Int32>.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<UInt8>) 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<UInt8>, 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<Int>) -> 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<Int>) -> 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
8 changes: 8 additions & 0 deletions XCode/Swifter.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -164,6 +167,7 @@
/* End PBXCopyFilesBuildPhase section */

/* Begin PBXFileReference section */
039CE04122F32BA600C9788F /* TlsSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TlsSession.swift; sourceTree = "<group>"; };
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 = "<group>"; };
043660DA21FED3A300497989 /* SwiftertvOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SwiftertvOSTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -353,6 +357,7 @@
7C76B2A11D369C9D00D35BFB /* Errno.swift */,
7C377E161D964B6A009C6148 /* String+File.swift */,
6AE2FF702048011A00302EC4 /* MimeTypes.swift */,
039CE04122F32BA600C9788F /* TlsSession.swift */,
);
path = Sources;
sourceTree = "<group>";
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down
21 changes: 21 additions & 0 deletions XCode/SwifterSampleOSX/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,34 @@
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
return .ok(.htmlBody("ok !"))
}

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
Expand Down