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
9 changes: 9 additions & 0 deletions XCode/Sources/Errno.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,13 @@ 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 = SecCopyErrorMessageString(status, nil) else {
return SocketError.tlsSessionFailed("<\(status): message is not provided>")
}
return SocketError.tlsSessionFailed(msg as NSString as String)
}
#endif
Copy link
Member

@Vkt0r Vkt0r Nov 5, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SecCopyErrorMessageString is only available for iOS 11.3+ and we're supporting iOS 8+, not sure which is the counterpart here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! Very weird that Xcode didn't notify me.
Could not find any counterpart so formed custom message.

}
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
138 changes: 138 additions & 0 deletions XCode/Sources/TlsSession.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
//
// 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 data = _data as NSData
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SecPKCS12Import(_:_:_:) is expecting a CFData here, what would be the difference between passing an NSData object instead of a CFData here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of Core Foundation types are toll-free bridged to their Cocoa Foundation counterparts and can be used interchangeably. But I agree, it's better to use actually expected type names here. Will fix that.

/cc https://developer.apple.com/documentation/corefoundation/cfdata-rv9
/cc https://developer.apple.com/library/archive/documentation/General/Conceptual/CocoaEncyclopedia/Toll-FreeBridgin/Toll-FreeBridgin.html

let options = [kSecImportExportPassphrase: password]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the way Apple does it in the page you referenced they make a cast to String like this:

 let options = [ kSecImportExportPassphrase as String: password ]

Do we know what difference could generate?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related to same discussion of String, NSString and CFString I believe they're all the same.

var items: CFArray!
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we please make this CFArray optional in order to avoid the force-unwrapping in the next lines?

try ensureNoErr(SecPKCS12Import(data, options as NSDictionary, &items))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SecPKCS12Import(_:_:_:) is expecting a UnsafeMutablePointer<CFArray?>What are differences here between pass a UnsafeMutablePointer<CFArray?> and raw CFArray?. Wouldn't be more type-safe to pass a type like this:

try ensureNoErr(withUnsafeMutablePointer(to: &items) { SecPKCS12Import(data, options as CFDictionary, $0) })

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not fully sure what &items generates but I assume it's something like a pointer to CFArray. Used https://developer.apple.com/documentation/security/certificate_key_and_trust_services/identities/importing_an_identity as an example.

let dictionary = (items! as [AnyObject])[0]
Copy link
Member

@Vkt0r Vkt0r Aug 22, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The items would be of [[String: Any]], can we please cast it as optional and use firsts instead of the first index directly? Something like this maybe:

guard let dictionary = (items as? [[String: Any]]).first else { throw SomeError }

let secIdentity = dictionary[kSecImportItemIdentity] as! SecIdentity
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here we can please avoid the force-unwrapping even when Apple does in his examples?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, this cannot quite be avoided, because when trying to make it optional compiler produces
Conditional downcast to CoreFoundation type 'SecIdentity' will always succeed error

let chain = dictionary[kSecImportItemCertChain] as! [SecCertificate]
let certs = [secIdentity] + chain.dropFirst().map { $0 as Any }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason we are dropping the first SecCertificate ?

Copy link
Author

@viktorasl viktorasl Aug 23, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from SSLSetCertificate:

You must place in certRefs[0] a SecIdentityRef object that identifies the leaf certificate and its corresponding private key

so secIdentity already identifies leaf certificate so that leaf certificate should be dropped from chain otherwise chain becomes invalid due to duplicate certs.

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 {
context = SSLCreateContext(nil, .serverSide, .streamType)!
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