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

Sanitize relay url typed #494

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
39 changes: 19 additions & 20 deletions Decimus/ManifestController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,32 @@
import Foundation
import os

/// Possible errors thrown by ``ManifestController``
enum ManifestControllerError: Error {
case invalidURL(_ url: URL)
case badState
}

/// Configuration object for ``ManifestController``.
struct ManifestServerConfig: Codable, Equatable {
/// URL scheme.
var scheme: String = "https"
/// Manifest/conference FQDN.
var url: String = "conf.quicr.ctgpoc.com"
/// Manifest/conference port.
var port: Int = 411
/// URL.
var url: URL = .init(string: "https://conf.quicr.ctgpoc.com:411")!
/// Which manifest configuration to query against.
var config: String = ""
var config: String = "testing"
}

/// Fetches and parses manifest/conference information from a server.
class ManifestController {
/// The shared ``ManifestController``.
static let shared = ManifestController()
private static let logger = DecimusLogger(ManifestController.self)

private var components: URLComponents = .init()
private let components: URLComponents
private var currentConfig: String = ""

/// Inject the server's configuration.
/// - Parameter config: The new configuration to use.
func setServer(config: ManifestServerConfig) {
self.components = URLComponents()
self.components.scheme = config.scheme
self.components.host = config.url
self.components.port = config.port
init(_ config: ManifestServerConfig) throws {
guard let components = URLComponents(url: config.url, resolvingAgainstBaseURL: false) else {
throw ManifestControllerError.invalidURL(config.url)
}
self.components = components
self.currentConfig = config.config
}

Expand Down Expand Up @@ -116,10 +114,11 @@ class ManifestController {
}

private func makeRequest(method: String, components: URLComponents) throws -> URLRequest {
guard let url = URL(string: components.string!) else {
throw "Invalid URL: \(components)"
guard let componentString = components.string,
let url = URL(string: componentString) else {
assert(false) // We should never be in this position.
throw ManifestControllerError.badState
}

var request = URLRequest(url: url)
request.httpMethod = method
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
Expand Down
6 changes: 1 addition & 5 deletions Decimus/Models/CallConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,7 @@ import Foundation
/// Configuration for joining a call.
struct CallConfig: Hashable {
/// Address of the server.
var address: String
/// Port to connect on.
var port: UInt16
/// Protocol to use in qmedia
var connectionProtocol: ProtocolType
var address: URL = defaultUrl
/// Email address of the user joining the call
var email: String = ""
/// Conference ID to join
Expand Down
13 changes: 3 additions & 10 deletions Decimus/Models/RelayConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,11 @@

import Foundation

/// Default ports for each supported protocol.
let defaultProtocolPorts: [ProtocolType: UInt16] = [
.UDP: 33434,
.QUIC: 33435
]
let defaultUrl = URL(string: "moq://relay.quicr.io:33435")!

/// Connection details for a MoQ relay.
struct RelayConfig: Codable {
static let defaultsKey = "relay_1"
/// FQDN.
var address: String = "relay.quicr.ctgpoc.com"
/// Protocol to use for the connection.
var connectionProtocol: ProtocolType = .QUIC
/// Port to connect on.
var port: UInt16 = defaultProtocolPorts[.QUIC]!
var address: URL = defaultUrl
}
26 changes: 11 additions & 15 deletions Decimus/Views/CallSetupView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ private struct LoginForm: View {
@AppStorage("email")
private var email: String = ""

@AppStorage("relayConfig")
@AppStorage(RelayConfig.defaultsKey)
private var relayConfig: AppStorageWrapper<RelayConfig> = .init(value: .init())

@AppStorage("manifestConfig")
Expand All @@ -32,16 +32,13 @@ private struct LoginForm: View {
@State private var meetings: [UInt32: String] = [:]
@State private var showContinuityDevicePicker: Bool = false

@State private var callConfig = CallConfig(address: "",
port: 0,
connectionProtocol: .QUIC,
email: "",
conferenceID: 0)
@State private var callConfig = CallConfig()
private var joinMeetingCallback: ConfigCallback
private let controller: ManifestController

init(_ onJoin: @escaping ConfigCallback) {
init(_ controller: ManifestController, _ onJoin: @escaping ConfigCallback) {
self.controller = controller
joinMeetingCallback = onJoin
ManifestController.shared.setServer(config: manifestConfig.value)
}

var body: some View {
Expand Down Expand Up @@ -162,13 +159,10 @@ private struct LoginForm: View {

private func fetchManifest() async throws {
callConfig = CallConfig(address: relayConfig.value.address,
port: relayConfig.value.port,
connectionProtocol: relayConfig.value.connectionProtocol,
email: email,
conferenceID: UInt32(confId))
isLoading = true
meetings = try await
ManifestController.shared.getConferences(for: callConfig.email)
self.meetings = try await self.controller.getConferences(for: self.callConfig.email)
.reduce(into: [:]) { $0[$1.id] = $1.title }
isLoading = false
}
Expand All @@ -181,8 +175,10 @@ private struct LoginForm: View {
struct CallSetupView: View {
private var joinMeetingCallback: ConfigCallback
@State private var settingsOpen: Bool = false
private let manifestController: ManifestController

init(_ onJoin: @escaping ConfigCallback) {
init(_ manifest: ManifestController, _ onJoin: @escaping ConfigCallback) {
self.manifestController = manifest
UIApplication.shared.isIdleTimerDisabled = false
joinMeetingCallback = onJoin
}
Expand Down Expand Up @@ -214,7 +210,7 @@ struct CallSetupView: View {
Text("Join a meeting")
.font(.title)
.foregroundColor(.white)
LoginForm(joinMeetingCallback)
LoginForm(self.manifestController, joinMeetingCallback)
#if targetEnvironment(macCatalyst)
.frame(maxWidth: 350)
#endif
Expand All @@ -233,6 +229,6 @@ struct CallSetupView: View {

struct CallSetupView_Previews: PreviewProvider {
static var previews: some View {
CallSetupView { _ in }
CallSetupView(try! ManifestController(.init())) { _ in }
}
}
57 changes: 57 additions & 0 deletions Decimus/Views/Components/URLField.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// SPDX-FileCopyrightText: Copyright (c) 2023 Cisco Systems
// SPDX-License-Identifier: BSD-2-Clause

import SwiftUI

struct URLField: View {

let name: String
let validation: ((URL) -> String?)?
@Binding var url: URL
@State private var error: String?

var body: some View {
VStack {
TextField(
self.name,
text: Binding(
get: {
self.url.absoluteString
},
set: {
guard let url = URL(string: $0) else {
self.error = "Invalid URL"
return
}
if let validation = self.validation,
let error = validation(url) {
self.error = error
return
}
self.error = nil
self.url = url
}))
.keyboardType(.URL)
.textContentType(.URL)
.autocorrectionDisabled(true)
.textInputAutocapitalization(.never)

if let error = self.error {
HStack {
Text(error).foregroundStyle(.red)
Spacer()
}
}
}
}
}

#Preview {
Form {
LabeledContent("URL") {
URLField(name: "Preview",
validation: nil,
url: .constant(.init(string: "http://example.org")!))
}
}
}
23 changes: 15 additions & 8 deletions Decimus/Views/ConfigCallView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,24 @@ import SwiftUI
struct ConfigCallView: View {
@State private var config: CallConfig?

@AppStorage("manifestConfig")
private var manifestConfig: AppStorageWrapper<ManifestServerConfig> = .init(value: .init())

var body: some View {
if config != nil {
InCallView(config: config!) { config = nil }
#if !os(tvOS)
if let controller = try? ManifestController(self.manifestConfig.value) {
if let config = self.config {
InCallView(config: config, manifest: controller) { self.config = nil }
#if !os(tvOS)
.navigationBarTitleDisplayMode(.inline)
#endif
} else {
CallSetupView { self.config = $0 }
#if !os(tvOS)
#endif
} else {
CallSetupView(controller) { self.config = $0 }
#if !os(tvOS)
.navigationBarTitleDisplayMode(.inline)
#endif
#endif
}
} else {
Text("Failed to parse Manifest URL - check settings").foregroundStyle(.red).font(.headline)
}
}
}
Expand Down
33 changes: 19 additions & 14 deletions Decimus/Views/InCallView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ struct InCallView: View {
.autoconnect()
#endif

init(config: CallConfig, onLeave: @escaping () -> Void) {
init(config: CallConfig, manifest: ManifestController, onLeave: @escaping () -> Void) {
UIApplication.shared.isIdleTimerDisabled = true
self.onLeave = onLeave
_viewModel = .init(wrappedValue: .init(config: config, onLeave: onLeave))
_viewModel = .init(wrappedValue: .init(config: config, manifest: manifest, onLeave: onLeave))
}

var body: some View {
Expand Down Expand Up @@ -184,15 +184,17 @@ extension InCallView {
private var audioCapture = false
private var videoCapture = false
private let onLeave: () -> Void
private let manifestController: ManifestController

@AppStorage("influxConfig")
private var influxConfig: AppStorageWrapper<InfluxConfig> = .init(value: .init())

@AppStorage("subscriptionConfig")
private var subscriptionConfig: AppStorageWrapper<SubscriptionConfig> = .init(value: .init())

init(config: CallConfig, onLeave: @escaping () -> Void) {
init(config: CallConfig, manifest: ManifestController, onLeave: @escaping () -> Void) {
self.config = config
self.manifestController = manifest
self.onLeave = onLeave
do {
self.engine = try .init()
Expand All @@ -201,10 +203,9 @@ extension InCallView {
self.engine = nil
}
let tags: [String: String] = [
"relay": "\(config.address):\(config.port)",
"relay": config.address.absoluteString,
"email": config.email,
"conference": "\(config.conferenceID)",
"protocol": "\(config.connectionProtocol)"
"conference": "\(config.conferenceID)"
]

if influxConfig.value.submit {
Expand Down Expand Up @@ -240,7 +241,13 @@ extension InCallView {

if let captureManager = self.captureManager,
let engine = self.engine {
let connectUri: String = "moq://\(config.address):\(config.port)"
let moqAddress = config.address.absoluteString
guard let scheme = config.address.scheme,
scheme == "moq" else {
Self.logger.error("URL scheme must be moq://")
return
}
let connectUri: String = "\(moqAddress)"
let endpointId: String = config.email
let qLogPath: URL
#if targetEnvironment(macCatalyst)
Expand Down Expand Up @@ -292,7 +299,7 @@ extension InCallView {

func join() async -> Bool {
guard let controller = self.controller else {
fatalError("No controller!?")
return false
}

// Connect to the relay/server.
Expand All @@ -314,9 +321,8 @@ extension InCallView {
// Fetch the manifest from the conference server.
let manifest: Manifest
do {
let mController = ManifestController.shared
manifest = try await mController.getManifest(confId: self.config.conferenceID,
email: self.config.email)
manifest = try await self.manifestController.getManifest(confId: self.config.conferenceID,
email: self.config.email)
} catch {
Self.logger.error("Failed to fetch manifest: \(error.localizedDescription)")
return false
Expand Down Expand Up @@ -390,8 +396,7 @@ extension InCallView.ViewModel {

struct InCallView_Previews: PreviewProvider {
static var previews: some View {
InCallView(config: .init(address: "127.0.0.1",
port: 5001,
connectionProtocol: .QUIC)) { }
InCallView(config: CallConfig(),
manifest: try! .init(.init())) { }
}
}
Loading