diff --git a/ios6/agent/ios6-agent.c b/ios6/agent/ios6-agent.c index 238d53acdf..1f38a7942f 100644 --- a/ios6/agent/ios6-agent.c +++ b/ios6/agent/ios6-agent.c @@ -734,6 +734,42 @@ static void cmd_touch(int fd, char **a, int n) reply_str(fd,rc==0,"touch %d %s",rc,bundle); } +static const char *front_target(char *bundle, size_t len) +{ + load_sbs(); bundle[0]=0; + CFStringRef front=p_SBSCopyFrontmostApplicationDisplayIdentifier ? p_SBSCopyFrontmostApplicationDisplayIdentifier() : NULL; + if(front) { cfstr(front,bundle,len); CFRelease(front); } + return bundle[0] && strcmp(bundle,"com.apple.springboard") ? bundle : NULL; +} + +/* text : une touche par unité UTF-16, vers l'app au premier plan. */ +static void cmd_text(int fd, char **a, int n) +{ + if (n != 1) { reply_str(fd, 0, "text chaine"); return; } + CFStringRef s = CFStringCreateWithCString(NULL, a[0], kCFStringEncodingUTF8); + if (!s) { reply_str(fd, 0, "UTF-8 invalide"); return; } + char bundle[256]; const char *target = front_target(bundle, sizeof bundle); + int rc = 0; + for (CFIndex i = 0; i < CFStringGetLength(s) && !rc; i++) rc = send_key_event(CFStringGetCharacterAtIndex(s, i), target); + CFRelease(s); + reply_str(fd, rc == 0, "text %d %s", rc, bundle); +} + +/* key return|backspace|tab|0xNNNN */ +static void cmd_key(int fd, char **a, int n) +{ + static const struct { const char *name; UniChar code; } keys[] = { {"return", '\n'}, {"backspace", 0x08}, {"tab", '\t'} }; + if (n < 1 || n > 3) { reply_str(fd, 0, "key return|backspace|tab|0xNNNN [sequence DdUu [keycode]]"); return; } + long code = -1; + for (size_t i = 0; i < sizeof keys / sizeof *keys; i++) if (!strcmp(a[0], keys[i].name)) code = keys[i].code; + if (code < 0 && !strncmp(a[0], "0x", 2)) { char *end; code = strtol(a[0] + 2, &end, 16); if (*end || code <= 0 || code > 0xffff) code = -1; } + if (code < 0) { reply_str(fd, 0, "touche inconnue : %s", a[0]); return; } + char bundle[256]; const char *target = front_target(bundle, sizeof bundle); + int rc = n > 1 ? send_key_sequence((UniChar)code, target, a[1], n > 2 ? (uint16_t)strtol(a[2], NULL, 0) : 0) + : send_key_event((UniChar)code, target); + reply_str(fd, rc == 0, "key %d %s", rc, bundle); +} + static void handle(int fd, char *line) { char *args[64]; @@ -745,6 +781,8 @@ static void handle(int fd, char *line) int na = n - 1; if (!strcmp(cmd, "ping")) reply_str(fd, 1, "emuagent %s", VERSION); else if (!strcmp(cmd, "touch")) cmd_touch(fd,a,na); + else if (!strcmp(cmd, "text")) cmd_text(fd,a,na); + else if (!strcmp(cmd, "key")) cmd_key(fd,a,na); else if (!strcmp(cmd, "home")) { void *h=dlopen("/System/Library/PrivateFrameworks/SpringBoardServices.framework/SpringBoardServices",RTLD_NOW); int (*fn)(void)=h?dlsym(h,"SBSSuspendFrontmostApplication"):NULL; @@ -778,7 +816,7 @@ static void handle(int fd, char *line) else if (!strcmp(cmd, "net")) cmd_net(fd); else if (!strcmp(cmd, "respring")) reply_str(fd, kill_by_name("SpringBoard", SIGTERM) > 0, "SpringBoard relancé"); else if (!strcmp(cmd, "sync")) { sync(); reply_str(fd, 1, "ok"); } - else reply_str(fd, 0, "commande inconnue : %s (ping exec launch fetch sc routes settime screen wake front apps put get ls rm mkdir ps kill net respring sync)", cmd); + else reply_str(fd, 0, "commande inconnue : %s (ping touch text key exec launch fetch sc routes settime screen wake front apps put get ls rm mkdir ps kill net respring sync)", cmd); } static void serve_client(int fd) diff --git a/ios6/macos/iOS6Simulator.swift b/ios6/macos/iOS6Simulator.swift new file mode 100644 index 0000000000..175c12c8c5 --- /dev/null +++ b/ios6/macos/iOS6Simulator.swift @@ -0,0 +1,291 @@ +// Fenêtre macOS pour la VM iOS 6 : affiche l'écran via le VNC local de QEMU +// et transmet la souris et le clavier à l'agent invité (127.0.0.1:7796). +import AppKit + +let vncPort: UInt16 = 5906 +let agentPort: UInt16 = 7796 + +// MARK: - Sockets bloquantes + +final class Socket { + let fd: Int32 + init?(port: UInt16) { + fd = socket(AF_INET, SOCK_STREAM, 0) + if fd < 0 { return nil } + var one: Int32 = 1 + setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &one, socklen_t(MemoryLayout.size)) + var addr = sockaddr_in() + addr.sin_family = sa_family_t(AF_INET) + addr.sin_port = port.bigEndian + addr.sin_addr.s_addr = inet_addr("127.0.0.1") + let ok = withUnsafePointer(to: &addr) { + $0.withMemoryRebound(to: sockaddr.self, capacity: 1) { + connect(fd, $0, socklen_t(MemoryLayout.size)) == 0 + } + } + if !ok { close(fd); return nil } + } + deinit { close(fd) } + func read(_ n: Int) throws -> [UInt8] { + var out = [UInt8](repeating: 0, count: n) + var got = 0 + while got < n { + let r = out.withUnsafeMutableBytes { recv(fd, $0.baseAddress! + got, n - got, 0) } + if r <= 0 { throw POSIXError(.ECONNRESET) } + got += r + } + return out + } + func readInto(_ buffer: UnsafeMutableRawPointer, _ n: Int) throws { + var got = 0 + while got < n { + let r = recv(fd, buffer + got, n - got, 0) + if r <= 0 { throw POSIXError(.ECONNRESET) } + got += r + } + } + func write(_ bytes: [UInt8]) throws { + var sent = 0 + while sent < bytes.count { + let r = bytes.withUnsafeBytes { send(fd, $0.baseAddress! + sent, bytes.count - sent, 0) } + if r <= 0 { throw POSIXError(.EPIPE) } + sent += r + } + } + func readLine() throws -> String { + var line = [UInt8]() + while true { + let c = try read(1)[0] + if c == 10 { return String(decoding: line, as: UTF8.self) } + line.append(c) + } + } +} + +func u16(_ b: [UInt8], _ i: Int) -> Int { Int(b[i]) << 8 | Int(b[i + 1]) } +func u32(_ b: [UInt8], _ i: Int) -> Int { u16(b, i) << 16 | u16(b, i + 2) } +func be16(_ v: Int) -> [UInt8] { [UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] } + +// MARK: - Agent invité + +final class Agent { + private let queue = DispatchQueue(label: "agent") + private var warnedKeyboard = false + /// Envoie une commande de l'agent ; les gestes sont exécutés dans l'ordre. + func send(_ fields: [String]) { + queue.async { + guard let s = Socket(port: agentPort) else { return } + do { + try s.write(Array((fields.joined(separator: "\t") + "\n").utf8)) + let header = try s.readLine().split(separator: " ", maxSplits: 2).map(String.init) + let size = header.count > 1 ? Int(header[1]) ?? 0 : 0 + let body = String(decoding: try s.read(size), as: UTF8.self) + if header.first == "ERR" { + if body.contains("commande inconnue") { + if !self.warnedKeyboard { self.warnedKeyboard = true; NSLog("Clavier indisponible : l'agent n'a pas encore text/key") } + } else { NSLog("Agent : %@", body) } + } + } catch {} + } + } +} + +// MARK: - Client VNC (encodage brut) + +final class VNCClient { + var onFrame: ((CGImage?) -> Void)? + private var width = 0, height = 0 + private var pixels = [UInt8]() + + func run() { + Thread.detachNewThread { [weak self] in + while let self { + try? self.session() + DispatchQueue.main.async { self.onFrame?(nil) } + Thread.sleep(forTimeInterval: 1) + } + } + } + + private func session() throws { + guard let s = Socket(port: vncPort) else { return } + _ = try s.read(12) + try s.write(Array("RFB 003.008\n".utf8)) + let count = Int(try s.read(1)[0]) + guard count > 0, try s.read(count).contains(1) else { return } + try s.write([1]) + guard try s.read(4) == [0, 0, 0, 0] else { return } + try s.write([1]) // connexion partagée + let initMsg = try s.read(24) + resize(u16(initMsg, 0), u16(initMsg, 2)) + _ = try s.read(u32(initMsg, 20)) + // 32 bpp, profondeur 24, little-endian, rouge << 16 : octets BGRX. + try s.write([0, 0, 0, 0, 32, 24, 0, 1, 0, 255, 0, 255, 0, 255, 16, 8, 0, 0, 0, 0]) + try s.write([2, 0] + be16(2) + [0, 0, 0, 0] + [0xff, 0xff, 0xff, 0x21]) // brut, DesktopSize + try request(s, incremental: false) + while true { + let type = try s.read(1)[0] + switch type { + case 0: + let head = try s.read(3) + for _ in 0.. Bool { true } + + func show(_ image: CGImage?) { + wantsLayer = true + layer?.backgroundColor = NSColor.black.cgColor + layer?.contentsGravity = .resize + layer?.magnificationFilter = .nearest + layer?.contents = image + if let image { screenSize = CGSize(width: image.width, height: image.height) } + } + + private func guestPoint(_ event: NSEvent) -> CGPoint { + let p = convert(event.locationInWindow, from: nil) + let x = min(max(p.x / bounds.width * screenSize.width, 0), screenSize.width - 1) + let y = min(max(p.y / bounds.height * screenSize.height, 0), screenSize.height - 1) + return CGPoint(x: x.rounded(), y: y.rounded()) + } + + override func mouseDown(with event: NSEvent) { press = (guestPoint(event), Date()) } + + override func mouseUp(with event: NSEvent) { + guard let (start, time) = press else { return } + press = nil + var end = guestPoint(event) + var ms = Int(Date().timeIntervalSince(time) * 1000) + if abs(end.x - start.x) < 6 && abs(end.y - start.y) < 6 { end = start } + ms = min(max(ms, 96), 5000) + agent.send(["touch", "\(Int(start.x))", "\(Int(start.y))", "\(Int(end.x))", "\(Int(end.y))", "\(ms)"]) + } + + override func rightMouseDown(with event: NSEvent) { agent.send(["home"]) } + + override func keyDown(with event: NSEvent) { + switch event.keyCode { + case 53: agent.send(["home"]) // Échap + case 36, 76: agent.send(["key", "return"]) + case 51: agent.send(["key", "backspace"]) + case 48: agent.send(["key", "tab"]) + default: + guard let text = event.characters, !text.isEmpty, + !event.modifierFlags.contains(.command), + text.unicodeScalars.allSatisfy({ $0.value >= 0x20 && $0.value != 0x7f && $0.value < 0xf700 }) + else { return super.keyDown(with: event) } + agent.send(["text", text]) + } + } +} + +// MARK: - Application + +final class AppDelegate: NSObject, NSApplicationDelegate { + var window: NSWindow! + let vnc = VNCClient() + + func applicationDidFinishLaunching(_ note: Notification) { + let view = ScreenView(frame: NSRect(x: 0, y: 0, width: 320, height: 480)) + view.show(nil) + window = NSWindow(contentRect: view.frame, styleMask: [.titled, .closable, .miniaturizable, .resizable], + backing: .buffered, defer: false) + window.title = "iOS 6" + window.contentView = view + window.contentAspectRatio = NSSize(width: 320, height: 480) + window.setContentSize(NSSize(width: 320, height: 480)) + window.center() + window.makeKeyAndOrderFront(nil) + window.makeFirstResponder(view) + buildMenu(view) + vnc.onFrame = { [weak view] image in view?.show(image) } + vnc.run() + NSApp.activate(ignoringOtherApps: true) + } + + func applicationShouldTerminateAfterLastWindowClosed(_ app: NSApplication) -> Bool { true } + + private func buildMenu(_ view: ScreenView) { + let main = NSMenu() + let appItem = NSMenuItem(); main.addItem(appItem) + let appMenu = NSMenu() + appMenu.addItem(withTitle: "Quitter", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q") + appItem.submenu = appMenu + let deviceItem = NSMenuItem(); main.addItem(deviceItem) + let device = NSMenu(title: "Appareil") + for (title, key, action) in [("Home", "h", #selector(home)), ("Taille réelle", "1", #selector(size1)), ("Taille ×2", "2", #selector(size2))] { + let item = device.addItem(withTitle: title, action: action, keyEquivalent: key) + item.keyEquivalentModifierMask = [.command, .shift] + item.target = self + } + deviceItem.submenu = device + NSApp.mainMenu = main + } + + @objc func home() { (window.contentView as? ScreenView)?.agent.send(["home"]) } + @objc func size1() { window.setContentSize(NSSize(width: 320, height: 480)) } + @objc func size2() { window.setContentSize(NSSize(width: 640, height: 960)) } +} + +let app = NSApplication.shared +let delegate = AppDelegate() +app.delegate = delegate +app.setActivationPolicy(.regular) +app.run() diff --git a/ios6/src/session_input.h b/ios6/src/session_input.h index 098377808e..3123448a8c 100644 --- a/ios6/src/session_input.h +++ b/ios6/src/session_input.h @@ -40,6 +40,53 @@ static int send_touch_gesture(float x1, float y1, float x2, float y2, unsigned d return 0; } +/* Touche clavier matériel (GSEventKeyDown 10 / KeyUp 11). GSEventCreateKeyEvent + * exige le clavier d'un processus UIKit : l'enregistrement est donc construit + * à la main. GSHardwareKeyInfo (144 octets) suit l'en-tête de 52 octets : + * keyCode, charIgnoringModifier, charCode, charSet, puis longueur et UTF-16 + * des caractères (32 max), même chose pour les caractères non modifiés. */ +static int send_key_sequence(UniChar unicode, const char *bundle, const char *seq, uint16_t keycode); +static int send_key_event(UniChar unicode, const char *bundle) +{ + /* Vérifié sous iOS 6.1.6 : UIKit insère le caractère porté par chaque + * événement, et seul un relâchement avec caractère arrête la répétition. + * L'appui est donc vide et le relâchement porte le caractère. */ + return send_key_sequence(unicode, bundle, "dU", 0); +} + +/* seq : D/d appui avec/sans caractère, U/u relâchement avec/sans caractère. */ +static int send_key_sequence(UniChar unicode, const char *bundle, const char *seq, uint16_t keycode) +{ + void *gs = dlopen("/System/Library/PrivateFrameworks/GraphicsServices.framework/GraphicsServices", RTLD_NOW); + if (!gs) return -1; + void (*send)(const void *) = dlsym(gs, "GSSendSystemEvent"); + void (*send_port)(const void *, unsigned) = dlsym(gs, "GSSendEvent"); + unsigned (*copy_port)(const char *) = dlsym(gs, "GSCopyPurpleNamedPort"); + uint64_t (*timestamp)(void) = dlsym(gs, "GSCurrentEventTimestamp"); + if (!send || !timestamp) { dlclose(gs); return -1; } + unsigned port = bundle && copy_port ? copy_port(bundle) : 0; + if (bundle && (!port || !send_port)) { dlclose(gs); return -2; } + for (const char *step = seq; *step; step++) { + uint32_t packet[49] = {0}; /* 52 + 144 octets */ + unsigned char *bytes = (void *)packet; + uint64_t time = timestamp(); + UniChar info[4] = {keycode, unicode, unicode, 0}; + uint16_t one = *step == 'D' || *step == 'U'; + packet[0] = *step == 'D' || *step == 'd' ? 10 : 11; + memcpy(bytes + 28, &time, sizeof(time)); + packet[12] = 144; /* infoSize */ + memcpy(bytes + 52, info, sizeof(info)); + memcpy(bytes + 60, &one, 2); /* characters_length */ + memcpy(bytes + 62, &unicode, 2); /* characters[0] */ + memcpy(bytes + 126, &one, 2); /* unmodified_characters_length */ + memcpy(bytes + 128, &unicode, 2); + if (port) send_port(packet, port); else send(packet); + usleep(50000); /* 20 ms : caractères perdus près des espaces */ + } + dlclose(gs); + return 0; +} + static void send_unlock_swipe(void) { send_touch_gesture(50, 433, 245, 433, 800, NULL); diff --git a/ios6/tools/build.sh b/ios6/tools/build.sh index d37afdbf22..0d4e1bb4a0 100755 --- a/ios6/tools/build.sh +++ b/ios6/tools/build.sh @@ -18,6 +18,7 @@ cd build --target-list=arm-softmmu --disable-docs --disable-werror \ --disable-gtk --disable-sdl --disable-slirp --disable-capstone \ --disable-tools --disable-guest-agent --disable-debug-info \ + --extra-cflags="$(pkg-config --cflags openssl 2>/dev/null)" \ --extra-ldflags="$(pkg-config --libs-only-L openssl 2>/dev/null) -lcrypto" > ../evidence/configure.log 2>&1 ninja -j"${IOS6_BUILD_JOBS:-4}" qemu-system-arm > ../evidence/build.log 2>&1 cd .. diff --git a/ios6/tools/build_agent.sh b/ios6/tools/build_agent.sh index 60724cd9a5..3c60b930cc 100755 --- a/ios6/tools/build_agent.sh +++ b/ios6/tools/build_agent.sh @@ -3,6 +3,16 @@ set -euo pipefail sim_root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) cd "$sim_root" mkdir -p build -/opt/cctools/bin/arm-apple-darwin9-gcc -isysroot /opt/theos/sdks/iPhoneOS2.2.1.sdk -miphoneos-version-min=2.2.1 -std=gnu99 -O2 agent/ios6-agent.c -framework CoreFoundation -framework SystemConfiguration -o build/ios6-agent -/opt/cctools/bin/ldid -Sagent/entitlements.plist build/ios6-agent +if [[ -x /opt/cctools/bin/arm-apple-darwin9-gcc ]]; then + /opt/cctools/bin/arm-apple-darwin9-gcc -isysroot /opt/theos/sdks/iPhoneOS2.2.1.sdk -miphoneos-version-min=2.2.1 -std=gnu99 -O2 agent/ios6-agent.c -framework CoreFoundation -framework SystemConfiguration -o build/ios6-agent + /opt/cctools/bin/ldid -Sagent/entitlements.plist build/ios6-agent +else + # macOS : clang de Xcode, mêmes contraintes que tools/build_app.py + # (cible 5.1 pour LC_UNIXTHREAD, code ARM seulement). + sdk=${IOS6_SDK:-$sim_root/references/sdk/iPhoneOS6.0.sdk} + [[ -f $sdk/SDKSettings.plist ]] || sdk=${THEOS:-$HOME/theos}/sdks/iPhoneOS6.1.sdk + xcrun clang -target armv7-apple-ios5.1 -marm -isysroot "$sdk" -std=gnu99 -O2 -Wno-incompatible-pointer-types-discards-qualifiers \ + agent/ios6-agent.c -framework CoreFoundation -framework SystemConfiguration -o build/ios6-agent + ldid -Sagent/entitlements.plist build/ios6-agent +fi bash tools/build_netd.sh diff --git a/ios6/tools/build_app.py b/ios6/tools/build_app.py index 9d45ae931d..1f24027625 100644 --- a/ios6/tools/build_app.py +++ b/ios6/tools/build_app.py @@ -1,15 +1,29 @@ #!/usr/bin/env python3 """Compile un projet Objective-C simple (.m) en bundle ARM pour la VM iOS 6.""" -import argparse,os,pathlib,plistlib,subprocess +import argparse,os,pathlib,plistlib,shutil,subprocess ROOT=pathlib.Path(__file__).resolve().parents[1] +CCTOOLS=pathlib.Path('/opt/cctools/bin') +def default_sdk(): + if os.environ.get('IOS6_SDK'): return os.environ['IOS6_SDK'] + for sdk in (ROOT/'references/sdk/iPhoneOS6.0.sdk',pathlib.Path(os.environ.get('THEOS',pathlib.Path.home()/'theos'))/'sdks/iPhoneOS6.1.sdk'): + if (sdk/'SDKSettings.plist').is_file(): return str(sdk) + return str(ROOT/'references/sdk/iPhoneOS6.0.sdk') +def compiler(): + """GCC cctools sous Linux, clang de Xcode sous macOS.""" + if (CCTOOLS/'arm-apple-darwin9-gcc').exists(): return [str(CCTOOLS/'arm-apple-darwin9-gcc'),'-arch','armv7','-miphoneos-version-min=6.0'],str(CCTOOLS/'ldid') + # Le ld récent ne pose plus le bit Thumb sur le point d'entrée LC_MAIN ni sur + # les IMP Objective-C : sous iOS 6, le saut part en mode ARM (SIGBUS + # EXC_ARM_DA_ALIGN). Cible 5.1 (crt1.o, LC_UNIXTHREAD) et code ARM seulement. + return ['xcrun','clang','-target','armv7-apple-ios5.1','-marm'],shutil.which('ldid') or 'ldid' def main(): - p=argparse.ArgumentParser(description=__doc__);p.add_argument('project',type=pathlib.Path,nargs='?',default=ROOT/'apps/SimulatorLab');p.add_argument('--bundle-id');p.add_argument('--sdk',default=os.environ.get('IOS6_SDK',str(ROOT/'references/sdk/iPhoneOS6.0.sdk')));a=p.parse_args() + p=argparse.ArgumentParser(description=__doc__);p.add_argument('project',type=pathlib.Path,nargs='?',default=ROOT/'apps/SimulatorLab');p.add_argument('--bundle-id');p.add_argument('--sdk',default=default_sdk());a=p.parse_args() if not pathlib.Path(a.sdk,'SDKSettings.plist').is_file(): p.error('SDK iOS absent : exécuter tools/prepare_sdk.py ou fournir --sdk') name=a.project.resolve().name;out=ROOT/'build/apps'/(name+'.app');out.mkdir(parents=True,exist_ok=True) a.bundle_id=a.bundle_id or 'org.ios6emu.'+name sources=sorted(a.project.glob('*.m')) if not sources: p.error('Aucune source Objective-C') - subprocess.run(['/opt/cctools/bin/arm-apple-darwin9-gcc','-isysroot',a.sdk,'-miphoneos-version-min=6.0','-arch','armv7','-fblocks','-std=gnu99','-O2','-Wall','-Wextra',*map(str,sources),'-framework','UIKit','-framework','Foundation','-framework','CoreGraphics','-o',str(out/name)],check=True) + cc,ldid=compiler() + subprocess.run([*cc,'-isysroot',a.sdk,'-fblocks','-std=gnu99','-O2','-Wall','-Wextra',*map(str,sources),'-framework','UIKit','-framework','Foundation','-framework','CoreGraphics','-o',str(out/name)],check=True) (out/'Info.plist').write_bytes(plistlib.dumps({'CFBundleIdentifier':a.bundle_id,'CFBundleExecutable':name,'CFBundleName':name,'CFBundleDisplayName':name,'CFBundlePackageType':'APPL','CFBundleVersion':'1','MinimumOSVersion':'6.0','LSRequiresIPhoneOS':True,'UIDeviceFamily':[1]})) - subprocess.run(['/opt/cctools/bin/ldid','-S',str(out/name)],check=True);print(out) + subprocess.run([ldid,'-S',str(out/name)],check=True);print(out) if __name__=='__main__': main() diff --git a/ios6/tools/build_mac_app.sh b/ios6/tools/build_mac_app.sh new file mode 100644 index 0000000000..daecc7403a --- /dev/null +++ b/ios6/tools/build_mac_app.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# Compile la fenêtre macOS de la VM iOS 6 dans build/iOS6 Simulator.app. +set -euo pipefail +sim_root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) +app="$sim_root/build/iOS6 Simulator.app" +mkdir -p "$app/Contents/MacOS" +xcrun swiftc -O -o "$app/Contents/MacOS/iOS6Simulator" "$sim_root/macos/iOS6Simulator.swift" +cat > "$app/Contents/Info.plist" <<'PLIST' + + + +CFBundleIdentifierorg.ios6emu.mac +CFBundleNameiOS6 Simulator +CFBundleExecutableiOS6Simulator +CFBundlePackageTypeAPPL +CFBundleShortVersionString1.0 +LSMinimumSystemVersion13.0 +NSHighResolutionCapable + +PLIST +codesign -s - --force "$app" >/dev/null +echo "$app" diff --git a/ios6/tools/build_netd.sh b/ios6/tools/build_netd.sh index 969494806a..8a59c3656c 100755 --- a/ios6/tools/build_netd.sh +++ b/ios6/tools/build_netd.sh @@ -4,5 +4,6 @@ set -euo pipefail sim_root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) cd "$sim_root" mkdir -p build -cc -O2 agent/netd.c -o build/ios6-netd.new $(pkg-config --cflags --libs slirp) +cc -O2 agent/netd.c -o build/ios6-netd.new \ + -I"$(pkg-config --variable=includedir slirp)" $(pkg-config --cflags --libs slirp) mv build/ios6-netd.new build/ios6-netd diff --git a/ios6/tools/recover_hfs.py b/ios6/tools/recover_hfs.py index 78d64abdbf..fc18b30846 100644 --- a/ios6/tools/recover_hfs.py +++ b/ios6/tools/recover_hfs.py @@ -11,8 +11,10 @@ import argparse import json import os import pathlib +import shutil import struct import subprocess +import sys def checksum(data): @@ -97,7 +99,12 @@ def recover(source, destination): position = advance(position, used) headers += 1 # Toutes les validations précèdent la création de la copie. - subprocess.run(['cp', '--reflink=auto', '--sparse=always', str(source), str(destination)], check=True) + if sys.platform == 'darwin': + # cp BSD : clone APFS (-c), copie simple si le volume ne le permet pas. + if subprocess.run(['cp', '-c', str(source), str(destination)]).returncode: + shutil.copyfile(source, destination) + else: + subprocess.run(['cp', '--reflink=auto', '--sparse=always', str(source), str(destination)], check=True) with destination.open('r+b') as image: for target, data in writes: image.seek(target) diff --git a/ios6/tools/update_agent.py b/ios6/tools/update_agent.py new file mode 100644 index 0000000000..13fdd47002 --- /dev/null +++ b/ios6/tools/update_agent.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +"""Remplace l'agent invité par build/ios6-agent ; launchd (KeepAlive) le relance. + +Le fichier est écrit de façon atomique puis l'agent courant est arrêté. Lancer +ensuite `bin/ios6 stop` pour conserver la nouvelle version sur le disque. +""" +import time + +from simulator import Agent, ROOT, command + +def main(): + data = (ROOT / 'build/ios6-agent').read_bytes() + with Agent() as agent: + agent.call('put', '/usr/libexec/ios6-agent', '755', len(data), data=data) + agent.call('sync') + # Chaque client est servi par un fils : arrêter le processus lancé par launchd. + main_pid = next(line.split()[0] for line in command('ps').splitlines() + if line.split()[1:] == ['1', '0', 'ios6-agent']) + try: command('kill', main_pid, 15, timeout=5) + except Exception: pass # l'agent s'arrête avant de répondre + deadline = time.monotonic() + 30 + while True: + time.sleep(1) + try: print('agent relancé :', command('ping', timeout=5)); return + except Exception: + if time.monotonic() > deadline: raise SystemExit("L'agent ne répond plus") + +if __name__ == '__main__': main() diff --git a/ios6/tools/vnc_input.py b/ios6/tools/vnc_input.py new file mode 100644 index 0000000000..5a7aeb17dc --- /dev/null +++ b/ios6/tools/vnc_input.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +"""Relais VNC : transmet l'affichage de QEMU et convertit la souris en gestes tactiles. + +Le client VNC se connecte au relais (127.0.0.1:5907 par défaut). L'affichage +et les messages du client passent tels quels vers QEMU (127.0.0.1:5906), sauf +les événements souris et clavier : un appui suivi d'un relâchement du bouton +gauche devient un geste `touch` de l'agent (tap ou glissé, avec sa durée), le +bouton droit et la touche Échap envoient `home`. Les touches imprimables +deviennent `text `, Entrée, Retour arrière et Tab `key `. +""" +import argparse +import queue +import socket +import struct +import sys +import threading +import time + +from simulator import command + +WIDTH, HEIGHT = 320, 480 +KEY_ESCAPE, KEY_HOME = 0xff1b, 0xff50 +SPECIAL_KEYS = {0xff0d: 'return', 0xff8d: 'return', 0xff08: 'backspace', 0xff09: 'tab'} + +def recv_exact(sock, n): + data = bytearray() + while len(data) < n: + chunk = sock.recv(n - len(data)) + if not chunk: raise ConnectionError + data.extend(chunk) + return bytes(data) + +def pipe(src, dst): + try: + while chunk := src.recv(65536): dst.sendall(chunk) + except OSError: pass + finally: + for s in (src, dst): + try: s.shutdown(socket.SHUT_RDWR) + except OSError: pass + +def gestures(actions): + warned = False + while True: + args = actions.get() + try: command(*args) + except Exception as e: + if 'commande inconnue' not in str(e): print('geste refusé :', e, file=sys.stderr, flush=True) + elif not warned: + warned = True + print("clavier indisponible : l'agent invité n'a pas encore les commandes text/key", file=sys.stderr, flush=True) + +def clamp(v, top): return min(max(v, 0), top - 1) + +def client_messages(client, qemu, actions): + """Lit le flux client RFB message par message pour isoler la souris et le clavier.""" + version = recv_exact(client, 12); qemu.sendall(version) + if version != b'RFB 003.003\n': qemu.sendall(recv_exact(client, 1)) # type de sécurité + qemu.sendall(recv_exact(client, 1)) # ClientInit + buttons, press = 0, None + while True: + kind = recv_exact(client, 1)[0] + if kind == 5: + mask, x, y = struct.unpack('>BHH', recv_exact(client, 5)) + x, y = clamp(x, WIDTH), clamp(y, HEIGHT) + if mask & 1 and not buttons & 1: press = (x, y, time.monotonic()) + elif buttons & 1 and not mask & 1 and press: + x0, y0, t0 = press; press = None + ms = int((time.monotonic() - t0) * 1000) + if abs(x - x0) < 6 and abs(y - y0) < 6: x, y, ms = x0, y0, max(ms, 96) + actions.put(('touch', x0, y0, x, y, min(max(ms, 96), 5000))) + if mask & 4 and not buttons & 4: actions.put(('home',)) + buttons = mask + continue + if kind == 4: + down, key = struct.unpack('>B2xI', recv_exact(client, 7)) + if not down: continue + if key in (KEY_ESCAPE, KEY_HOME): actions.put(('home',)) + elif key in SPECIAL_KEYS: actions.put(('key', SPECIAL_KEYS[key])) + elif 0x20 <= key < 0x100 and key != 0x7f: actions.put(('text', chr(key))) + elif key & 0xff000000 == 0x01000000: actions.put(('text', chr(key & 0xffffff))) + continue + if kind == 255: + body = recv_exact(client, 11) # événement clavier étendu QEMU, ignoré + if body[0] != 0: raise ConnectionError(f'message QEMU {body[0]} non géré') + continue + if kind == 0: body = recv_exact(client, 19) + elif kind == 2: + head = recv_exact(client, 3); body = head + recv_exact(client, 4 * struct.unpack('>xH', head)[0]) + elif kind == 3: body = recv_exact(client, 9) + elif kind == 6: + head = recv_exact(client, 7); body = head + recv_exact(client, struct.unpack('>3xI', head)[0]) + elif kind == 150: body = recv_exact(client, 9) + elif kind == 248: + head = recv_exact(client, 7); body = head + recv_exact(client, 16 * head[4]) + elif kind == 250: + head = recv_exact(client, 8); body = head + recv_exact(client, head[7]) + else: raise ConnectionError(f'message client {kind} non géré') + qemu.sendall(bytes([kind]) + body) + +def serve(client, target, actions): + with client, socket.create_connection(target) as qemu: + threading.Thread(target=pipe, args=(qemu, client), daemon=True).start() + try: client_messages(client, qemu, actions) + except (ConnectionError, OSError) as e: + if str(e): print('connexion fermée :', e, file=sys.stderr, flush=True) + +def main(): + p = argparse.ArgumentParser(description=__doc__) + p.add_argument('--port', type=int, default=5907) + p.add_argument('--qemu-port', type=int, default=5906) + a = p.parse_args() + actions = queue.Queue() + threading.Thread(target=gestures, args=(actions,), daemon=True).start() + server = socket.create_server(('127.0.0.1', a.port)) + print(f'Relais VNC tactile : vnc://127.0.0.1:{a.port}', flush=True) + while True: + client, _ = server.accept() + threading.Thread(target=serve, args=(client, ('127.0.0.1', a.qemu_port), actions), daemon=True).start() + +if __name__ == '__main__': main()