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

Add support for L2CAP and refactor state management #32

Merged
merged 20 commits into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 178 additions & 0 deletions Sources/MobileSdk/BLEConnection.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
// Derived from MIT-licensed work by Paul Wilkinson: https://github.com/paulw11/L2Cap

import CoreBluetooth
import Foundation

/// The base BLE connection, only intended for subclassing.
class BLEInternalConnection: NSObject, StreamDelegate {
sbihel marked this conversation as resolved.
Show resolved Hide resolved
var channel: CBL2CAPChannel?

private var outputData = Data()
private var incomingData = Data()
private var incomingTime = Date(timeIntervalSinceNow: 0)
private var incomingDelivered = false
private var openCount = 0
private var totalBytesWritten = 0

/// Handle stream events. Many of these we hand to local methods which the child classes are expected to
/// override.
func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
switch eventCode {
case Stream.Event.openCompleted:
// TODO: This is a bit of a hack, but it'll do for now. There are two streams, one input, one
// output, and we get notified about both. We really only want to start doing things when
// both are available.
openCount += 1

if openCount == 2 {
streamIsOpen()
}

case Stream.Event.endEncountered:
openCount -= 1
streamEnded()

case Stream.Event.hasBytesAvailable:
streamBytesAvailable()
readBytes(from: aStream as! InputStream)

case Stream.Event.hasSpaceAvailable:
streamSpaceAvailable()
send()

case Stream.Event.errorOccurred:
streamError()

default:
streamUnknownEvent()
}
}

/// Public send() interface.
public func send(data: Data) {
outputData.append(data)
totalBytesWritten = 0
send()
}

/// Internal send() interface.
private func send() {
guard let ostream = channel?.outputStream, !outputData.isEmpty, ostream.hasSpaceAvailable else {
return
}
let bytesWritten = ostream.write(outputData)

totalBytesWritten += bytesWritten

// The isEmpty guard above should prevent div0 errors here.
let fracDone = Double(totalBytesWritten) / Double(outputData.count)

streamSentData(bytes: bytesWritten, total: totalBytesWritten, fraction: fracDone)

if bytesWritten < outputData.count {
outputData = outputData.advanced(by: bytesWritten)
} else {
outputData.removeAll()
}
}

/// Close the stream.
public func close() {
if let ch = channel {
ch.outputStream.close()
ch.inputStream.close()
ch.inputStream.remove(from: .main, forMode: .default)
ch.outputStream.remove(from: .main, forMode: .default)
ch.inputStream.delegate = nil
ch.outputStream.delegate = nil
openCount = 0
}

channel = nil
}

/// Read from the stream.
private func readBytes(from stream: InputStream) {
let bufLength = 1024
let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: bufLength)
defer {
buffer.deallocate()
}
let bytesRead = stream.read(buffer, maxLength: bufLength)
incomingData.append(buffer, count: bytesRead)

// This is an awful hack to work around a hairy problem. L2CAP is a stream protocol; there's
sbihel marked this conversation as resolved.
Show resolved Hide resolved
// no framing on data, so there's no way to signal that the data exchange is complete. In principle
// we could build a framing protocol on top, or we could use the State characteristics to signal out
// of band, but neither of those are specified by the spec, so we'd be out of compliance. The State
// signalling is what the non-L2CAP flow uses, but the spec explicitly says it's not used with L2CAP.
//
// Another thing we could do would be close the connection, but there are two problems with that;
// the first is we'd be out of spec compliance again, and the second is that we actually have two
// messages going, one in each direction, serially. If we closed to indicate the length of the first,
// we'd have no connection for the second.
//
// So, we have data coming in, and we don't know how much. The stream lets us know when more data
// has arrived, the data comes in chunks. What we do, then, is timestamp when we receive some data,
// and then half a second later see if we got any more. Hopefully the half second delay is small
// enough not to annoy the user and large enough to account for noisy radio environments, but the
// numbers here are a heuristic, and may need to be tuned. If we have no recent data, we assume
// everything is ok, and declare the transmission complete.

incomingTime = Date(timeIntervalSinceNow: 0)

DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { // Half second delay.
if self.incomingDelivered {
return
}

let timeSinceLastData = -self.incomingTime.timeIntervalSinceNow // Make it positive.
let complete = timeSinceLastData > 0.25

if complete {
self.streamReceivedData(self.incomingData)
self.incomingDelivered = true
}
}

if stream.hasBytesAvailable {
readBytes(from: stream)
}
}

/// Methods to be overridden by child classes.
func streamIsOpen() { print("The stream is open.") }
func streamEnded() { print("The stream has ended.") }
func streamBytesAvailable() { print("The stream has bytes available.") }
func streamSpaceAvailable() { print("The stream has space available.") }
func streamError() { print("Stream error.") }
func streamUnknownEvent() { print("Stream unknown event.") }
func streamSentData(bytes _: Int, total _: Int, fraction _: Double) { print("Stream sent data.") }
func streamReceivedData(_: Data) { print("Stream received data.") }
}

/// A UInt16 from Data extension.
extension UInt16 {
var data: Data {
var int = self
return Data(bytes: &int, count: MemoryLayout<UInt16>.size)
}
}

/// A Data from UInt16 extension.
extension Data {
var uint16: UInt16 {
let i16array = withUnsafeBytes { $0.load(as: UInt16.self) }
return i16array
}
}

/// A write() on OutputStream extension.
extension OutputStream {
func write(_ data: Data) -> Int {
return data.withUnsafeBytes { (rawBufferPointer: UnsafeRawBufferPointer) -> Int in
let bufferPointer = rawBufferPointer.bindMemory(to: UInt8.self)
return self.write(bufferPointer.baseAddress!, maxLength: data.count)
}
}
}
2 changes: 1 addition & 1 deletion Sources/MobileSdk/Credential.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ open class Credential: Identifiable {

open func get(keys: [String]) -> [String: GenericJSON] {
if keys.contains("id") {
return ["id": GenericJSON.string(self.id)]
return ["id": GenericJSON.string(id)]
} else {
return [:]
}
Expand Down
23 changes: 11 additions & 12 deletions Sources/MobileSdk/CredentialPack.swift
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import Foundation
import CryptoKit
import Foundation

public class CredentialPack {

private var credentials: [Credential]

public init() {
self.credentials = []
credentials = []
}

public init(credentials: [Credential]) {
Expand All @@ -16,8 +15,8 @@ public class CredentialPack {
public func addW3CVC(credentialString: String) throws -> [Credential]? {
do {
let credential = try W3CVC(credentialString: credentialString)
self.credentials.append(credential)
return self.credentials
credentials.append(credential)
return credentials
} catch {
throw error
}
Expand All @@ -26,28 +25,28 @@ public class CredentialPack {
public func addMDoc(mdocBase64: String, keyAlias: String = UUID().uuidString) throws -> [Credential]? {
let mdocData = Data(base64Encoded: mdocBase64)!
let credential = MDoc(fromMDoc: mdocData, namespaces: [:], keyAlias: keyAlias)!
self.credentials.append(credential)
return self.credentials
credentials.append(credential)
return credentials
}

public func get(keys: [String]) -> [String: [String: GenericJSON]] {
var values: [String: [String: GenericJSON]] = [:]
for cred in self.credentials {
for cred in credentials {
values[cred.id] = cred.get(keys: keys)
}

return values
}

public func get(credentialsIds: [String]) -> [Credential] {
return self.credentials.filter { credentialsIds.contains($0.id) }
return credentials.filter { credentialsIds.contains($0.id) }
}

public func get(credentialId: String) -> Credential? {
if let credential = self.credentials.first(where: { $0.id == credentialId }) {
return credential
if let credential = credentials.first(where: { $0.id == credentialId }) {
return credential
} else {
return nil
return nil
}
}
}
4 changes: 2 additions & 2 deletions Sources/MobileSdk/Credentials.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ public class CredentialStore {
}

// swiftlint:disable force_cast
public func presentMdocBLE(deviceEngagement: DeviceEngagement,
public func presentMdocBLE(deviceEngagement _: DeviceEngagement,
callback: BLESessionStateDelegate
// , trustedReaders: TrustedReaders
) -> BLESessionManager? {
if let firstMdoc = self.credentials.first(where: {$0 is MDoc}) {
if let firstMdoc = credentials.first(where: { $0 is MDoc }) {
return BLESessionManager(mdoc: firstMdoc as! MDoc, engagement: DeviceEngagement.QRCode, callback: callback)
} else {
return nil
Expand Down
16 changes: 8 additions & 8 deletions Sources/MobileSdk/DataConversions.swift
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import Foundation

extension Data {
var base64EncodedUrlSafe: String {
let string = self.base64EncodedString()
var base64EncodedUrlSafe: String {
let string = base64EncodedString()

// Make this URL safe and remove padding
return string
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "=", with: "")
}
// Make this URL safe and remove padding
return string
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "=", with: "")
}
}
25 changes: 13 additions & 12 deletions Sources/MobileSdk/GenericJSON.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ extension GenericJSON: Codable {

public func toString() -> String {
switch self {
case .string(let str):
case let .string(str):
return str
case .number(let num):
case let .number(num):
return num.debugDescription
case .bool(let bool):
case let .bool(bool):
return bool.description
case .null:
return "null"
Expand Down Expand Up @@ -71,11 +71,11 @@ extension GenericJSON: Codable {
extension GenericJSON: CustomDebugStringConvertible {
public var debugDescription: String {
switch self {
case .string(let str):
case let .string(str):
return str.debugDescription
case .number(let num):
case let .number(num):
return num.debugDescription
case .bool(let bool):
case let .bool(bool):
return bool.description
case .null:
return "null"
Expand All @@ -89,26 +89,28 @@ extension GenericJSON: CustomDebugStringConvertible {

public extension GenericJSON {
var dictValue: [String: GenericJSON]? {
if case .object(let value) = self {
if case let .object(value) = self {
return value
}
return nil
}

var arrayValue: [GenericJSON]? {
if case .array(let value) = self {
if case let .array(value) = self {
return value
}
return nil
}

subscript(index: Int) -> GenericJSON? {
if case .array(let arr) = self, arr.indices.contains(index) {
if case let .array(arr) = self, arr.indices.contains(index) {
return arr[index]
}
return nil
}

subscript(key: String) -> GenericJSON? {
if case .object(let dict) = self {
if case let .object(dict) = self {
return dict[key]
}
return nil
Expand All @@ -123,7 +125,7 @@ public extension GenericJSON {
}

func queryKeyPath<T>(_ path: T) -> GenericJSON? where T: Collection, T.Element == String {
guard case .object(let object) = self else {
guard case let .object(object) = self else {
return nil
}
guard let head = path.first else {
Expand All @@ -135,5 +137,4 @@ public extension GenericJSON {
let tail = path.dropFirst()
return tail.isEmpty ? value : value.queryKeyPath(tail)
}

}
Loading
Loading