Skip to content

Commit

Permalink
Migrate from NetServiceBrowser to NWBrowser; prepare CastSocket!
Browse files Browse the repository at this point in the history
  • Loading branch information
dhleong committed Dec 5, 2020
1 parent f092d5b commit de8ae8e
Show file tree
Hide file tree
Showing 9 changed files with 1,098 additions and 76 deletions.
9 changes: 9 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
{
"object": {
"pins": [
{
"package": "SwiftProtobuf",
"repositoryURL": "https://github.com/apple/swift-protobuf.git",
"state": {
"branch": null,
"revision": "da9a52be9cd36c63993291ce3f1b65dafcd1e826",
"version": "1.14.0"
}
},
{
"package": "SwiftCoroutine",
"repositoryURL": "https://github.com/belozierov/SwiftCoroutine",
Expand Down
6 changes: 6 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,19 @@ let package = Package(

dependencies: [
.package(url: "https://github.com/belozierov/SwiftCoroutine", from: "2.1.9"),
.package(
name: "SwiftProtobuf",
url: "https://github.com/apple/swift-protobuf.git",
from: "1.14.0"
),
],

targets: [
.target(
name: "castable Extension",
dependencies: [
"SwiftCoroutine",
"SwiftProtobuf",
],
path: "castable Extension/",
exclude: [
Expand Down
99 changes: 99 additions & 0 deletions cast_channel.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

syntax = "proto2";

option optimize_for = LITE_RUNTIME;

package cast_channel;

message CastMessage {
// Always pass a version of the protocol for future compatibility
// requirements.
enum ProtocolVersion { CASTV2_1_0 = 0; }
required ProtocolVersion protocol_version = 1;

// source and destination ids identify the origin and destination of the
// message. They are used to route messages between endpoints that share a
// device-to-device channel.
//
// For messages between applications:
// - The sender application id is a unique identifier generated on behalf of
// the sender application.
// - The receiver id is always the the session id for the application.
//
// For messages to or from the sender or receiver platform, the special ids
// 'sender-0' and 'receiver-0' can be used.
//
// For messages intended for all endpoints using a given channel, the
// wildcard destination_id '*' can be used.
required string source_id = 2;
required string destination_id = 3;

// This is the core multiplexing key. All messages are sent on a namespace
// and endpoints sharing a channel listen on one or more namespaces. The
// namespace defines the protocol and semantics of the message.
required string namespace = 4;

// Encoding and payload info follows.

// What type of data do we have in this message.
enum PayloadType {
STRING = 0;
BINARY = 1;
}
required PayloadType payload_type = 5;

// Depending on payload_type, exactly one of the following optional fields
// will always be set.
optional string payload_utf8 = 6;
optional bytes payload_binary = 7;
}

enum SignatureAlgorithm {
UNSPECIFIED = 0;
RSASSA_PKCS1v15 = 1;
RSASSA_PSS = 2;
}

enum HashAlgorithm {
SHA1 = 0;
SHA256 = 1;
}

// Messages for authentication protocol between a sender and a receiver.
message AuthChallenge {
optional SignatureAlgorithm signature_algorithm = 1
[default = RSASSA_PKCS1v15];
optional bytes sender_nonce = 2;
optional HashAlgorithm hash_algorithm = 3 [default = SHA1];
}

message AuthResponse {
required bytes signature = 1;
required bytes client_auth_certificate = 2;
repeated bytes intermediate_certificate = 3;
optional SignatureAlgorithm signature_algorithm = 4
[default = RSASSA_PKCS1v15];
optional bytes sender_nonce = 5;
optional HashAlgorithm hash_algorithm = 6 [default = SHA1];
optional bytes crl = 7;
}

message AuthError {
enum ErrorType {
INTERNAL_ERROR = 0;
NO_TLS = 1; // The underlying connection is not TLS
SIGNATURE_ALGORITHM_UNAVAILABLE = 2;
}
required ErrorType error_type = 1;
}

message DeviceAuthMessage {
// Request fields
optional AuthChallenge challenge = 1;
// Response fields
optional AuthResponse response = 2;
optional AuthError error = 3;
}
134 changes: 59 additions & 75 deletions castable Extension/shared/Cast/CastDiscovery.swift
Original file line number Diff line number Diff line change
@@ -1,93 +1,43 @@
import Foundation
import Network
import SafariServices

@available(OSX 10.15, *)
class CastDiscovery {
private class BrowserDelegate : NSObject, NetServiceBrowserDelegate, NetServiceDelegate {

private var resolving: [NetService] = []

func netServiceBrowser(
_ browser: NetServiceBrowser,
didFind service: NetService,
moreComing: Bool
) {
// start trying to resolve, because somehow we don't
// have the remote address yet (?!)
resolving.append(service)
NSLog("castable: BONJOUR didFind (more=\(moreComing)) \(service) -> \(resolving.count)")

service.delegate = self
service.resolve(withTimeout: 5)
}

func netServiceDidResolveAddress(_ sender: NetService) {
let service = sender

guard let addresses = service.addresses, !addresses.isEmpty else {
NSLog("castable: no addresses")
return
}

guard let data = service.txtRecordData() else {
NSLog("castable: no text")
return
}
let txt = NetService.dictionary(fromTXTRecord: data)

guard let nameData = txt["fn"] else {
NSLog("castable: no name")
return
}
private var browser: NWBrowser? = nil

guard let modelData = txt["md"] else {
NSLog("castable: no model")
return
}
func discover() {
if browser != nil {
NSLog("castable: WARNING: duplicate discover() without stop()")
return
}

service.remove(from: RunLoop.main, forMode: .common)
service.stop()
let b = NWBrowser(for: .bonjourWithTXTRecord(type: "_googlecast._tcp.", domain: nil), using: NWParameters.tcp)
browser = b

if let index = resolving.firstIndex(of: service) {
resolving.remove(at: index)
b.browseResultsChangedHandler = { results, _ in
let descriptors = results.compactMap { result in
CastServiceDescriptor(from: result)
}
NSLog("castable: results = \(descriptors)")

// TODO provide this... somewhere
let desc = CastServiceDescriptor(
id: service.name,
name: String(decoding: nameData, as: UTF8.self),
address: addresses[0],
model: String(decoding: modelData, as: UTF8.self)
)
NSLog("castable: RESOLVED \(desc)")
// TODO emit on channel or something

// FIXME this is gross; some sort of observer should
// be responsible for this, perhaps over a coroutine channel?
if #available(OSX 10.15, *) {
let state = AppState.instance
state.devices.removeAll {
$0.id == desc.id
}
state.devices.append(CastDevice(withDescriptor: desc))
let state = AppState.instance
state.devices = descriptors.map { CastDevice(withDescriptor: $0) }

// FIXME STOPSHIP testing only:
if let desc = descriptors.first {
NSLog("castable: open socket")
let socket = CastSocket(withAddress: desc.address)
socket.open()
}
}

}

private let browserDelegate = BrowserDelegate()

private var browser: NetServiceBrowser? = nil

func discover() {
if browser != nil {
NSLog("castable: WARNING: duplicate discover() without stop()")
return
}

let b = NetServiceBrowser()
browser = b

b.delegate = browserDelegate
b.searchForServices(ofType: "_googlecast._tcp.", inDomain: "")
b.start(queue: DispatchQueue.main)

NSLog("castable: Scheduled service browser")
}
Expand All @@ -97,9 +47,43 @@ class CastDiscovery {

if let browser = browser {
NSLog("castable: close browser")
browser.stop()
browser.cancel()
}

browser = nil
}
}

@available(OSX 10.15, *)
extension CastServiceDescriptor {
init?(from: NWBrowser.Result) {
if case let .bonjour(text) = from.metadata {
if let id = text["id"] {
self.id = id
} else {
NSLog("castable: No ID in \(text)")
return nil
}

if let name = text["fn"] {
self.name = name
} else {
NSLog("castable: No name in \(text)")
return nil
}

if let model = text["md"] {
self.model = model
} else {
NSLog("castable: No model in \(text)")
return nil
}

} else {
NSLog("castable: Unexpected result type: \(from.metadata) of \(from)")
return nil
}

self.address = from.endpoint
}
}
3 changes: 2 additions & 1 deletion castable Extension/shared/Cast/CastServiceDescriptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@
//

import Foundation
import Network

struct CastServiceDescriptor : Identifiable {
typealias ID = String

let id: String
let name: String
let address: Data
let address: NWEndpoint
let model: String
}
77 changes: 77 additions & 0 deletions castable Extension/shared/Cast/CastSocket.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
//
// CastSocket.swift
// castable
//
// Created by Daniel Leong on 12/5/20.
//

import Foundation
import Network

/// Low-level communication with a Chromecast device
class CastSocket {

private var address: NWEndpoint

private var connection: NWConnection? = nil

init(withAddress address: NWEndpoint) {
self.address = address
}

func open() {
NSLog("castable: open()")
if let old = connection {
NSLog("castable: WARN: duplicate open()")
old.cancel()
}

let address = self.address
let conn = NWConnection(to: address, using: insecureTLSParameters())
connection = conn

conn.start(queue: DispatchQueue.main)
conn.stateUpdateHandler = { state in
NSLog("castable: connection(\(address)) state <- \(state)")
switch state {
case .ready:
NSLog("castable: connection(\(address)) READY")
self.close()

case .failed(let error):
NSLog("castable: connection(\(address)) FAILED: \(error)")
self.close()
break

default:
break // nop
}
}

NSLog("castable: connected")
}

func close() {
connection?.cancel()
connection = nil
}

private func insecureTLSParameters() -> NWParameters {
let opts = NWProtocolTLS.Options()
let address = self.address

// borrowed from: https://stackoverflow.com/a/54467228
sec_protocol_options_set_verify_block(opts.securityProtocolOptions, { (sec_protocol_metadata, sec_trust, sec_protocol_verify_complete) in
let trust = sec_trust_copy_ref(sec_trust).takeRetainedValue()
var error: CFError?
if SecTrustEvaluateWithError(trust, &error) {
sec_protocol_verify_complete(true)
} else {
NSLog("castable: allowing insecure: \(trust) for \(address)")
sec_protocol_verify_complete(true)
}
}, DispatchQueue.main)

return NWParameters(tls: opts)
}
}
Loading

0 comments on commit de8ae8e

Please sign in to comment.