Skip to content

Commit

Permalink
home: remove URI automation scheme
Browse files Browse the repository at this point in the history
The URL automation scheme has been replaced by AppleScript support. We are
removing it due to potential abuse by attackers to launch a VM from the web
browser without user knowledge.

Resolves #6155
  • Loading branch information
osy committed Mar 30, 2024
1 parent 86dd6c7 commit 4d88c35
Show file tree
Hide file tree
Showing 4 changed files with 0 additions and 250 deletions.
61 changes: 0 additions & 61 deletions Platform/Shared/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -154,67 +154,6 @@ struct ContentView: View {
}
}
}

@MainActor private func handleUTMURL(with components: URLComponents) async throws {
func findVM() -> VMData? {
if let vmName = components.queryItems?.first(where: { $0.name == "name" })?.value {
return data.virtualMachines.first(where: { $0.detailsTitleLabel == vmName })
} else {
return nil
}
}

if let action = components.host {
switch action {
case "start":
if let vm = findVM(), vm.state == .stopped {
data.run(vm: vm)
}
break
case "stop":
if let vm = findVM(), vm.state == .started {
try await vm.wrapped!.stop(usingMethod: .force)
data.stop(vm: vm)
}
break
case "restart":
if let vm = findVM(), vm.state == .started {
try await vm.wrapped!.restart()
}
break
case "pause":
if let vm = findVM(), vm.state == .started {
let shouldSaveOnPause: Bool
if let vm = vm.wrapped as? (any UTMSpiceVirtualMachine) {
shouldSaveOnPause = !vm.isRunningAsDisposible
} else {
shouldSaveOnPause = true
}
try await vm.wrapped!.pause()
if shouldSaveOnPause {
try? await vm.wrapped!.saveSnapshot(name: nil)
}
}
case "resume":
if let vm = findVM(), vm.state == .paused {
try await vm.wrapped!.resume()
}
break
case "sendText":
if let vm = findVM(), vm.state == .started {
data.automationSendText(to: vm, urlComponents: components)
}
break
case "click":
if let vm = findVM(), vm.state == .started {
data.automationSendMouse(to: vm, urlComponents: components)
}
break
default:
return
}
}
}
}

extension ContentView: DropDelegate {
Expand Down
63 changes: 0 additions & 63 deletions Platform/UTMData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -901,69 +901,6 @@ struct AlertMessage: Identifiable {
}
}
}

// MARK: - Automation Features

/// Send text as keyboard input to VM
/// - Parameters:
/// - vm: VM to send text to
/// - components: Data (see UTM Wiki for details)
func automationSendText(to vm: VMData, urlComponents components: URLComponents) {
guard let queryItems = components.queryItems else { return }
guard let text = queryItems.first(where: { $0.name == "text" })?.value else { return }
#if os(macOS)
trySendTextSpice(vm: vm, text: text)
#else
trySendTextSpice(text)
#endif
}

/// Send mouse/tablet coordinates to VM
/// - Parameters:
/// - vm: VM to send mouse/tablet coordinates to
/// - components: Data (see UTM Wiki for details)
func automationSendMouse(to vm: VMData, urlComponents components: URLComponents) {
guard let qemuVm = vm.wrapped as? any UTMSpiceVirtualMachine else { return } // FIXME: implement for Apple VM
guard !qemuVm.config.displays.isEmpty else { return }
guard let queryItems = components.queryItems else { return }
/// Parse targeted position
var x: CGFloat? = nil
var y: CGFloat? = nil
let nf = NumberFormatter()
nf.allowsFloats = false
if let xStr = components.queryItems?.first(where: { item in
item.name == "x"
})?.value {
x = nf.number(from: xStr) as? CGFloat
}
if let yStr = components.queryItems?.first(where: { item in
item.name == "y"
})?.value {
y = nf.number(from: yStr) as? CGFloat
}
guard let xPos = x, let yPos = y else { return }
let point = CGPoint(x: xPos, y: yPos)
/// Parse which button should be clicked
var button: CSInputButton = .left
if let buttonStr = queryItems.first(where: { $0.name == "button"})?.value {
switch buttonStr {
case "middle":
button = .middle
break
case "right":
button = .right
break
default:
break
}
}
/// All parameters parsed, perform the click
#if os(macOS)
tryClickAtPoint(vm: vm, point: point, button: button)
#else
tryClickAtPoint(point: point, button: button)
#endif
}

// MARK: - AltKit

Expand Down
19 changes: 0 additions & 19 deletions Platform/iOS/UTMDataExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,23 +51,4 @@ extension UTMData {
func close(vm: VMData) {
// do nothing
}

func tryClickAtPoint(point: CGPoint, button: CSInputButton) {
if let vc = vmVC as? VMDisplayMetalViewController, let input = vc.vmInput {
input.sendMouseButton(button, pressed: true)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) {
input.sendMouseButton(button, pressed: false)
}
}
}

func trySendTextSpice(_ text: String) {
if let vc = vmVC as? VMDisplayMetalViewController {
#if !os(visionOS) // FIXME: broken in visionOS
vc.keyboardView.insertText(text)
#endif
} else if let vc = vmVC as? VMDisplayTerminalViewController {
vc.vmSerialPort.write(text.data(using: .nonLossyASCII)!)
}
}
}
107 changes: 0 additions & 107 deletions Platform/macOS/UTMDataExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -129,111 +129,4 @@ extension UTMData {
}
}
}

func trySendTextSpice(vm: VMData, text: String) {
guard text.count > 0 else { return }
if let vc = vmWindows[vm] as? VMDisplayQemuMetalWindowController {
KeyCodeMap.createKeyMapIfNeeded()

func sleep() {
Thread.sleep(forTimeInterval: 0.05)
}
func keyDown(keyCode: Int) {
if let scanCodes = KeyCodeMap.keyCodeToScanCodes[keyCode] {
vc.keyDown(scanCode: Int(scanCodes.down))
sleep()
}
}
func keyUp(keyCode: Int) {
/// Due to how Spice works we need to send keyUp for the .down scan code
/// instead of sending the key down for the scan code that indicates key up.
if let scanCodes = KeyCodeMap.keyCodeToScanCodes[keyCode] {
vc.keyUp(scanCode: Int(scanCodes.down))
sleep()
}
}
func press(keyCode: Int) {
keyDown(keyCode: keyCode)
keyUp(keyCode: keyCode)
}

func simulateKeyPress(_ keyCodeDict: [String: Int]) {
/// Press modifier keys if necessary
let optionUsed = keyCodeDict["option"] == 1
if optionUsed {
keyDown(keyCode: kVK_Option)
sleep()
}
let shiftUsed = keyCodeDict["shift"] == 1
if shiftUsed {
keyDown(keyCode: kVK_Shift)
sleep()
}
let fnUsed = keyCodeDict["function"] == 1
if fnUsed {
keyDown(keyCode: kVK_Function)
sleep()
}
let ctrlUsed = keyCodeDict["control"] == 1
if ctrlUsed {
keyDown(keyCode: kVK_Control)
sleep()
}
let cmdUsed = keyCodeDict["command"] == 1
if cmdUsed {
keyDown(keyCode: kVK_Command)
sleep()
}
/// Press the key now
let keyCode = keyCodeDict["virtKeyCode"]!
press(keyCode: keyCode)
/// Release modifiers
if optionUsed {
keyUp(keyCode: kVK_Option)
sleep()
}
if shiftUsed {
keyUp(keyCode: kVK_Shift)
sleep()
}
if fnUsed {
keyUp(keyCode: kVK_Function)
sleep()
}
if ctrlUsed {
keyUp(keyCode: kVK_Control)
sleep()
}
if cmdUsed {
keyUp(keyCode: kVK_Command)
sleep()
}
}
DispatchQueue.global(qos: .userInitiated).async {
text.enumerated().forEach { stringItem in
let char = stringItem.element
/// drop unknown chars
if let keyCodeDict = KeyCodeMap.characterToKeyCode(character: char) {
simulateKeyPress(keyCodeDict)
} else {
logger.warning("SendText dropping unknown char: \(char)")
}
}
}
} else if let terminal = vmWindows[vm] as? VMDisplayTerminal {
terminal.sendString(text)
}
}

func tryClickAtPoint(vm: VMData, point: CGPoint, button: CSInputButton) {
if let vc = vmWindows[vm] as? VMDisplayQemuMetalWindowController {
vc.mouseMove(absolutePoint: point, button: [])
DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) {
vc.mouseDown(button: button)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) {
vc.mouseUp(button: button)
}
}
}
}
}

0 comments on commit 4d88c35

Please sign in to comment.