computer-helper.swift 7.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. import Foundation
  2. import Security
  3. import AppKit
  4. import ApplicationServices
  5. import Darwin
  6. // This helper's signed identity owns each Keychain item. Plaintext travels only
  7. // on private stdin/stdout, never in command arguments or logs.
  8. func fail(_ message: String) -> Never {
  9. FileHandle.standardError.write(Data((message + "\n").utf8)); exit(1)
  10. }
  11. #if !DEVELOPMENT_HELPER
  12. // The distributed helper accepts only its containing, validly signed App with
  13. // the same signing team. Copying the helper or invoking it from a shell fails.
  14. @_silgen_name("proc_pidpath")
  15. func processPath(_ pid: Int32, _ buffer: UnsafeMutableRawPointer, _ size: UInt32) -> Int32
  16. var parentPath = [CChar](repeating: 0, count: 4096)
  17. let pathLength = parentPath.withUnsafeMutableBytes { processPath(getppid(), $0.baseAddress!, UInt32($0.count)) }
  18. guard pathLength > 0 else { fail("无法核对 App 身份") }
  19. let parentURL = URL(fileURLWithPath: String(cString: parentPath)).resolvingSymlinksInPath()
  20. let expected = parentURL.deletingLastPathComponent().appendingPathComponent("../Resources/runtime/computer-helper").standardizedFileURL.resolvingSymlinksInPath()
  21. guard expected.path == URL(fileURLWithPath: CommandLine.arguments[0]).resolvingSymlinksInPath().path else { fail("电脑助手只允许所属 App 调用") }
  22. var parentCode: SecCode?, ownCode: SecCode?
  23. guard SecCodeCopyGuestWithAttributes(nil, [kSecGuestAttributePid as String: getppid()] as CFDictionary, [], &parentCode) == errSecSuccess,
  24. SecCodeCopySelf([], &ownCode) == errSecSuccess, let parentCode, let ownCode,
  25. SecCodeCheckValidity(parentCode, [], nil) == errSecSuccess else { fail("App 签名无效") }
  26. func signingInfo(_ code: SecCode) -> [String: Any] {
  27. var info: CFDictionary?
  28. var staticCode: SecStaticCode?
  29. guard SecCodeCopyStaticCode(code, [], &staticCode) == errSecSuccess, let staticCode else { fail("无法读取签名身份") }
  30. guard SecCodeCopySigningInformation(staticCode, SecCSFlags(rawValue: kSecCSSigningInformation), &info) == errSecSuccess else { fail("无法核对签名身份") }
  31. return info as? [String: Any] ?? [:]
  32. }
  33. let parentInfo = signingInfo(parentCode), ownInfo = signingInfo(ownCode)
  34. guard let team = ownInfo[kSecCodeInfoTeamIdentifier as String] as? String,
  35. parentInfo[kSecCodeInfoTeamIdentifier as String] as? String == team,
  36. // The client keeps the historical identifier for upgrade continuity;
  37. // the full dual-mode build moved to its own identifier.
  38. ["cn.samien.yaoyao.desktop", "cn.samien.yaoyao.desktop.full"]
  39. .contains(parentInfo[kSecCodeInfoIdentifier as String] as? String ?? "") else { fail("App 签名身份不匹配") }
  40. #endif
  41. let bytes = FileHandle.standardInput.readDataToEndOfFile()
  42. guard bytes.count <= 131072,
  43. let input = try? JSONSerialization.jsonObject(with: bytes) as? [String: Any],
  44. let action = input["action"] as? [String: Any], let kind = action["kind"] as? String,
  45. let frame = input["frame"] as? [String: Any], let bounds = frame["bounds"] as? [String: Any],
  46. let width = frame["width"] as? Double, let height = frame["height"] as? Double,
  47. let bx = bounds["x"] as? Double, let by = bounds["y"] as? Double,
  48. let bw = bounds["width"] as? Double, let bh = bounds["height"] as? Double,
  49. width > 0, height > 0, bw > 0, bh > 0 else { fail("电脑输入格式无效") }
  50. guard AXIsProcessTrusted() else { fail("请在系统设置中允许夭夭使用辅助功能") }
  51. func point(_ xKey: String, _ yKey: String) -> CGPoint {
  52. guard let x = action[xKey] as? Double, let y = action[yKey] as? Double,
  53. x.isFinite, y.isFinite, x >= 0, y >= 0, x < width, y < height else { fail("输入超出当前画面") }
  54. return CGPoint(x: bx + x * bw / width, y: by + y * bh / height)
  55. }
  56. func mouse(_ type: CGEventType, _ point: CGPoint, _ button: CGMouseButton = .left, _ clicks: Int64 = 1) {
  57. guard let event = CGEvent(mouseEventSource: nil, mouseType: type, mouseCursorPosition: point, mouseButton: button) else { fail("无法创建鼠标输入") }
  58. event.setIntegerValueField(.mouseEventClickState, value: clicks); event.post(tap: .cghidEventTap)
  59. }
  60. switch kind {
  61. case "click":
  62. let p = point("x", "y"), name = action["button"] as? String ?? "left"
  63. let button: CGMouseButton = name == "right" ? .right : name == "middle" ? .center : .left
  64. let down: CGEventType = button == .left ? .leftMouseDown : button == .right ? .rightMouseDown : .otherMouseDown
  65. let up: CGEventType = button == .left ? .leftMouseUp : button == .right ? .rightMouseUp : .otherMouseUp
  66. let count = action["count"] as? Int ?? 1
  67. guard (1...2).contains(count) else { fail("点击次数无效") }
  68. mouse(.mouseMoved, p)
  69. for i in 1...count { mouse(down,p,button,Int64(i)); mouse(up,p,button,Int64(i)); usleep(50000) }
  70. case "drag":
  71. let start = point("fromX", "fromY"), end = point("toX", "toY")
  72. mouse(.mouseMoved,start); mouse(.leftMouseDown,start)
  73. for step in 1...12 { mouse(.leftMouseDragged,CGPoint(x:start.x+(end.x-start.x)*Double(step)/12,y:start.y+(end.y-start.y)*Double(step)/12)); usleep(12000) }
  74. mouse(.leftMouseUp,end)
  75. case "text":
  76. guard let text = action["text"] as? String, text.utf16.count <= 16000 else { fail("输入文本过长") }
  77. let characters = Array(text.utf16)
  78. for offset in stride(from: 0, to: characters.count, by: 200) {
  79. let chunk = Array(characters[offset..<min(offset+200,characters.count)])
  80. for down in [true,false] { guard let event = CGEvent(keyboardEventSource:nil,virtualKey:0,keyDown:down) else { fail("无法创建键盘输入") };chunk.withUnsafeBufferPointer { event.keyboardSetUnicodeString(stringLength:chunk.count,unicodeString:$0.baseAddress!) };event.post(tap:.cghidEventTap) }
  81. }
  82. case "key":
  83. let keys: [String:CGKeyCode] = ["a":0,"s":1,"d":2,"f":3,"h":4,"g":5,"z":6,"x":7,"c":8,"v":9,"b":11,"q":12,"w":13,"e":14,"r":15,"y":16,"t":17,"1":18,"2":19,"3":20,"4":21,"6":22,"5":23,"9":25,"7":26,"8":28,"0":29,"o":31,"u":32,"i":34,"p":35,"Return":36,"Enter":36,"l":37,"j":38,"k":40,"n":45,"m":46,"Tab":48,"Space":49,"space":49,"BackSpace":51,"Backspace":51,"Escape":53,"Delete":117,"Home":115,"End":119,"PageUp":116,"PageDown":121,"Left":123,"Right":124,"Down":125,"Up":126]
  84. guard let name = action["key"] as? String, let key = keys[name] ?? keys[name.lowercased()] else { fail("暂不支持这个按键") }
  85. var flags = CGEventFlags()
  86. for modifier in action["modifiers"] as? [String] ?? [] { switch modifier { case "ctrl": flags.insert(.maskControl); case "alt": flags.insert(.maskAlternate); case "shift": flags.insert(.maskShift); case "super": flags.insert(.maskCommand); default: fail("按键修饰符无效") } }
  87. for down in [true,false] { guard let event = CGEvent(keyboardEventSource:nil,virtualKey:key,keyDown:down) else { fail("无法创建键盘输入") };event.flags=flags;event.post(tap:.cghidEventTap) }
  88. case "scroll":
  89. guard let direction = action["direction"] as? String, ["up","down","left","right"].contains(direction) else { fail("滚动方向无效") }
  90. let amount = action["amount"] as? Int ?? 3
  91. guard (1...50).contains(amount) else { fail("滚动距离无效") }
  92. let vertical = direction == "up" ? amount : direction == "down" ? -amount : 0
  93. let horizontal = direction == "left" ? amount : direction == "right" ? -amount : 0
  94. guard let event = CGEvent(scrollWheelEvent2Source:nil,units:.line,wheelCount:2,wheel1:Int32(vertical),wheel2:Int32(horizontal),wheel3:0) else { fail("无法创建滚动输入") };event.post(tap:.cghidEventTap)
  95. default: fail("电脑操作无效")
  96. }
  97. FileHandle.standardOutput.write(Data("{\"ok\":true}".utf8))