How Should Secure Transport Tls Be Used with Bsd Sockets in Swift

How should Secure Transport TLS be used with BSD sockets in Swift?

Starting with Swift 2 included with the Xcode 7 beta, Function Pointers in Swift work and have been greatly simplified. I turned my example above into this, which works:

import Foundation

func sslReadCallback(connection: SSLConnectionRef,
data: UnsafeMutablePointer<Void>,
var dataLength: UnsafeMutablePointer<Int>) -> OSStatus {

let socketfd = UnsafePointer<Int32>(connection).memory

let bytesRequested = dataLength.memory
let bytesRead = read(socketfd, data, UnsafePointer<Int>(dataLength).memory)

if (bytesRead > 0) {
dataLength = UnsafeMutablePointer<Int>.alloc(1)
dataLength.initialize(bytesRead)
if bytesRequested > bytesRead {
return Int32(errSSLWouldBlock)
} else {
return noErr
}
} else if (bytesRead == 0) {
dataLength = UnsafeMutablePointer<Int>.alloc(1)
dataLength.initialize(0)
return Int32(errSSLClosedGraceful)
} else {
dataLength = UnsafeMutablePointer<Int>.alloc(1)
dataLength.initialize(0)
switch (errno) {
case ENOENT: return Int32(errSSLClosedGraceful)
case EAGAIN: return Int32(errSSLWouldBlock)
case ECONNRESET: return Int32(errSSLClosedAbort)
default: return Int32(errSecIO)
}
}
}

func sslWriteCallback(connection: SSLConnectionRef,
data: UnsafePointer<Void>,
var dataLength: UnsafeMutablePointer<Int>) -> OSStatus {

let socketfd = UnsafePointer<Int32>(connection).memory

let bytesToWrite = dataLength.memory
let bytesWritten = write(socketfd, data, UnsafePointer<Int>(dataLength).memory)

if (bytesWritten > 0) {
dataLength = UnsafeMutablePointer<Int>.alloc(1)
dataLength.initialize(bytesWritten)
if (bytesToWrite > bytesWritten) {
return Int32(errSSLWouldBlock)
} else {
return noErr
}
} else if (bytesWritten == 0) {
dataLength = UnsafeMutablePointer<Int>.alloc(1)
dataLength.initialize(0)
return Int32(errSSLClosedGraceful)
} else {
dataLength = UnsafeMutablePointer<Int>.alloc(1)
dataLength.initialize(0)
if (EAGAIN == errno) {
return Int32(errSSLWouldBlock)
} else {
return Int32(errSecIO)
}
}
}
var socketfd = Darwin.socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)

var addr = Darwin.sockaddr_in(sin_len: __uint8_t(sizeof(sockaddr_in)), sin_family: sa_family_t(AF_INET), sin_port: CFSwapInt16(8080), sin_addr: in_addr(s_addr: inet_addr("192.168.0.113")), sin_zero: (0, 0, 0, 0, 0, 0, 0, 0))
var sock_addr = Darwin.sockaddr(sa_len: 0, sa_family: 0, sa_data: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0))
Darwin.memcpy(&sock_addr, &addr, Int(sizeof(sockaddr_in)))

var err = Darwin.connect(socketfd, &sock_addr, socklen_t(sizeof(sockaddr_in)))

if let umc = SSLCreateContext(kCFAllocatorDefault, kSSLClientSide, kSSLStreamType) {
var sslContext = umc.takeRetainedValue()
SSLSetIOFuncs(sslContext, sslReadCallback, sslWriteCallback)
SSLSetConnection(sslContext, &socketfd)
SSLSetSessionOption(sslContext, kSSLSessionOptionBreakOnClientAuth, Boolean(1))
SSLHandshake(sslContext)
}

Cannot implement Swift SSLCreateContext

Please use the identifiers declared in Swift:

if let sslContext = SSLCreateContext(kCFAllocatorDefault, SSLProtocolSide.ClientSide, SSLConnectionType.StreamType) {
SSLSetIOFuncs(sslContext, sslReadCallback, sslWriteCallback)
SSLSetConnection(sslContext, &socketfd)
SSLSetSessionOption(sslContext, SSLSessionOption.BreakOnClientAuth, true)
SSLHandshake(sslContext)
}

How to use URLSessionStreamTask with URLSession for chunked-encoding transfer

Received lots of info courtesy of Quinn "The Eskimo" at Apple.

Alas, you have the wrong end of the stick here. URLSessionStreamTask is for wrangling a naked TCP (or TLS over TCP) connection, without the HTTP framing on top. You can think of it as a high-level equivalent to the BSD Sockets API.

The chunked transfer encoding is part of HTTP, and is thus supported by all of the other URLSession task types (data task, upload task, download task). You don’t need to do anything special to enable this. The chunked transfer encoding is a mandatory part of the HTTP 1.1 standard, and is thus is always enabled.

You do, however, have an option as to how you receive the returned data. If you use the URLSession convenience APIs (dataTask(with:completionHandler:) and so on), URLSession will buffer all the incoming data and then pass it to your completion handler in one large Data value. That’s convenient in many situations but it doesn’t work well with a streamed resource. In that case you need to use the URLSession delegate-based APIs (dataTask(with:) and so on), which will call the urlSession(_:dataTask:didReceive:) session delegate method with chunks of data as they arrive.

As for the specific endpoint I was testing, the following was uncovered:
It seems that the server only enables its streaming response (the chunked transfer encoding) if the client sends it a streaming request. That’s kinda weird, and definitely not required by the HTTP spec.

Fortunately, it is possible to force URLSession to send a streaming request:

  1. Create your task with uploadTask(withStreamedRequest:)

  2. Implement the urlSession(_:task:needNewBodyStream:) delegate method to return an input stream that, when read, returns the request body

  3. Profit!

I’ve attached some test code that shows this in action. In this case it uses a bound pair of streams, passing the input stream to the request (per step 2 above) and holding on to the output stream.

If you want to actually send data as part of the request body you can do so by writing to the output stream.

class NetworkManager : NSObject, URLSessionDataDelegate {

static var shared = NetworkManager()

private var session: URLSession! = nil

override init() {
super.init()
let config = URLSessionConfiguration.default
config.requestCachePolicy = .reloadIgnoringLocalCacheData
self.session = URLSession(configuration: config, delegate: self, delegateQueue: .main)
}

private var streamingTask: URLSessionDataTask? = nil

var isStreaming: Bool { return self.streamingTask != nil }

func startStreaming() {
precondition( !self.isStreaming )

let url = URL(string: "ENTER STREAMING URL HERE")!
let request = URLRequest(url: url)
let task = self.session.uploadTask(withStreamedRequest: request)
self.streamingTask = task
task.resume()
}

func stopStreaming() {
guard let task = self.streamingTask else {
return
}
self.streamingTask = nil
task.cancel()
self.closeStream()
}

var outputStream: OutputStream? = nil

private func closeStream() {
if let stream = self.outputStream {
stream.close()
self.outputStream = nil
}
}

func urlSession(_ session: URLSession, task: URLSessionTask, needNewBodyStream completionHandler: @escaping (InputStream?) -> Void) {
self.closeStream()

var inStream: InputStream? = nil
var outStream: OutputStream? = nil
Stream.getBoundStreams(withBufferSize: 4096, inputStream: &inStream, outputStream: &outStream)
self.outputStream = outStream

completionHandler(inStream)
}

func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
NSLog("task data: %@", data as NSData)
}

func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let error = error as NSError? {
NSLog("task error: %@ / %d", error.domain, error.code)
} else {
NSLog("task complete")
}
}
}

And you can call the networking code from anywhere such as:

class MainViewController : UITableViewController {

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if NetworkManager.shared.isStreaming {
NetworkManager.shared.stopStreaming()
} else {
NetworkManager.shared.startStreaming()
}
self.tableView.deselectRow(at: indexPath, animated: true)
}
}

Hope this helps.



Related Topics



Leave a reply



Submit