From b956cde48765a586273e94d87367edc748c1f322 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ole=20Andr=C3=A9=20Vadla=20Ravn=C3=A5s?= Date: Thu, 30 May 2024 13:40:21 +0200 Subject: [PATCH] fruity: Add RemoteXPC support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Kudos to @doronz88 for figuring out most of the protocol bits that we needed. Co-authored-by: Håvard Sørbø --- lib/base/frida-darwin.c | 51 + lib/base/frida-darwin.h | 22 + lib/base/meson.build | 14 + lib/base/xpc.vala | 236 ++ meson.build | 45 +- src/droidy/droidy-host-session.vala | 8 +- src/frida.vala | 101 +- src/fruity/dtx.vala | 280 +- src/fruity/fruity-host-session.vala | 800 ++++- src/fruity/injector.vala | 6 +- src/fruity/iokit.vala | 88 + src/fruity/xpc-darwin.vala | 298 ++ src/fruity/xpc-linux.vala | 426 +++ src/fruity/xpc-macos.vala | 421 +++ src/fruity/xpc-test.vala | 363 +++ src/fruity/xpc.vala | 4588 +++++++++++++++++++++++++++ src/host-session-service.vala | 10 +- src/meson.build | 45 + tests/test-host-session.vala | 61 +- vapi/corefoundation.vapi | 104 + vapi/darwin-dns-sd.vapi | 134 + vapi/darwin-gcd.vapi | 30 + vapi/darwin-iokit.vapi | 58 + vapi/darwin-net.vapi | 6 + vapi/darwin-xnu.vapi | 104 + vapi/darwin-xpc.vapi | 161 + vapi/libnghttp2.vapi | 358 +++ vapi/libngtcp2.vapi | 535 ++++ vapi/libngtcp2_crypto_quictls.vapi | 28 + vapi/lwip.vapi | 195 ++ vapi/openssl.vapi | 501 ++- 31 files changed, 9863 insertions(+), 214 deletions(-) create mode 100644 lib/base/frida-darwin.c create mode 100644 lib/base/frida-darwin.h create mode 100644 lib/base/xpc.vala create mode 100644 src/fruity/iokit.vala create mode 100644 src/fruity/xpc-darwin.vala create mode 100644 src/fruity/xpc-linux.vala create mode 100644 src/fruity/xpc-macos.vala create mode 100644 src/fruity/xpc-test.vala create mode 100644 src/fruity/xpc.vala create mode 100644 vapi/corefoundation.vapi create mode 100644 vapi/darwin-dns-sd.vapi create mode 100644 vapi/darwin-gcd.vapi create mode 100644 vapi/darwin-iokit.vapi create mode 100644 vapi/darwin-net.vapi create mode 100644 vapi/darwin-xnu.vapi create mode 100644 vapi/darwin-xpc.vapi create mode 100644 vapi/libnghttp2.vapi create mode 100644 vapi/libngtcp2.vapi create mode 100644 vapi/libngtcp2_crypto_quictls.vapi create mode 100644 vapi/lwip.vapi diff --git a/lib/base/frida-darwin.c b/lib/base/frida-darwin.c new file mode 100644 index 000000000..495f8b561 --- /dev/null +++ b/lib/base/frida-darwin.c @@ -0,0 +1,51 @@ +#include "frida-darwin.h" + +gpointer +_frida_dispatch_retain (gpointer object) +{ + dispatch_retain (object); + return object; +} + +void +_frida_xpc_connection_set_event_handler (xpc_connection_t connection, FridaXpcHandler handler, gpointer user_data) +{ + xpc_connection_set_event_handler (connection, ^(xpc_object_t object) + { + handler (object, user_data); + }); +} + +void +_frida_xpc_connection_send_message_with_reply (xpc_connection_t connection, xpc_object_t message, dispatch_queue_t replyq, + FridaXpcHandler handler, gpointer user_data, GDestroyNotify notify) +{ + xpc_connection_send_message_with_reply (connection, message, replyq, ^(xpc_object_t object) + { + handler (object, user_data); + if (notify != NULL) + notify (user_data); + }); +} + +gchar * +_frida_xpc_object_to_string (xpc_object_t object) +{ + gchar * result; + char * str; + + str = xpc_copy_description (object); + result = g_strdup (str); + free (str); + + return result; +} + +gboolean +_frida_xpc_dictionary_apply (xpc_object_t dict, FridaXpcDictionaryApplier applier, gpointer user_data) +{ + return xpc_dictionary_apply (dict, ^bool (const char * key, xpc_object_t val) + { + return applier (key, val, user_data); + }); +} diff --git a/lib/base/frida-darwin.h b/lib/base/frida-darwin.h new file mode 100644 index 000000000..cf0153038 --- /dev/null +++ b/lib/base/frida-darwin.h @@ -0,0 +1,22 @@ +#ifndef __FRIDA_DARWIN_H__ +#define __FRIDA_DARWIN_H__ + +#ifdef HAVE_MACOS + +#include +#include + +typedef void (* FridaXpcHandler) (xpc_object_t object, gpointer user_data); +typedef gboolean (* FridaXpcDictionaryApplier) (const gchar * key, xpc_object_t val, gpointer user_data); + +gpointer _frida_dispatch_retain (gpointer object); + +void _frida_xpc_connection_set_event_handler (xpc_connection_t connection, FridaXpcHandler handler, gpointer user_data); +void _frida_xpc_connection_send_message_with_reply (xpc_connection_t connection, xpc_object_t message, dispatch_queue_t replyq, + FridaXpcHandler handler, gpointer user_data, GDestroyNotify notify); +gchar * _frida_xpc_object_to_string (xpc_object_t object); +gboolean _frida_xpc_dictionary_apply (xpc_object_t dict, FridaXpcDictionaryApplier applier, gpointer user_data); + +#endif + +#endif diff --git a/lib/base/meson.build b/lib/base/meson.build index a98193057..d40f3691b 100644 --- a/lib/base/meson.build +++ b/lib/base/meson.build @@ -13,6 +13,14 @@ base_sources = [ ] extra_vala_args = [] + +if host_os == 'macos' + base_sources += [ + 'xpc.vala', + 'frida-darwin.c', + ] +endif + extra_deps = [] if host_os_family != 'windows' extra_vala_args += '--pkg=gio-unix-2.0' @@ -23,6 +31,12 @@ base_vala_args = [gum_vala_args, '--pkg=gio-2.0', '--pkg=json-glib-1.0'] if nice_dep.found() base_vala_args += '--pkg=nice' endif +if host_os_family == 'darwin' + base_vala_args += [ + '--pkg=darwin-gcd', + '--pkg=darwin-xpc', + ] +endif base = static_library('frida-base-' + api_version, base_sources, c_args: frida_component_cflags, diff --git a/lib/base/xpc.vala b/lib/base/xpc.vala new file mode 100644 index 000000000..85d242a38 --- /dev/null +++ b/lib/base/xpc.vala @@ -0,0 +1,236 @@ +namespace Frida { + public class XpcClient : Object { + public signal void message (Darwin.Xpc.Object obj); + + public State state { + get { + return _state; + } + } + + public Darwin.Xpc.Connection connection { + get; + construct; + } + + public Darwin.GCD.DispatchQueue queue { + get; + construct; + } + + private State _state; + private string? close_reason; + private MainContext main_context; + + public enum State { + OPEN, + CLOSING, + CLOSED, + } + + public static XpcClient make_for_mach_service (string name, Darwin.GCD.DispatchQueue queue) { + return new XpcClient (Darwin.Xpc.Connection.create_mach_service (name, queue, 0), queue); + } + + public XpcClient (Darwin.Xpc.Connection connection, Darwin.GCD.DispatchQueue queue) { + Object (connection: connection, queue: queue); + } + + construct { + main_context = MainContext.ref_thread_default (); + + connection.set_event_handler (on_event); + connection.activate (); + } + + public override void dispose () { + if (close_reason != null) { + change_state (CLOSED); + } else { + change_state (CLOSING); + this.ref (); + connection.cancel (); + } + + base.dispose (); + } + + public async Darwin.Xpc.Object request (Darwin.Xpc.Object message, Cancellable? cancellable) throws Error, IOError { + Darwin.Xpc.Object? reply = null; + connection.send_message_with_reply (message, queue, r => { + schedule_on_frida_thread (() => { + reply = r; + request.callback (); + return Source.REMOVE; + }); + }); + + yield; + + if (reply.type == Darwin.Xpc.Error.TYPE) { + var e = (Darwin.Xpc.Error) reply; + throw new Error.NOT_SUPPORTED ("%s", e.get_string (Darwin.Xpc.Error.KEY_DESCRIPTION)); + } + + return reply; + } + + private void change_state (State new_state) { + _state = new_state; + notify_property ("state"); + } + + private void on_event (Darwin.Xpc.Object obj) { + schedule_on_frida_thread (() => { + if (obj.type == Darwin.Xpc.Error.TYPE) { + var e = (Darwin.Xpc.Error) obj; + close_reason = e.get_string (Darwin.Xpc.Error.KEY_DESCRIPTION); + if (state == CLOSING) { + change_state (CLOSED); + unref (); + } + } else { + message (obj); + } + return Source.REMOVE; + }); + } + + private void schedule_on_frida_thread (owned SourceFunc function) { + var source = new IdleSource (); + source.set_callback ((owned) function); + source.attach (main_context); + } + } + + public class XpcObjectReader { + public Darwin.Xpc.Object root_object { + get { + return scopes.peek_head ().object; + } + } + + public Darwin.Xpc.Object current_object { + get { + return scopes.peek_tail ().object; + } + } + + private Gee.Deque scopes = new Gee.ArrayQueue (); + + public XpcObjectReader (Darwin.Xpc.Object obj) { + push_scope (obj); + } + + public bool has_member (string name) throws Error { + return peek_scope ().get_dictionary ().get_value (name) != null; + } + + public bool try_read_member (string name) throws Error { + var scope = peek_scope (); + var dict = scope.get_dictionary (); + var val = dict.get_value (name); + if (val == null) + return false; + + push_scope (val); + + return true; + } + + public unowned XpcObjectReader read_member (string name) throws Error { + var scope = peek_scope (); + var dict = scope.get_dictionary (); + var val = dict.get_value (name); + if (val == null) + throw new Error.PROTOCOL ("Key '%s' not found in dictionary: %s", name, scope.object.to_string ()); + + push_scope (val); + + return this; + } + + public unowned XpcObjectReader end_member () { + pop_scope (); + + return this; + } + + public size_t count_elements () throws Error { + return peek_scope ().get_array ().count; + } + + public unowned XpcObjectReader read_element (size_t index) throws Error { + push_scope (peek_scope ().get_array ().get_value (index)); + + return this; + } + + public unowned XpcObjectReader end_element () throws Error { + pop_scope (); + + return this; + } + + public bool get_bool_value () throws Error { + return peek_scope ().get_object (Darwin.Xpc.Bool.TYPE).get_value (); + } + + public int64 get_int64_value () throws Error { + return peek_scope ().get_object (Darwin.Xpc.Int64.TYPE).get_value (); + } + + public uint64 get_uint64_value () throws Error { + return peek_scope ().get_object (Darwin.Xpc.UInt64.TYPE).get_value (); + } + + public unowned string get_string_value () throws Error { + return peek_scope ().get_object (Darwin.Xpc.String.TYPE).get_string_ptr (); + } + + public unowned string get_error_description () throws Error { + var error = peek_scope ().get_object (Darwin.Xpc.Error.TYPE); + return error.get_string (Darwin.Xpc.Error.KEY_DESCRIPTION); + } + + public unowned Darwin.Xpc.Object get_object_value (Darwin.Xpc.Type expected_type) throws Error { + return peek_scope ().get_object (expected_type); + } + + private void push_scope (Darwin.Xpc.Object obj) { + scopes.offer_tail (new Scope (obj)); + } + + private Scope peek_scope () { + return scopes.peek_tail (); + } + + private Scope pop_scope () { + return scopes.poll_tail (); + } + + private class Scope { + public Darwin.Xpc.Object object; + private unowned Darwin.Xpc.Type type; + + public Scope (Darwin.Xpc.Object obj) { + object = obj; + type = obj.type; + } + + public unowned T get_object (Darwin.Xpc.Type expected_type) throws Error { + if (type != expected_type) + throw new Error.PROTOCOL ("Expected type '%s', got '%s'", expected_type.name, type.name); + return object; + } + + public unowned Darwin.Xpc.Array get_array () throws Error { + return get_object (Darwin.Xpc.Array.TYPE); + } + + public unowned Darwin.Xpc.Dictionary get_dictionary () throws Error { + return get_object (Darwin.Xpc.Dictionary.TYPE); + } + } + } +} diff --git a/meson.build b/meson.build index 007824ba6..930d54944 100644 --- a/meson.build +++ b/meson.build @@ -437,6 +437,39 @@ backend_deps_private = [] backend_reqs_private = [] backend_libs_private = [] +nice_dep = dependency('nice', required: get_option('connectivity'), default_options: [ + 'gupnp=disabled', + 'gstreamer=disabled', + 'crypto-library=openssl', + 'examples=disabled', + 'tests=disabled', + 'introspection=disabled', +]) + +if have_fruity_backend or nice_dep.found() + openssl_options = [ + 'cli=disabled', + ] + if host_os_family == 'windows' and cc.get_argument_syntax() != 'msvc' + openssl_options += 'asm=disabled' + endif + openssl_dep = dependency('openssl', + modules: cc.get_argument_syntax() == 'msvc' ? ['OpenSSL::SSL'] : [], + default_options: openssl_options, + ) +else + openssl_dep = [] +endif + +if have_fruity_backend + backend_deps_private += [ + dependency('libnghttp2'), + dependency('libngtcp2'), + dependency('libngtcp2_crypto_quictls'), + dependency('lwip'), + ] +endif + if quickjs_dep.found() backend_deps_private += quickjs_dep backend_reqs_private += 'quickjs' @@ -464,16 +497,7 @@ if tls_provider_dep.found() backend_reqs_private += 'gioopenssl' endif -nice_dep = dependency('nice', required: get_option('connectivity'), default_options: [ - 'gupnp=disabled', - 'gstreamer=disabled', - 'crypto-library=openssl', - 'examples=disabled', - 'tests=disabled', - 'introspection=disabled', -]) if nice_dep.found() - openssl_dep = dependency('openssl') usrsctp_dep = dependency('usrsctp', default_options: [ 'sctp_inet=false', 'sctp_inet6=false', @@ -489,7 +513,6 @@ if nice_dep.found() cdata.set('HAVE_SCONN_LEN', 1) endif else - openssl_dep = [] usrsctp_dep = [] endif @@ -500,7 +523,7 @@ if host_os_family == 'darwin' backend_libs_private += ['-Wl,-framework,Foundation', '-lbsm'] endif if host_os == 'macos' - backend_libs_private += ['-Wl,-framework,AppKit'] + backend_libs_private += ['-Wl,-framework,AppKit', '-Wl,-framework,IOKit'] endif if host_os in ['ios', 'tvos'] backend_libs_private += ['-Wl,-framework,CoreGraphics', '-Wl,-framework,UIKit'] diff --git a/src/droidy/droidy-host-session.vala b/src/droidy/droidy-host-session.vala index 8bf559e09..0a8dc6fd3 100644 --- a/src/droidy/droidy-host-session.vala +++ b/src/droidy/droidy-host-session.vala @@ -86,7 +86,7 @@ namespace Frida { } } - public class DroidyHostSessionProvider : Object, HostSessionProvider, ChannelProvider { + public class DroidyHostSessionProvider : Object, HostSessionProvider, HostChannelProvider { public string id { get { return device_details.serial; } } @@ -162,7 +162,7 @@ namespace Frida { agent_session_detached (id, reason, crash); } - public async IOStream open_channel (string address, Cancellable? cancellable = null) throws Error, IOError { + public async IOStream open_channel (string address, Cancellable? cancellable) throws Error, IOError { if (address.contains (":")) { Droidy.Client client = null; try { @@ -188,7 +188,7 @@ namespace Frida { construct; } - public weak ChannelProvider channel_provider { + public weak HostChannelProvider channel_provider { get; construct; } @@ -216,7 +216,7 @@ namespace Frida { private const double MIN_SERVER_CHECK_INTERVAL = 5.0; private const string GADGET_APP_ID = "re.frida.Gadget"; - public DroidyHostSession (Droidy.DeviceDetails device_details, ChannelProvider channel_provider) { + public DroidyHostSession (Droidy.DeviceDetails device_details, HostChannelProvider channel_provider) { Object ( device_details: device_details, channel_provider: channel_provider diff --git a/src/frida.vala b/src/frida.vala index ab12f07aa..46d9a1c47 100644 --- a/src/frida.vala +++ b/src/frida.vala @@ -530,6 +530,7 @@ namespace Frida { private Gee.HashSet> pending_attach_requests = new Gee.HashSet> (); private Gee.HashMap> pending_detach_requests = new Gee.HashMap> (AgentSessionId.hash, AgentSessionId.equal); + private Gee.Set services = new Gee.HashSet (); private Bus _bus; internal Device (DeviceManager? manager, string id, string name, HostSessionProviderKind kind, HostSessionProvider provider, @@ -1228,7 +1229,7 @@ namespace Frida { public async IOStream open_channel (string address, Cancellable? cancellable = null) throws Error, IOError { check_open (); - var channel_provider = provider as ChannelProvider; + var channel_provider = provider as HostChannelProvider; if (channel_provider == null) throw new Error.NOT_SUPPORTED ("Channels are not supported by this device"); @@ -1249,6 +1250,38 @@ namespace Frida { } } + public async Service open_service (string address, Cancellable? cancellable = null) throws Error, IOError { + check_open (); + + var service_provider = provider as HostServiceProvider; + if (service_provider == null) + throw new Error.NOT_SUPPORTED ("Services are not supported by this device"); + + var service = yield service_provider.open_service (address, cancellable); + service.close.connect (on_service_closed); + services.add (service); + + return service; + } + + public Service open_service_sync (string address, Cancellable? cancellable = null) throws Error, IOError { + var task = create (); + task.address = address; + return task.execute (cancellable); + } + + private class OpenServiceTask : DeviceTask { + public string address; + + protected override async Service perform_operation () throws Error, IOError { + return yield parent.open_service (address, cancellable); + } + } + + private void on_service_closed (Service service) { + services.remove (service); + } + public async void unpair (Cancellable? cancellable = null) throws Error, IOError { check_open (); @@ -1357,6 +1390,10 @@ namespace Frida { close_request = new Promise (); try { + foreach (var service in services.to_array ()) + yield service.cancel (cancellable); + services.clear (); + while (!pending_detach_requests.is_empty) { var iterator = pending_detach_requests.entries.iterator (); iterator.next (); @@ -1964,6 +2001,68 @@ namespace Frida { } } + public interface Service : Object { + public signal void close (); + public signal void message (Variant message); + + public abstract bool is_closed (); + + public abstract async void activate (Cancellable? cancellable = null) throws Error, IOError; + + public void activate_sync (Cancellable? cancellable = null) throws Error, IOError { + create ().execute (cancellable); + } + + private class ActivateTask : ServiceTask { + protected override async void perform_operation () throws Error, IOError { + yield parent.activate (cancellable); + } + } + + public abstract async void cancel (Cancellable? cancellable = null) throws IOError; + + public void cancel_sync (Cancellable? cancellable = null) throws IOError { + try { + create ().execute (cancellable); + } catch (Error e) { + assert_not_reached (); + } + } + + private class CancelTask : ServiceTask { + protected override async void perform_operation () throws IOError { + yield parent.cancel (cancellable); + } + } + + public abstract async Variant request (Variant parameters, Cancellable? cancellable = null) throws Error, IOError; + + public Variant request_sync (Variant parameters, Cancellable? cancellable = null) throws Error, IOError { + var task = create () as RequestTask; + task.parameters = parameters; + return task.execute (cancellable); + } + + private class RequestTask : ServiceTask { + public Variant parameters; + + protected override async Variant perform_operation () throws Error, IOError { + return yield parent.request (parameters, cancellable); + } + } + + private T create () { + return Object.new (typeof (T), parent: this); + } + + private abstract class ServiceTask : AsyncTask { + public weak Service parent { + get; + construct; + } + } + } + public class Session : Object, AgentMessageSink { public signal void detached (SessionDetachReason reason, Crash? crash); diff --git a/src/fruity/dtx.vala b/src/fruity/dtx.vala index 538474943..4fc4ff921 100644 --- a/src/fruity/dtx.vala +++ b/src/fruity/dtx.vala @@ -1,18 +1,18 @@ [CCode (gir_namespace = "FridaFruity", gir_version = "1.0")] namespace Frida.Fruity { public class DeviceInfoService : Object, AsyncInitable { - public ChannelProvider channel_provider { + public HostChannelProvider channel_provider { get; construct; } private DTXChannel channel; - private DeviceInfoService (ChannelProvider channel_provider) { + private DeviceInfoService (HostChannelProvider channel_provider) { Object (channel_provider: channel_provider); } - public static async DeviceInfoService open (ChannelProvider channel_provider, Cancellable? cancellable = null) + public static async DeviceInfoService open (HostChannelProvider channel_provider, Cancellable? cancellable = null) throws Error, IOError { var service = new DeviceInfoService (channel_provider); @@ -74,21 +74,53 @@ namespace Frida.Fruity { return "/private" + name; return name; } + + public class ProcessInfo : Object { + public uint pid { + get; + set; + } + + public string name { + get; + set; + } + + public string real_app_name { + get; + set; + } + + public bool is_application { + get; + set; + } + + public bool foreground_running { + get; + set; + } + + public DateTime? start_date { + get; + set; + } + } } public class ApplicationListingService : Object, AsyncInitable { - public ChannelProvider channel_provider { + public HostChannelProvider channel_provider { get; construct; } private DTXChannel channel; - private ApplicationListingService (ChannelProvider channel_provider) { + private ApplicationListingService (HostChannelProvider channel_provider) { Object (channel_provider: channel_provider); } - public static async ApplicationListingService open (ChannelProvider channel_provider, Cancellable? cancellable = null) + public static async ApplicationListingService open (HostChannelProvider channel_provider, Cancellable? cancellable = null) throws Error, IOError { var service = new ApplicationListingService (channel_provider); @@ -178,171 +210,139 @@ namespace Frida.Fruity { return result; } - } - - public class ProcessControlService : Object, AsyncInitable { - public ChannelProvider channel_provider { - get; - construct; - } - private DTXChannel channel; - - private ProcessControlService (ChannelProvider channel_provider) { - Object (channel_provider: channel_provider); - } - - public static async ProcessControlService open (ChannelProvider channel_provider, Cancellable? cancellable = null) - throws Error, IOError { - var service = new ProcessControlService (channel_provider); + public class ApplicationInfo : Object { + public ApplicationType app_type { + get; + set; + } - try { - yield service.init_async (Priority.DEFAULT, cancellable); - } catch (GLib.Error e) { - throw_api_error (e); + public string display_name { + get; + set; } - return service; - } + public string bundle_identifier { + get; + set; + } - private async bool init_async (int io_priority, Cancellable? cancellable) throws Error, IOError { - var connection = yield DTXConnection.obtain (channel_provider, cancellable); + public string bundle_path { + get; + set; + } - channel = connection.make_channel ("com.apple.instruments.server.services.processcontrol"); + public string? version { + get; + set; + } - return true; - } + public bool placeholder { + get; + set; + } - public async void kill (uint pid, Cancellable? cancellable = null) throws Error, IOError { - var args = new DTXArgumentListBuilder () - .append_object (new NSNumber.from_integer (pid)); - yield channel.invoke ("killPid:", args, cancellable); - } - } + public bool restricted { + get; + set; + } - public class ProcessInfo : Object { - public uint pid { - get; - set; - } + public string? executable_name { + get; + set; + } - public string name { - get; - set; - } + public string[]? app_extension_uuids { + get; + set; + } - public string real_app_name { - get; - set; - } + public string? plugin_uuid { + get; + set; + } - public bool is_application { - get; - set; - } + public string? plugin_identifier { + get; + set; + } - public bool foreground_running { - get; - set; - } + public string? container_bundle_identifier { + get; + set; + } - public DateTime? start_date { - get; - set; + public string? container_bundle_path { + get; + set; + } } - } - public class ApplicationInfo : Object { - public ApplicationType app_type { - get; - set; - } + public enum ApplicationType { + SYSTEM = 1, + USER, + PLUGIN_KIT; - public string display_name { - get; - set; - } + public static ApplicationType from_nick (string nick) throws Error { + return Marshal.enum_from_nick (nick); + } - public string bundle_identifier { - get; - set; - } + public string to_nick () { + return Marshal.enum_to_nick (this); + } - public string bundle_path { - get; - set; - } + internal static ApplicationType from_dtx (string type) { + if (type == "System") + return SYSTEM; - public string? version { - get; - set; - } + if (type == "User") + return USER; - public bool placeholder { - get; - set; - } + if (type == "PluginKit") + return PLUGIN_KIT; - public bool restricted { - get; - set; + assert_not_reached (); + } } + } - public string? executable_name { + public class ProcessControlService : Object, AsyncInitable { + public HostChannelProvider channel_provider { get; - set; + construct; } - public string[]? app_extension_uuids { - get; - set; - } + private DTXChannel channel; - public string? plugin_uuid { - get; - set; + private ProcessControlService (HostChannelProvider channel_provider) { + Object (channel_provider: channel_provider); } - public string? plugin_identifier { - get; - set; - } + public static async ProcessControlService open (HostChannelProvider channel_provider, Cancellable? cancellable = null) + throws Error, IOError { + var service = new ProcessControlService (channel_provider); - public string? container_bundle_identifier { - get; - set; - } + try { + yield service.init_async (Priority.DEFAULT, cancellable); + } catch (GLib.Error e) { + throw_api_error (e); + } - public string? container_bundle_path { - get; - set; + return service; } - } - public enum ApplicationType { - SYSTEM = 1, - USER, - PLUGIN_KIT; + private async bool init_async (int io_priority, Cancellable? cancellable) throws Error, IOError { + var connection = yield DTXConnection.obtain (channel_provider, cancellable); - public static ApplicationType from_nick (string nick) throws Error { - return Marshal.enum_from_nick (nick); - } + channel = connection.make_channel ("com.apple.instruments.server.services.processcontrol"); - public string to_nick () { - return Marshal.enum_to_nick (this); + return true; } - internal static ApplicationType from_dtx (string type) { - if (type == "System") - return SYSTEM; - - if (type == "User") - return USER; - - if (type == "PluginKit") - return PLUGIN_KIT; - - assert_not_reached (); + public async void kill (uint pid, Cancellable? cancellable = null) throws Error, IOError { + var args = new DTXArgumentListBuilder () + .append_object (new NSNumber.from_integer (pid)); + yield channel.invoke ("killPid:", args, cancellable); } } @@ -363,7 +363,7 @@ namespace Frida.Fruity { CLOSED } - private static Gee.HashMap> connections; + private static Gee.HashMap> connections; private State _state = OPEN; @@ -385,17 +385,19 @@ namespace Frida.Fruity { private const size_t MAX_BUFFERED_SIZE = 30 * 1024 * 1024; private const size_t MAX_MESSAGE_SIZE = 128 * 1024 * 1024; private const size_t MAX_FRAGMENT_SIZE = 128 * 1024; - private const string REMOTESERVER_ENDPOINT_MODERN = "lockdown:com.apple.instruments.remoteserver.DVTSecureSocketProxy"; + private const string REMOTESERVER_ENDPOINT_17PLUS = "lockdown:com.apple.instruments.dtservicehub"; + private const string REMOTESERVER_ENDPOINT_14PLUS = "lockdown:com.apple.instruments.remoteserver.DVTSecureSocketProxy"; private const string REMOTESERVER_ENDPOINT_LEGACY = "lockdown:com.apple.instruments.remoteserver?tls=handshake-only"; private const string[] REMOTESERVER_ENDPOINT_CANDIDATES = { - REMOTESERVER_ENDPOINT_MODERN, + REMOTESERVER_ENDPOINT_17PLUS, + REMOTESERVER_ENDPOINT_14PLUS, REMOTESERVER_ENDPOINT_LEGACY, }; - public static async DTXConnection obtain (ChannelProvider channel_provider, Cancellable? cancellable) + public static async DTXConnection obtain (HostChannelProvider channel_provider, Cancellable? cancellable) throws Error, IOError { if (connections == null) - connections = new Gee.HashMap> (); + connections = new Gee.HashMap> (); while (connections.has_key (channel_provider)) { var future = connections[channel_provider]; @@ -442,7 +444,7 @@ namespace Frida.Fruity { throw api_error; } - public static async void close_all (ChannelProvider channel_provider, Cancellable? cancellable) throws IOError { + public static async void close_all (HostChannelProvider channel_provider, Cancellable? cancellable) throws IOError { if (connections == null) return; diff --git a/src/fruity/fruity-host-session.vala b/src/fruity/fruity-host-session.vala index 5e7ea6dff..d6cf84777 100644 --- a/src/fruity/fruity-host-session.vala +++ b/src/fruity/fruity-host-session.vala @@ -37,7 +37,6 @@ namespace Frida { private async void do_start () { bool success = yield try_start_control_connection (); - if (success) { /* Perform a dummy-request to flush out any pending device attach notifications. */ try { @@ -207,7 +206,8 @@ namespace Frida { out Variant? icon) throws Error; } - public class FruityHostSessionProvider : Object, HostSessionProvider, ChannelProvider, FruityLockdownProvider, Pairable { + public class FruityHostSessionProvider : Object, HostSessionProvider, HostChannelProvider, HostServiceProvider, + FruityLockdownProvider, Pairable { public string id { get { return device_details.udid.raw_value; } } @@ -245,6 +245,7 @@ namespace Frida { private FruityHostSession? host_session; private Promise? lockdown_client_request; + private Promise? tunnel_request; private Timer? lockdown_client_timer; private const double MAX_LOCKDOWN_CLIENT_AGE = 30.0; @@ -260,6 +261,17 @@ namespace Frida { public async void close (Cancellable? cancellable) throws IOError { yield Fruity.DTXConnection.close_all (this, cancellable); + if (tunnel_request != null) { + Fruity.Tunnel? tunnel = null; + try { + tunnel = yield find_tunnel (cancellable); + } catch (Error e) { + } + + if (tunnel != null) + yield tunnel.close (cancellable); + } + if (lockdown_client_request != null) { Fruity.LockdownClient? lockdown = null; try { @@ -306,7 +318,7 @@ namespace Frida { agent_session_detached (id, reason, crash); } - public async IOStream open_channel (string address, Cancellable? cancellable = null) throws Error, IOError { + public async IOStream open_channel (string address, Cancellable? cancellable) throws Error, IOError { if (address.has_prefix ("tcp:")) { ulong raw_port; if (!ulong.try_parse (address.substring (4), out raw_port) || raw_port == 0 || raw_port > uint16.MAX) @@ -360,14 +372,7 @@ namespace Frida { if (service_name.length != 0) { var client = yield get_lockdown_client (cancellable); - - try { - return yield client.start_service (service_name, cancellable); - } catch (GLib.Error e) { - if (e is Fruity.LockdownError.INVALID_SERVICE) - throw new Error.NOT_SUPPORTED ("%s", e.message); - throw new Error.TRANSPORT ("%s", e.message); - } + return yield open_lockdown_service (service_name, client, cancellable); } else { try { var client = yield Fruity.LockdownClient.open (device_details, cancellable); @@ -382,6 +387,63 @@ namespace Frida { throw new Error.NOT_SUPPORTED ("Unsupported channel address"); } + public async IOStream open_lockdown_service (string service_name, Fruity.LockdownClient client, Cancellable? cancellable) + throws Error, IOError { + var tunnel = yield find_tunnel (cancellable); + if (tunnel != null) { + Fruity.ServiceInfo? service_info = null; + try { + service_info = tunnel.discovery.get_service (service_name); + } catch (Error e) { + if (!(e is Error.NOT_SUPPORTED)) + throw e; + } + if (service_info == null) + service_info = tunnel.discovery.get_service (service_name + ".shim.remote"); + return yield tunnel.open_tcp_connection (service_info.port, cancellable); + } + + try { + return yield client.start_service (service_name, cancellable); + } catch (GLib.Error e) { + if (e is Fruity.LockdownError.INVALID_SERVICE) + throw new Error.NOT_SUPPORTED ("%s", e.message); + throw new Error.TRANSPORT ("%s", e.message); + } + } + + public async Service open_service (string address, Cancellable? cancellable) throws Error, IOError { + string[] tokens = address.split (":", 2); + unowned string protocol = tokens[0]; + unowned string service_name = tokens[1]; + + if (protocol == "plist") { + var client = yield get_lockdown_client (cancellable); + var stream = yield open_lockdown_service (service_name, client, cancellable); + + return new PlistService (stream); + } + + if (protocol == "dtx") { + var connection = yield Fruity.DTXConnection.obtain (this, cancellable); + + return new DTXService (service_name, connection); + } + + if (protocol == "xpc") { + var tunnel = yield find_tunnel (cancellable); + if (tunnel == null) + throw new Error.NOT_SUPPORTED ("RemoteXPC not supported by device"); + + var service_info = tunnel.discovery.get_service (service_name); + var stream = yield tunnel.open_tcp_connection (service_info.port, cancellable); + + return new XpcService (new Fruity.XpcConnection (stream)); + } + + throw new Error.NOT_SUPPORTED ("Unsupported service address"); + } + private async Fruity.LockdownClient get_lockdown_client (Cancellable? cancellable) throws Error, IOError { if (lockdown_client_timer != null) { if (lockdown_client_timer.elapsed () > MAX_LOCKDOWN_CLIENT_AGE) @@ -427,6 +489,45 @@ namespace Frida { lockdown_client_timer = null; } + private async Fruity.Tunnel? find_tunnel (Cancellable? cancellable) throws Error, IOError { + while (tunnel_request != null) { + try { + return yield tunnel_request.future.wait_async (cancellable); + } catch (Error e) { + throw e; + } catch (IOError e) { + cancellable.set_error_if_cancelled (); + } + } + tunnel_request = new Promise (); + + try { + bool supported_by_os = true; + var lockdown = yield get_lockdown_client (cancellable); + var response = yield lockdown.get_value (null, null, cancellable); + Fruity.PlistDict properties = response.get_dict ("Value"); + if (properties.get_string ("ProductName") == "iPhone OS") { + uint ios_major_version = uint.parse (properties.get_string ("ProductVersion").split (".")[0]); + supported_by_os = ios_major_version >= 17; + } + + Fruity.Tunnel? tunnel = null; + if (supported_by_os) + tunnel = yield Fruity.TunnelFinder.make_default ().find (device_details.udid.raw_value, cancellable); + + tunnel_request.resolve (tunnel); + + return tunnel; + } catch (GLib.Error e) { + var api_error = new Error.NOT_SUPPORTED ("%s", e.message); + + tunnel_request.reject (api_error); + tunnel_request = null; + + throw_api_error (api_error); + } + } + private async void unpair (Cancellable? cancellable) throws Error, IOError { try { var client = yield Fruity.LockdownClient.open (device_details, cancellable); @@ -441,10 +542,12 @@ namespace Frida { public interface FruityLockdownProvider : Object { public abstract async Fruity.LockdownClient get_lockdown_client (Cancellable? cancellable) throws Error, IOError; + public abstract async IOStream open_lockdown_service (string service_name, Fruity.LockdownClient client, + Cancellable? cancellable) throws Error, IOError; } public class FruityHostSession : Object, HostSession { - public weak ChannelProvider channel_provider { + public weak HostChannelProvider channel_provider { get; construct; } @@ -472,14 +575,16 @@ namespace Frida { private const double MIN_SERVER_CHECK_INTERVAL = 5.0; private const string GADGET_APP_ID = "re.frida.Gadget"; - private const string DEBUGSERVER_ENDPOINT_MODERN = "com.apple.debugserver.DVTSecureSocketProxy"; + private const string DEBUGSERVER_ENDPOINT_17PLUS = "com.apple.internal.dt.remote.debugproxy"; + private const string DEBUGSERVER_ENDPOINT_14PLUS = "com.apple.debugserver.DVTSecureSocketProxy"; private const string DEBUGSERVER_ENDPOINT_LEGACY = "com.apple.debugserver?tls=handshake-only"; private const string[] DEBUGSERVER_ENDPOINT_CANDIDATES = { - DEBUGSERVER_ENDPOINT_MODERN, + DEBUGSERVER_ENDPOINT_17PLUS, + DEBUGSERVER_ENDPOINT_14PLUS, DEBUGSERVER_ENDPOINT_LEGACY, }; - public FruityHostSession (ChannelProvider channel_provider, FruityLockdownProvider lockdown_provider) { + public FruityHostSession (HostChannelProvider channel_provider, FruityLockdownProvider lockdown_provider) { Object ( channel_provider: channel_provider, lockdown_provider: lockdown_provider @@ -599,15 +704,16 @@ namespace Frida { var opts = FrontmostQueryOptions._deserialize (options); var scope = opts.scope; - var processes_request = new Promise> (); + var processes_request = new Promise> (); var apps_request = new Promise> (); fetch_processes.begin (processes_request, cancellable); fetch_apps.begin (apps_request, cancellable); - Gee.List processes = yield processes_request.future.wait_async (cancellable); - Fruity.ProcessInfo? process = null; + Gee.List processes = + yield processes_request.future.wait_async (cancellable); + Fruity.DeviceInfoService.ProcessInfo? process = null; string? app_path = null; - foreach (Fruity.ProcessInfo candidate in processes) { + foreach (Fruity.DeviceInfoService.ProcessInfo candidate in processes) { if (!candidate.foreground_running) continue; @@ -671,7 +777,7 @@ namespace Frida { var scope = opts.scope; var apps_request = new Promise> (); - var processes_request = new Promise> (); + var processes_request = new Promise> (); fetch_apps.begin (apps_request, cancellable); fetch_processes.begin (processes_request, cancellable); @@ -694,9 +800,10 @@ namespace Frida { } } - Gee.List processes = yield processes_request.future.wait_async (cancellable); - var process_by_app_path = new Gee.HashMap (); - foreach (Fruity.ProcessInfo process in processes) { + Gee.List processes = + yield processes_request.future.wait_async (cancellable); + var process_by_app_path = new Gee.HashMap (); + foreach (Fruity.DeviceInfoService.ProcessInfo process in processes) { bool is_main_process; string app_path = compute_app_path_from_executable_path (process.real_app_name, out is_main_process); if (is_main_process) @@ -707,7 +814,7 @@ namespace Frida { foreach (Fruity.ApplicationDetails app in apps) { unowned string identifier = app.identifier; - Fruity.ProcessInfo? process = process_by_app_path[app.path]; + Fruity.DeviceInfoService.ProcessInfo? process = process_by_app_path[app.path]; var info = HostApplicationInfo (identifier, app.name, (process != null) ? process.pid : 0, make_parameters_dict ()); @@ -753,12 +860,13 @@ namespace Frida { var opts = ProcessQueryOptions._deserialize (options); var scope = opts.scope; - var processes_request = new Promise> (); + var processes_request = new Promise> (); var apps_request = new Promise> (); fetch_processes.begin (processes_request, cancellable); fetch_apps.begin (apps_request, cancellable); - Gee.List processes = yield processes_request.future.wait_async (cancellable); + Gee.List processes = + yield processes_request.future.wait_async (cancellable); processes = maybe_filter_processes (processes, opts); Gee.List apps = yield apps_request.future.wait_async (cancellable); @@ -770,7 +878,7 @@ namespace Frida { var app_pids = new Gee.ArrayList (); var app_by_main_pid = new Gee.HashMap (); var app_by_related_pid = new Gee.HashMap (); - foreach (Fruity.ProcessInfo process in processes) { + foreach (Fruity.DeviceInfoService.ProcessInfo process in processes) { unowned string executable_path = process.real_app_name; bool is_main_process; @@ -812,7 +920,7 @@ namespace Frida { var result = new HostProcessInfo[0]; - foreach (Fruity.ProcessInfo process in processes) { + foreach (Fruity.DeviceInfoService.ProcessInfo process in processes) { uint pid = process.pid; if (pid == 0) continue; @@ -876,7 +984,8 @@ namespace Frida { } } - private async void fetch_processes (Promise> promise, Cancellable? cancellable) { + private async void fetch_processes (Promise> promise, + Cancellable? cancellable) { try { var device_info = yield Fruity.DeviceInfoService.open (channel_provider, cancellable); @@ -909,18 +1018,18 @@ namespace Frida { return filtered_apps; } - private Gee.List maybe_filter_processes (Gee.List processes, - ProcessQueryOptions options) { + private Gee.List maybe_filter_processes ( + Gee.List processes, ProcessQueryOptions options) { if (!options.has_selected_pids ()) return processes; - var process_by_pid = new Gee.HashMap (); - foreach (Fruity.ProcessInfo process in processes) + var process_by_pid = new Gee.HashMap (); + foreach (Fruity.DeviceInfoService.ProcessInfo process in processes) process_by_pid[process.pid] = process; - var filtered_processes = new Gee.ArrayList (); + var filtered_processes = new Gee.ArrayList (); options.enumerate_selected_pids (pid => { - Fruity.ProcessInfo? process = process_by_pid[pid]; + Fruity.DeviceInfoService.ProcessInfo? process = process_by_pid[pid]; if (process != null) filtered_processes.add (process); }); @@ -967,7 +1076,7 @@ namespace Frida { parameters["debuggable"] = true; } - private void add_app_state (HashTable parameters, Fruity.ProcessInfo process) { + private void add_app_state (HashTable parameters, Fruity.DeviceInfoService.ProcessInfo process) { if (process.foreground_running) parameters["frontmost"] = true; } @@ -983,7 +1092,7 @@ namespace Frida { parameters["icons"] = icons.end (); } - private void add_process_metadata (HashTable parameters, Fruity.ProcessInfo? process) { + private void add_process_metadata (HashTable parameters, Fruity.DeviceInfoService.ProcessInfo? process) { DateTime? started = process.start_date; if (started != null) parameters["started"] = started.format_iso8601 (); @@ -1210,10 +1319,10 @@ namespace Frida { throws Error, IOError { foreach (unowned string endpoint in DEBUGSERVER_ENDPOINT_CANDIDATES) { try { - var lldb_stream = yield lockdown.start_service (endpoint, cancellable); + var lldb_stream = yield lockdown_provider.open_lockdown_service (endpoint, lockdown, cancellable); return yield LLDB.Client.open (lldb_stream, cancellable); - } catch (Fruity.LockdownError e) { - if (!(e is Fruity.LockdownError.INVALID_SERVICE)) + } catch (Error e) { + if (!(e is Error.NOT_SUPPORTED)) throw new Error.NOT_SUPPORTED ("%s", e.message); } } @@ -1556,14 +1665,15 @@ namespace Frida { construct; } - public weak ChannelProvider channel_provider { + public weak HostChannelProvider channel_provider { get; construct; } private Promise? gadget_request; - public LLDBSession (LLDB.Client lldb, LLDB.Process process, string? gadget_path, ChannelProvider channel_provider) { + public LLDBSession (LLDB.Client lldb, LLDB.Process process, string? gadget_path, + HostChannelProvider channel_provider) { Object ( lldb: lldb, process: process, @@ -1791,4 +1901,610 @@ namespace Frida { } } } + + private sealed class PlistService : Object, Service { + public IOStream stream { + get; + construct; + } + + private State state = INACTIVE; + private Fruity.PlistServiceClient client; + private bool client_closed = false; + + private enum State { + INACTIVE, + ACTIVE, + } + + public PlistService (IOStream stream) { + Object (stream: stream); + } + + construct { + client = new Fruity.PlistServiceClient (stream); + client.closed.connect (on_client_closed); + } + + public bool is_closed () { + return client_closed; + } + + public async void activate (Cancellable? cancellable) throws Error, IOError { + ensure_active (); + } + + private void ensure_active () throws Error { + if (state == INACTIVE) { + state = ACTIVE; + + if (client_closed) { + close (); + throw new Error.INVALID_OPERATION ("Service is closed"); + } + } + } + + public async void cancel (Cancellable? cancellable) throws IOError { + if (client_closed) + return; + client_closed = true; + + yield client.close (cancellable); + } + + public async Variant request (Variant parameters, Cancellable? cancellable = null) throws Error, IOError { + ensure_active (); + + var reader = new VariantReader (parameters); + + string type = reader.read_member ("type").get_string_value (); + reader.end_member (); + + try { + if (type == "query") { + reader.read_member ("payload"); + var payload = plist_from_variant (reader.current_object); + var raw_response = yield client.query (payload, cancellable); + return plist_to_variant (raw_response); + } else if (type == "read") { + var plist = yield client.read_message (cancellable); + return plist_to_variant (plist); + } else { + throw new Error.INVALID_ARGUMENT ("Unsupported request type: %s", type); + } + } catch (Fruity.PlistServiceError e) { + if (e is Fruity.PlistServiceError.CONNECTION_CLOSED) + throw new Error.TRANSPORT ("Connection closed during request"); + throw new Error.PROTOCOL ("%s", e.message); + } + } + + private void on_client_closed () { + client_closed = true; + close (); + } + + private Fruity.Plist plist_from_variant (Variant val) throws Error { + if (!val.is_of_type (VariantType.VARDICT)) + throw new Error.INVALID_ARGUMENT ("Expected a dictionary"); + + var plist = new Fruity.Plist (); + + foreach (var item in val) { + string k; + Variant v; + item.get ("{sv}", out k, out v); + + plist.set_value (k, plist_value_from_variant (v)); + } + + return plist; + } + + private Value plist_value_from_variant (Variant val) throws Error { + switch (val.classify ()) { + case BOOLEAN: + return val.get_boolean (); + case INT64: + return val.get_int64 (); + case DOUBLE: + return val.get_double (); + case STRING: + return val.get_string (); + case ARRAY: + if (val.is_of_type (new VariantType ("ay"))) + return val.get_data_as_bytes (); + + if (val.is_of_type (VariantType.VARDICT)) { + var dict = new Fruity.PlistDict (); + + foreach (var item in val) { + string k; + Variant v; + item.get ("{sv}", out k, out v); + + dict.set_value (k, plist_value_from_variant (v)); + } + + return dict; + } + + if (val.is_of_type (new VariantType ("av"))) { + var arr = new Fruity.PlistArray (); + + foreach (var item in val) { + Variant v; + item.get ("v", out v); + + arr.add_value (plist_value_from_variant (v)); + } + + return arr; + } + + break; + default: + break; + } + + throw new Error.INVALID_ARGUMENT ("Unsupported type: %s", (string) val.get_type ().peek_string ()); + } + + private Variant plist_to_variant (Fruity.Plist plist) { + return plist_dict_to_variant (plist); + } + + private Variant plist_dict_to_variant (Fruity.PlistDict dict) { + var builder = new VariantBuilder (VariantType.VARDICT); + foreach (var e in dict.entries) + builder.add ("{sv}", e.key, plist_value_to_variant (e.value)); + return builder.end (); + } + + private Variant plist_array_to_variant (Fruity.PlistArray arr) { + var builder = new VariantBuilder (new VariantType.array (VariantType.VARIANT)); + foreach (var e in arr.elements) + builder.add ("v", plist_value_to_variant (e)); + return builder.end (); + } + + private Variant plist_value_to_variant (Value * v) { + Type t = v.type (); + + if (t == typeof (bool)) + return v.get_boolean (); + + if (t == typeof (int64)) + return v.get_int64 (); + + if (t == typeof (float)) + return (double) v.get_float (); + + if (t == typeof (double)) + return v.get_double (); + + if (t == typeof (string)) + return v.get_string (); + + if (t == typeof (Bytes)) { + var bytes = (Bytes) v.get_boxed (); + return Variant.new_from_data (new VariantType.array (VariantType.BYTE), bytes.get_data (), true, bytes); + } + + if (t == typeof (Fruity.PlistDict)) + return plist_dict_to_variant ((Fruity.PlistDict) v.get_object ()); + + if (t == typeof (Fruity.PlistArray)) + return plist_array_to_variant ((Fruity.PlistArray) v.get_object ()); + + if (t == typeof (Fruity.PlistUid)) + return ((Fruity.PlistUid) v.get_object ()).uid; + + assert_not_reached (); + } + } + + private sealed class DTXService : Object, Service { + public string identifier { + get; + construct; + } + + public Fruity.DTXConnection connection { + get; + construct; + } + + private State state = INACTIVE; + private bool connection_closed = false; + private Fruity.DTXChannel? channel; + + private enum State { + INACTIVE, + ACTIVE, + } + + public DTXService (string identifier, Fruity.DTXConnection connection) { + Object (identifier: identifier, connection: connection); + } + + construct { + connection.notify["state"].connect (on_connection_state_changed); + } + + public bool is_closed () { + return connection_closed; + } + + public async void activate (Cancellable? cancellable) throws Error, IOError { + ensure_active (); + } + + private void ensure_active () throws Error { + if (state == INACTIVE) { + state = ACTIVE; + + if (connection_closed) { + close (); + throw new Error.INVALID_OPERATION ("Service is closed"); + } + + channel = connection.make_channel (identifier); + channel.invocation.connect (on_channel_invocation); + channel.notification.connect (on_channel_notification); + } + } + + private void ensure_closed () { + if (connection_closed) + return; + connection_closed = true; + channel = null; + close (); + } + + public async void cancel (Cancellable? cancellable) throws IOError { + ensure_closed (); + } + + public async Variant request (Variant parameters, Cancellable? cancellable = null) throws Error, IOError { + ensure_active (); + + var reader = new VariantReader (parameters); + + string method_name = reader.read_member ("method").get_string_value (); + reader.end_member (); + + Fruity.DTXArgumentListBuilder? args = null; + if (reader.has_member ("args")) { + reader.read_member ("args"); + args = new Fruity.DTXArgumentListBuilder (); + uint n = reader.count_elements (); + for (uint i = 0; i != n; i++) { + reader.read_element (i); + args.append_object (nsobject_from_variant (reader.current_object)); + reader.end_element (); + } + } + + var result = yield channel.invoke (method_name, args, cancellable); + + return nsobject_to_variant (result); + } + + private void on_connection_state_changed (Object obj, ParamSpec pspec) { + if (connection.state == CLOSED) + ensure_closed (); + } + + private void on_channel_invocation (string method_name, Fruity.DTXArgumentList args, + Fruity.DTXMessageTransportFlags transport_flags) { + var envelope = new HashTable (str_hash, str_equal); + envelope["type"] = "invocation"; + envelope["payload"] = invocation_to_variant (method_name, args); + envelope["expects-reply"] = (transport_flags & Fruity.DTXMessageTransportFlags.EXPECTS_REPLY) != 0; + message (envelope); + } + + private void on_channel_notification (Fruity.NSObject obj) { + var envelope = new HashTable (str_hash, str_equal); + envelope["type"] = "notification"; + envelope["payload"] = nsobject_to_variant (obj); + message (envelope); + } + + private Fruity.NSObject? nsobject_from_variant (Variant val) throws Error { + switch (val.classify ()) { + case BOOLEAN: + return new Fruity.NSNumber.from_boolean (val.get_boolean ()); + case INT64: + return new Fruity.NSNumber.from_integer (val.get_int64 ()); + case DOUBLE: + return new Fruity.NSNumber.from_double (val.get_double ()); + case STRING: + return new Fruity.NSString (val.get_string ()); + case ARRAY: + if (val.is_of_type (new VariantType ("ay"))) + return new Fruity.NSData (val.get_data_as_bytes ()); + + if (val.is_of_type (VariantType.VARDICT)) { + var dict = new Fruity.NSDictionary (); + + foreach (var item in val) { + string k; + Variant v; + item.get ("{sv}", out k, out v); + + dict.set_value (k, nsobject_from_variant (v)); + } + + return dict; + } + + if (val.is_of_type (new VariantType ("av"))) { + var arr = new Fruity.NSArray (); + + foreach (var item in val) { + Variant v; + item.get ("v", out v); + + arr.add_object (nsobject_from_variant (v)); + } + + return arr; + } + + break; + default: + break; + } + + throw new Error.INVALID_ARGUMENT ("Unsupported type: %s", (string) val.get_type ().peek_string ()); + } + + private Variant nsobject_to_variant (Fruity.NSObject? obj) { + if (obj == null) + return new Variant ("()"); + + var num = obj as Fruity.NSNumber; + if (num != null) + return num.integer; + + var str = obj as Fruity.NSString; + if (str != null) + return str.str; + + var data = obj as Fruity.NSData; + if (data != null) { + Bytes bytes = data.bytes; + return Variant.new_from_data (new VariantType.array (VariantType.BYTE), bytes.get_data (), true, bytes); + } + + var dict = obj as Fruity.NSDictionary; + if (dict != null) + return nsdictionary_to_variant (dict); + + var dict_raw = obj as Fruity.NSDictionaryRaw; + if (dict_raw != null) + return nsdictionary_raw_to_variant (dict_raw); + + var arr = obj as Fruity.NSArray; + if (arr != null) + return nsarray_to_variant (arr); + + var date = obj as Fruity.NSDate; + if (date != null) + return date.to_date_time ().format_iso8601 (); + + var err = obj as Fruity.NSError; + if (err != null) + return nserror_to_variant (err); + + var msg = obj as Fruity.DTTapMessage; + if (msg != null) + return nsdictionary_to_variant (msg.plist); + + assert_not_reached (); + } + + private Variant nsdictionary_to_variant (Fruity.NSDictionary dict) { + var builder = new VariantBuilder (VariantType.VARDICT); + foreach (var e in dict.entries) + builder.add ("{sv}", e.key, nsobject_to_variant (e.value)); + return builder.end (); + } + + private Variant nsdictionary_raw_to_variant (Fruity.NSDictionaryRaw dict) { + var builder = new VariantBuilder ( + new VariantType.array (new VariantType.dict_entry (VariantType.VARIANT, VariantType.VARIANT))); + foreach (var e in dict.entries) + builder.add ("{vv}", nsobject_to_variant (e.key), nsobject_to_variant (e.value)); + return builder.end (); + } + + private Variant nsarray_to_variant (Fruity.NSArray arr) { + var builder = new VariantBuilder (new VariantType.array (VariantType.VARIANT)); + foreach (var e in arr.elements) + builder.add ("v", nsobject_to_variant (e)); + return builder.end (); + } + + private Variant nserror_to_variant (Fruity.NSError e) { + var result = new HashTable (str_hash, str_equal); + result["domain"] = e.domain.str; + result["code"] = e.code; + result["user-info"] = nsdictionary_to_variant (e.user_info); + return result; + } + + private Variant invocation_to_variant (string method_name, Fruity.DTXArgumentList args) { + var invocation = new HashTable (str_hash, str_equal); + invocation["method"] = method_name; + invocation["args"] = invocation_args_to_variant (args); + return invocation; + } + + private Variant invocation_args_to_variant (Fruity.DTXArgumentList args) { + var builder = new VariantBuilder (new VariantType.array (VariantType.VARIANT)); + foreach (var e in args.elements) + builder.add ("v", value_to_variant (e)); + return builder.end (); + } + + private Variant value_to_variant (Value v) { + Type t = v.type (); + + if (t == typeof (int)) + return v.get_int (); + + if (t == typeof (int64)) + return v.get_int64 (); + + if (t == typeof (double)) + return v.get_double (); + + if (t == typeof (string)) + return v.get_string (); + + if (t.is_a (typeof (Fruity.NSObject))) + return nsobject_to_variant ((Fruity.NSObject) v.get_boxed ()); + + assert_not_reached (); + } + } + + private sealed class XpcService : Object, Service { + public Fruity.XpcConnection connection { + get; + construct; + } + + private State state = INACTIVE; + private bool connection_closed = false; + + private enum State { + INACTIVE, + ACTIVE, + } + + public XpcService (Fruity.XpcConnection connection) { + Object (connection: connection); + } + + construct { + connection.close.connect (on_close); + connection.message.connect (on_message); + } + + public bool is_closed () { + return connection_closed; + } + + public async void activate (Cancellable? cancellable) throws Error, IOError { + ensure_active (); + } + + private void ensure_active () throws Error { + if (state == INACTIVE) { + state = ACTIVE; + + if (connection_closed) { + close (); + throw new Error.INVALID_OPERATION ("Service is closed"); + } + + connection.activate (); + } + } + + public async void cancel (Cancellable? cancellable) throws IOError { + connection.cancel (); + } + + public async Variant request (Variant parameters, Cancellable? cancellable = null) throws Error, IOError { + ensure_active (); + + yield connection.wait_until_ready (cancellable); + + if (!parameters.is_of_type (VariantType.VARDICT)) + throw new Error.INVALID_ARGUMENT ("Expected a dictionary"); + + var builder = new Fruity.XpcBodyBuilder (); + builder.begin_dictionary (); + add_vardict_values (parameters, builder); + Fruity.TrustedService.add_standard_request_values (builder); + builder.end_dictionary (); + + Fruity.XpcMessage response = yield connection.request (builder.build (), cancellable); + + return response.body; + } + + private void on_close (Error? error) { + connection_closed = true; + close (); + } + + private void on_message (Fruity.XpcMessage msg) { + message (msg.body); + } + + private static void add_vardict_values (Variant dict, Fruity.XpcBodyBuilder builder) throws Error { + foreach (var item in dict) { + string key; + Variant val; + item.get ("{sv}", out key, out val); + + builder.set_member_name (key); + add_variant_value (val, builder); + } + } + + private static void add_vararray_values (Variant arr, Fruity.XpcBodyBuilder builder) throws Error { + foreach (var item in arr) { + Variant val; + item.get ("v", out val); + + add_variant_value (val, builder); + } + } + + private static void add_variant_value (Variant val, Fruity.XpcBodyBuilder builder) throws Error { + switch (val.classify ()) { + case BOOLEAN: + builder.add_bool_value (val.get_boolean ()); + return; + case INT64: + builder.add_int64_value (val.get_int64 ()); + return; + case STRING: + builder.add_string_value (val.get_string ()); + return; + case ARRAY: + if (val.is_of_type (new VariantType ("ay"))) { + builder.add_data_value (val.get_data_as_bytes ()); + return; + } + + if (val.is_of_type (VariantType.VARDICT)) { + builder.begin_dictionary (); + add_vardict_values (val, builder); + builder.end_dictionary (); + return; + } + + if (val.is_of_type (new VariantType ("av"))) { + add_vararray_values (val, builder); + return; + } + + break; + default: + break; + } + + throw new Error.INVALID_ARGUMENT ("Unsupported type: %s", (string) val.get_type ().peek_string ()); + } + } } diff --git a/src/fruity/injector.vala b/src/fruity/injector.vala index 6aa7113a8..4de64df81 100644 --- a/src/fruity/injector.vala +++ b/src/fruity/injector.vala @@ -1,6 +1,6 @@ [CCode (gir_namespace = "FridaFruityInjector", gir_version = "1.0")] namespace Frida.Fruity.Injector { - public static async GadgetDetails inject (owned Gum.DarwinModule module, LLDB.Client lldb, ChannelProvider channel_provider, + public static async GadgetDetails inject (owned Gum.DarwinModule module, LLDB.Client lldb, HostChannelProvider channel_provider, Cancellable? cancellable) throws Error, IOError { var session = new Session (module, lldb, channel_provider); return yield session.run (cancellable); @@ -32,7 +32,7 @@ namespace Frida.Fruity.Injector { construct; } - public ChannelProvider channel_provider { + public HostChannelProvider channel_provider { get; construct; } @@ -70,7 +70,7 @@ namespace Frida.Fruity.Injector { private size_t module_size; - public Session (Gum.DarwinModule module, LLDB.Client lldb, ChannelProvider channel_provider) { + public Session (Gum.DarwinModule module, LLDB.Client lldb, HostChannelProvider channel_provider) { Object ( module: module, lldb: lldb, diff --git a/src/fruity/iokit.vala b/src/fruity/iokit.vala new file mode 100644 index 000000000..46b4239e1 --- /dev/null +++ b/src/fruity/iokit.vala @@ -0,0 +1,88 @@ +namespace Frida { + using CoreFoundation; + using Darwin.XNU; + + internal class IORegistry { + private MachPort main_port; + + public static IORegistry open () throws Error { + MachPort port; + kern_check (Darwin.IOKit.main_port (MachPort.NULL, out port)); + return new IORegistry (port); + } + + private IORegistry (MachPort main_port) { + this.main_port = main_port; + } + + public IOIterator matching_services (owned MutableDictionary matching_dict) throws Error { + Darwin.IOKit.IOIterator h; + kern_check (Darwin.IOKit.get_matching_services (main_port, matching_dict, out h)); + return new IOIterator ((owned) h); + } + } + + internal class IORegistryEntry : GLib.Object { + public IOObject io_object { + get; + construct; + } + + internal IORegistryEntry (IOObject obj) { + GLib.Object (io_object: obj); + } + + public MutableDictionary get_properties () throws Error { + MutableDictionary properties; + kern_check (unwrap ().create_cf_properties (out properties, null, 0)); + return properties; + } + + public string? get_string_property (string key) { + var v = (String) unwrap ().create_cf_property (String.make (key), null, 0); + return (v != null) ? v.to_string () : null; + } + + public IORegistryEntry parent (string plane) throws Error { + Darwin.IOKit.IOObject h; + kern_check (unwrap ().get_parent_entry (plane, out h)); + return new IORegistryEntry (new IOObject ((owned) h)); + } + + private unowned Darwin.IOKit.IORegistryEntry unwrap () { + return (Darwin.IOKit.IORegistryEntry) io_object.handle; + } + } + + internal class IOIterator { + private Darwin.IOKit.IOIterator handle; + + internal IOIterator (owned Darwin.IOKit.IOIterator handle) { + this.handle = (owned) handle; + } + + public IOIterator iterator () { + return this; + } + + public T? next_value () { + Darwin.IOKit.IOObject h = handle.next (); + if (h == Darwin.IOKit.IOObject.NULL) + return null; + return GLib.Object.new (typeof (T), io_object: new IOObject ((owned) h)); + } + } + + internal class IOObject { + internal Darwin.IOKit.IOObject handle; + + internal IOObject (owned Darwin.IOKit.IOObject handle) { + this.handle = (owned) handle; + } + } + + private void kern_check (KernReturn result) throws Error { + if (result != KernReturn.SUCCESS) + throw new Error.NOT_SUPPORTED ("%s", mach_error_string (result)); + } +} diff --git a/src/fruity/xpc-darwin.vala b/src/fruity/xpc-darwin.vala new file mode 100644 index 000000000..9c7f170a7 --- /dev/null +++ b/src/fruity/xpc-darwin.vala @@ -0,0 +1,298 @@ +[CCode (gir_namespace = "FridaFruity", gir_version = "1.0")] +namespace Frida.Fruity { + using Darwin.GCD; + using Darwin.DNSSD; + using Darwin.Net; + + public class DarwinPairingBrowser : Object, PairingBrowser { + private MainContext main_context; + private DispatchQueue dispatch_queue = new DispatchQueue ("re.frida.fruity.browser-queue", DispatchQueueAttr.SERIAL); + + private DNSService dns_connection; + private DNSService browse_session; + private TaskQueue task_queue; + + private Gee.List current_batch = new Gee.ArrayList (); + + construct { + main_context = MainContext.ref_thread_default (); + + dispatch_queue.dispatch_async (() => { + DNSService.create_connection (out dns_connection); + dns_connection.set_dispatch_queue (dispatch_queue); + + DNSService session = dns_connection; + DNSService.browse (ref session, PrivateFive | ShareConnection, 0, PAIRING_REGTYPE, PAIRING_DOMAIN, + on_browse_reply); + browse_session = session; + + task_queue = new TaskQueue (this); + }); + } + + ~DarwinPairingBrowser () { + dispatch_queue.dispatch_sync (() => { + browse_session.deallocate (); + dns_connection.deallocate (); + }); + } + + private void on_browse_reply (DNSService sd_ref, DNSService.Flags flags, uint32 interface_index, + DNSService.ErrorType error_code, string service_name, string regtype, string reply_domain) { + if (error_code != NoError) + return; + + var interface_name_buf = new char[IFNAMSIZ]; + unowned string interface_name = if_indextoname (interface_index, interface_name_buf); + + var service = new DarwinPairingServiceDetails (service_name, interface_index, interface_name, task_queue); + current_batch.add (service); + + if ((flags & DNSService.Flags.MoreComing) != 0) + return; + + var services = current_batch; + current_batch = new Gee.ArrayList (); + + schedule_on_frida_thread (() => { + services_discovered (services.to_array ()); + return Source.REMOVE; + }); + } + + private void schedule_on_frida_thread (owned SourceFunc function) { + var source = new IdleSource (); + source.set_callback ((owned) function); + source.attach (main_context); + } + + private class TaskQueue : Object, DNSServiceProvider { + private weak DarwinPairingBrowser parent; + + private DNSServiceTask? current = null; + private Gee.Deque pending = new Gee.ArrayQueue (); + + public TaskQueue (DarwinPairingBrowser parent) { + this.parent = parent; + } + + private async T with_dns_service (DNSServiceTask task, Cancellable? cancellable) throws Error, IOError { + var promise = new Promise (); + task.set_data ("promise", promise); + pending.offer_tail (task); + + maybe_start_next (); + + return (T) yield promise.future.wait_async (cancellable); + } + + private void maybe_start_next () { + if (current != null) + return; + + DNSServiceTask? task = pending.poll_head (); + if (task == null) + return; + current = task; + + parent.dispatch_queue.dispatch_async (() => { + current.dns_connection = parent.dns_connection; + current.on_complete = on_complete; + current.start (); + }); + } + + private void on_complete (Object? result, Error? error) { + parent.schedule_on_frida_thread (() => { + Promise promise = current.steal_data ("promise"); + + if (error != null) + promise.reject (error); + else + promise.resolve (result); + + current = null; + maybe_start_next (); + + return Source.REMOVE; + }); + } + } + } + + private interface DNSServiceProvider : Object { + public abstract async T with_dns_service (DNSServiceTask task, Cancellable? cancellable) throws Error, IOError; + } + + private abstract class DNSServiceTask : Object { + internal DNSService? dns_connection; + internal CompleteFunc? on_complete; + + protected DNSService? session; + + public delegate void CompleteFunc (Object? result, Error? error); + + public abstract void start (); + + protected void complete (Object? result, Error? error) { + if (session != null && session != dns_connection) { + session.deallocate (); + session = null; + } + + on_complete (result, error); + on_complete = null; + } + } + + public class DarwinPairingServiceDetails : Object, PairingServiceDetails { + public string name { + get { return _name; } + } + + public uint interface_index { + get { return _interface_index; } + } + + public string interface_name { + get { return _interface_name; } + } + + private string _name; + private uint _interface_index; + private string _interface_name; + + private DNSServiceProvider dns; + + internal DarwinPairingServiceDetails (string name, uint interface_index, string interface_name, DNSServiceProvider dns) { + _name = name; + _interface_index = interface_index; + _interface_name = interface_name; + + this.dns = dns; + } + + public async Gee.List resolve (Cancellable? cancellable) throws Error, IOError { + var task = new ResolveTask (this); + return yield dns.with_dns_service (task, cancellable); + } + + private class ResolveTask : DNSServiceTask { + private weak DarwinPairingServiceDetails parent; + + private Gee.List hosts = new Gee.ArrayList (); + + public ResolveTask (DarwinPairingServiceDetails parent) { + this.parent = parent; + } + + public override void start () { + session = dns_connection; + DNSService.resolve (ref session, PrivateFive | ShareConnection, parent.interface_index, parent.name, + PAIRING_REGTYPE, PAIRING_DOMAIN, on_resolve_reply); + } + + private void on_resolve_reply (DNSService sd_ref, DNSService.Flags flags, uint32 interface_index, + DNSService.ErrorType error_code, string fullname, string hosttarget, uint16 port, + uint8[] raw_txt_record) { + if (error_code != NoError) { + complete (null, + new Error.TRANSPORT ("Unable to resolve service '%s' on interface %s", + parent.name, parent.interface_name)); + return; + } + + var txt_record = new Gee.ArrayList (); + size_t cursor = 0; + size_t remaining = raw_txt_record.length; + while (remaining != 0) { + size_t len = raw_txt_record[cursor]; + cursor++; + remaining--; + if (len > remaining) +break; + + unowned string raw_val = (string) raw_txt_record[cursor:cursor + len]; + string val = raw_val.make_valid ((ssize_t) len); + txt_record.add (val); + + cursor += len; + remaining -= len; + } + + hosts.add (new DarwinPairingServiceHost (parent, hosttarget, uint16.from_big_endian (port), txt_record, + parent.dns)); + + if ((flags & DNSService.Flags.MoreComing) == 0) + complete (hosts, null); + } + } + } + + public class DarwinPairingServiceHost : Object, PairingServiceHost { + public string name { + get { return _name; } + } + + public uint16 port { + get { return _port; } + } + + public Gee.List txt_record { + get { return _txt_record; } + } + + private string _name; + private uint16 _port; + private Gee.List _txt_record; + + private PairingServiceDetails service; + private DNSServiceProvider dns; + + internal DarwinPairingServiceHost (PairingServiceDetails service, string name, uint16 port, Gee.List txt_record, + DNSServiceProvider dns) { + _name = name; + _port = port; + _txt_record = txt_record; + + this.service = service; + this.dns = dns; + } + + public async Gee.List resolve (Cancellable? cancellable) throws Error, IOError { + var task = new ResolveTask (this); + return yield dns.with_dns_service (task, cancellable); + } + + private class ResolveTask : DNSServiceTask { + private weak DarwinPairingServiceHost parent; + + private Gee.List addresses = new Gee.ArrayList (); + + public ResolveTask (DarwinPairingServiceHost parent) { + this.parent = parent; + } + + public override void start () { + session = dns_connection; + DNSService.get_addr_info (ref session, PrivateFive | ShareConnection, parent.service.interface_index, IPv6, + parent.name, on_info_reply); + } + + private void on_info_reply (DNSService sd_ref, DNSService.Flags flags, uint32 interface_index, + DNSService.ErrorType error_code, string hostname, void * address, uint32 ttl) { + if (error_code != NoError) { + complete (null, + new Error.TRANSPORT ("Unable to resolve host '%s' on interface %s", + parent.name, parent.service.interface_name)); + return; + } + + addresses.add ((InetSocketAddress) SocketAddress.from_native (address, sizeof (Posix.SockAddrIn6))); + + if ((flags & DNSService.Flags.MoreComing) == 0) + complete (addresses, null); + } + } + } +} diff --git a/src/fruity/xpc-linux.vala b/src/fruity/xpc-linux.vala new file mode 100644 index 000000000..37ec75522 --- /dev/null +++ b/src/fruity/xpc-linux.vala @@ -0,0 +1,426 @@ +[CCode (gir_namespace = "FridaFruity", gir_version = "1.0")] +namespace Frida.Fruity { + public sealed class LinuxTunnelFinder : Object, TunnelFinder { + public async Tunnel? find (string udid, Cancellable? cancellable) throws Error, IOError { + NcmPeer? peer = yield locate_ncm_peer (udid, cancellable); + if (peer == null) + return null; + + var bootstrap_disco = yield DiscoveryService.open ( + yield peer.open_tcp_connection (58783, cancellable), cancellable); + + var tunnel_service = bootstrap_disco.get_service ("com.apple.internal.dt.coredevice.untrusted.tunnelservice"); + var pairing_transport = new XpcPairingTransport (yield peer.open_tcp_connection (tunnel_service.port, cancellable)); + var pairing_service = yield PairingService.open (pairing_transport, cancellable); + + TunnelConnection tc = yield pairing_service.open_tunnel (peer.address, cancellable); + + var disco = yield DiscoveryService.open (yield tc.open_connection (tc.remote_rsd_port, cancellable), cancellable); + + return new LinuxTunnel (tc, disco); + } + + private static async NcmPeer? locate_ncm_peer (string udid, Cancellable? cancellable) throws Error, IOError { + var device_ifaddrs = new Gee.ArrayList (); + var fruit_finder = FruitFinder.make_default (); + string raw_udid = udid.replace ("-", ""); + Linux.Network.IfAddrs ifaddrs; + Linux.Network.getifaddrs (out ifaddrs); + for (unowned Linux.Network.IfAddrs candidate = ifaddrs; candidate != null; candidate = candidate.ifa_next) { + if (candidate.ifa_addr.sa_family != Posix.AF_INET6) + continue; + + string? candidate_udid = fruit_finder.udid_from_iface (candidate.ifa_name); + if (candidate_udid != raw_udid) + continue; + + device_ifaddrs.add (candidate); + } + if (device_ifaddrs.is_empty) + return null; + + var sockets = new Gee.ArrayList (); + var sources = new Gee.ArrayList (); + var readable_sockets = new Gee.ArrayQueue (); + bool timed_out = false; + bool waiting = false; + + var remote_address = new InetSocketAddress.from_string ("ff02::fb", 5353); + var remoted_mdns_request = make_remoted_mdns_request (); + var main_context = MainContext.get_thread_default (); + + foreach (unowned Linux.Network.IfAddrs ifaddr in device_ifaddrs) { + try { + var sock = new Socket (IPV6, DATAGRAM, UDP); + sock.bind (SocketAddress.from_native ((void *) ifaddr.ifa_addr, sizeof (Posix.SockAddrIn6)), false); + sock.send_to (remote_address, remoted_mdns_request.get_data (), cancellable); + + var source = sock.create_source (IN, cancellable); + source.set_callback (() => { + readable_sockets.offer (sock); + if (waiting) + locate_ncm_peer.callback (); + return Source.REMOVE; + }); + source.attach (main_context); + + sockets.add (sock); + sources.add (source); + } catch (GLib.Error e) { + } + } + + var timeout_source = new TimeoutSource.seconds (2); + timeout_source.set_callback (() => { + timed_out = true; + if (waiting) + locate_ncm_peer.callback (); + return Source.REMOVE; + }); + timeout_source.attach (main_context); + sources.add (timeout_source); + + Socket? sock; + while ((sock = readable_sockets.poll ()) == null && !timed_out) { + waiting = true; + yield; + waiting = false; + } + + foreach (var source in sources) + source.destroy (); + + if (sock == null && timed_out) + throw new Error.TIMED_OUT ("Unexpectedly timed out while waiting for mDNS reply"); + + InetSocketAddress sender; + try { + SocketAddress raw_sender; + var response_buf = new uint8[2048]; + sock.receive_from (out raw_sender, response_buf, cancellable); + sender = (InetSocketAddress) raw_sender; + } catch (GLib.Error e) { + throw new Error.TRANSPORT ("%s", e.message); + } + + return new NcmPeer ("%s%%%u".printf (sender.address.to_string (), sender.scope_id)); + } + + private static Bytes make_remoted_mdns_request () { + uint16 transaction_id = 0; + uint16 flags = 0; + uint16 num_questions = 1; + uint16 answer_rrs = 0; + uint16 authority_rrs = 0; + uint16 additional_rrs = 0; + string components[] = { "_remoted", "_tcp", "local" }; + uint16 record_type = 12; + uint16 dns_class = 1; + return new BufferBuilder (BIG_ENDIAN) + .append_uint16 (transaction_id) + .append_uint16 (flags) + .append_uint16 (num_questions) + .append_uint16 (answer_rrs) + .append_uint16 (authority_rrs) + .append_uint16 (additional_rrs) + .append_uint8 ((uint8) components[0].length) + .append_string (components[0], StringTerminator.NONE) + .append_uint8 ((uint8) components[1].length) + .append_string (components[1], StringTerminator.NONE) + .append_uint8 ((uint8) components[2].length) + .append_string (components[2], StringTerminator.NUL) + .append_uint16 (record_type) + .append_uint16 (dns_class) + .build (); + } + + private class NcmPeer { + public string address; + + public class NcmPeer (string address) { + this.address = address; + } + + public async IOStream open_tcp_connection (uint16 port, Cancellable? cancellable) throws Error, IOError { + SocketConnection connection; + try { + NetworkAddress service_address = NetworkAddress.parse (address, port); + + var client = new SocketClient (); + connection = yield client.connect_async (service_address, cancellable); + } catch (GLib.Error e) { + throw new Error.TRANSPORT ("%s", e.message); + } + + Tcp.enable_nodelay (connection.socket); + + return connection; + } + } + } + + private sealed class LinuxTunnel : Object, Tunnel { + public DiscoveryService discovery { + get { + return _discovery; + } + } + + private TunnelConnection tunnel_connection; + private DiscoveryService _discovery; + + public LinuxTunnel (TunnelConnection conn, DiscoveryService disco) { + tunnel_connection = conn; + _discovery = disco; + } + + public async void close (Cancellable? cancellable) throws IOError { + tunnel_connection.cancel (); + } + + public async IOStream open_tcp_connection (uint16 port, Cancellable? cancellable) throws Error, IOError { + return yield tunnel_connection.open_connection (port, cancellable); + } + } + + public class LinuxFruitFinder : Object, FruitFinder { + public string? udid_from_iface (string ifname) throws Error { + var net = "/sys/class/net"; + + var directory = File.new_build_filename (net, ifname); + if (!directory.query_exists ()) + return null; + + try { + var info = directory.query_info ("standard::*", 0); + if (!info.get_is_symlink ()) + return null; + var dev_path = Path.build_filename (net, info.get_symlink_target ()); + + var iface = File.new_build_filename (dev_path, "..", "..", "interface"); + if (!iface.query_exists ()) + return null; + var iface_stream = new DataInputStream (iface.read ()); + string iface_name = iface_stream.read_line (); + if (iface_name != "NCM Control" && iface_name != "AppleUSBEthernet") + return null; + + var serial = File.new_build_filename (dev_path, "..", "..", "..", "serial"); + if (!serial.query_exists ()) + return null; + + var serial_stream = new DataInputStream (serial.read ()); + return serial_stream.read_line (); + } catch (GLib.Error e) { + throw new Error.NOT_SUPPORTED ("%s", e.message); + } + } + } + + public class LinuxPairingBrowser : Object, PairingBrowser { + private DBusConnection connection; + private AvahiServer server; + private AvahiServiceBrowser browser; + + private Gee.List current_batch = new Gee.ArrayList (); + + private Cancellable io_cancellable = new Cancellable (); + + construct { + start.begin (); + } + + private async void start () { + try { + connection = yield GLib.Bus.get (BusType.SYSTEM, io_cancellable); + + server = yield connection.get_proxy (AVAHI_SERVICE_NAME, "/", DO_NOT_LOAD_PROPERTIES, io_cancellable); + + GLib.ObjectPath browser_path = yield server.service_browser_prepare (-1, INET6, PAIRING_REGTYPE, + PAIRING_DOMAIN, 0, io_cancellable); + browser = yield connection.get_proxy (AVAHI_SERVICE_NAME, browser_path, DO_NOT_LOAD_PROPERTIES, + io_cancellable); + browser.item_new.connect (on_item_new); + browser.all_for_now.connect (on_all_for_now); + yield browser.start (io_cancellable); + } catch (GLib.Error e) { + printerr ("Oopsie: %s\n", e.message); + } + } + + private void on_item_new (int interface_index, AvahiProtocol protocol, string name, string type, string domain, uint flags) { + char raw_interface_name[Linux.Network.INTERFACE_NAME_SIZE]; + Linux.Network.if_indextoname (interface_index, (string) raw_interface_name); + unowned string interface_name = (string) raw_interface_name; + current_batch.add (new LinuxPairingServiceDetails (name, interface_index, interface_name, protocol, server)); + } + + private void on_all_for_now () { + services_discovered (current_batch.to_array ()); + current_batch.clear (); + } + } + + private class LinuxPairingServiceDetails : Object, PairingServiceDetails { + public string name { + get { return _name; } + } + + public uint interface_index { + get { return _interface_index; } + } + + public string interface_name { + get { return _interface_name; } + } + + private string _name; + private uint _interface_index; + private string _interface_name; + private AvahiProtocol protocol; + + private AvahiServer server; + + internal LinuxPairingServiceDetails (string name, uint interface_index, string interface_name, AvahiProtocol protocol, + AvahiServer server) { + _name = name; + _interface_index = interface_index; + _interface_name = interface_name; + this.protocol = protocol; + + this.server = server; + } + + public async Gee.List resolve (Cancellable? cancellable) throws Error, IOError { + AvahiServiceResolver resolver; + try { + GLib.ObjectPath path = yield server.service_resolver_prepare ((int) interface_index, protocol, name, + PAIRING_REGTYPE, PAIRING_DOMAIN, INET6, 0, cancellable); + DBusConnection connection = ((DBusProxy) server).get_connection (); + resolver = yield connection.get_proxy (AVAHI_SERVICE_NAME, path, DO_NOT_LOAD_PROPERTIES, cancellable); + } catch (GLib.Error e) { + throw new Error.NOT_SUPPORTED ("%s", e.message); + } + + var promise = new Promise> (); + var hosts = new Gee.ArrayList (); + resolver.found.connect ((interface_index, protocol, name, type, domain, host, address_protocol, address, port, txt, + flags) => { + var txt_record = new Gee.ArrayList (); + var iter = new VariantIter (txt); + Variant? cur; + while ((cur = iter.next_value ()) != null) { + unowned string raw_val = (string) cur.get_data (); + string val = raw_val.make_valid ((ssize_t) cur.get_size ()); + txt_record.add (val); + } + + hosts.add (new LinuxPairingServiceHost ( + host, + new InetSocketAddress.from_string (address, port), + port, + txt_record)); + + if (!promise.future.ready) + promise.resolve (hosts); + }); + resolver.failure.connect (error => { + if (!promise.future.ready) + promise.reject (new Error.NOT_SUPPORTED ("%s", error)); + }); + + try { + yield resolver.start (cancellable); + } catch (GLib.Error e) { + throw new Error.NOT_SUPPORTED ("%s", e.message); + } + + return yield promise.future.wait_async (cancellable); + } + } + + public class LinuxPairingServiceHost : Object, PairingServiceHost { + public string name { + get { return _name; } + } + + public InetSocketAddress address { + get { return _address; } + } + + public uint16 port { + get { return _port; } + } + + public Gee.List txt_record { + get { return _txt_record; } + } + + private string _name; + private InetSocketAddress _address; + private uint16 _port; + private Gee.List _txt_record; + + internal LinuxPairingServiceHost (string name, InetSocketAddress address, uint16 port, Gee.List txt_record) { + _name = name; + _address = address; + _port = port; + _txt_record = txt_record; + } + + public async Gee.List resolve (Cancellable? cancellable) throws Error, IOError { + var result = new Gee.ArrayList (); + result.add (address); + return result; + } + } + + private const string AVAHI_SERVICE_NAME = "org.freedesktop.Avahi"; + + [DBus (name = "org.freedesktop.Avahi.Server2")] + private interface AvahiServer : Object { + public abstract async GLib.ObjectPath service_browser_prepare (int interface_index, AvahiProtocol protocol, string type, + string domain, uint flags, Cancellable? cancellable) throws GLib.Error; + public abstract async GLib.ObjectPath service_resolver_prepare (int interface_index, AvahiProtocol protocol, string name, + string type, string domain, AvahiProtocol aprotocol, AvahiLookupFlags flags, Cancellable? cancellable) + throws GLib.Error; + } + + [DBus (name = "org.freedesktop.Avahi.ServiceBrowser")] + private interface AvahiServiceBrowser : Object { + public signal void item_new (int interface_index, AvahiProtocol protocol, string name, string type, string domain, + uint flags); + public signal void item_remove (int interface_index, AvahiProtocol protocol, string name, string type, string domain, + uint flags); + public signal void failure (string error); + public signal void all_for_now (); + public signal void cache_exhausted (); + + public abstract async void start (Cancellable? cancellable) throws GLib.Error; + public abstract async void free (Cancellable? cancellable) throws GLib.Error; + } + + [DBus (name = "org.freedesktop.Avahi.ServiceResolver")] + private interface AvahiServiceResolver : Object { + public signal void found (int interface_index, AvahiProtocol protocol, string name, string type, string domain, string host, + AvahiProtocol address_protocol, string address, uint16 port, [DBus (signature = "aay")] Variant txt, uint flags); + public signal void failure (string error); + + public abstract async void start (Cancellable? cancellable) throws GLib.Error; + public abstract async void free (Cancellable? cancellable) throws GLib.Error; + } + + private enum AvahiProtocol { + INET, + INET6, + UNSPEC = -1, + } + + [Flags] + private enum AvahiLookupFlags { + USE_WIDE_AREA = 1, + USE_MULTICAST = 2, + NO_TXT = 4, + NO_ADDRESS = 8, + } +} diff --git a/src/fruity/xpc-macos.vala b/src/fruity/xpc-macos.vala new file mode 100644 index 000000000..3bf90a559 --- /dev/null +++ b/src/fruity/xpc-macos.vala @@ -0,0 +1,421 @@ +[CCode (gir_namespace = "FridaFruity", gir_version = "1.0")] +namespace Frida.Fruity { + using Darwin.GCD; + using Darwin.IOKit; + + public sealed class MacOSTunnelFinder : Object, TunnelFinder { + public async Tunnel? find (string udid, Cancellable? cancellable) throws Error, IOError { + var main_context = MainContext.ref_thread_default (); + var event_queue = new DispatchQueue ("re.frida.fruity.tunnel", DispatchQueueAttr.SERIAL); + + Darwin.Xpc.Dictionary? device_info = null; + bool all_listed = false; + bool waiting = false; + + var pairingd = XpcClient.make_for_mach_service ("com.apple.CoreDevice.remotepairingd", event_queue); + pairingd.notify["state"].connect ((obj, pspec) => { + if (waiting) + find.callback (); + }); + pairingd.message.connect (obj => { + var reader = new XpcObjectReader (obj); + + try { + reader.read_member ("mangledTypeName"); + if (reader.get_string_value () == "RemotePairing.ServiceEvent") { + reader + .end_member () + .read_member ("value"); + if (reader.try_read_member ("deviceFound")) { + reader + .read_member ("_0") + .read_member ("deviceInfo") + .read_member ("udid"); + if (reader.get_string_value () == udid) { + reader.end_member (); + device_info = (Darwin.Xpc.Dictionary) reader.current_object; + } + } else if (reader.has_member ("allCurrentDevicesListed")) { + all_listed = true; + } + } + } catch (Error e) { + printerr ("%s\n", e.message); + } + + var source = new IdleSource (); + source.set_callback (() => { + if (waiting) + find.callback (); + return Source.REMOVE; + }); + source.attach (main_context); + }); + + var r = new PairingdRequest ("RemotePairing.BrowseRequest"); + r.body.set_bool ("currentDevicesOnly", true); + yield pairingd.request (r.message, cancellable); + + while (!all_listed && pairingd.state == OPEN) { + waiting = true; + yield; + waiting = false; + } + + if (device_info == null) { + if (all_listed) + throw new Error.INVALID_ARGUMENT ("Device not found"); + else + throw new Error.NOT_SUPPORTED ("Unexpectedly lost connection to remotepairingd"); + } + + var pairing_device = new XpcClient (device_info.create_connection ("endpoint"), event_queue); + var tunnel = new DarwinTunnel (pairing_device); + yield tunnel.attach (cancellable); + return tunnel; + } + } + + private sealed class MacOSTunnel : Object, Tunnel { + public DiscoveryService discovery { + get { + return _discovery; + } + } + + private XpcClient pairing_device; + private Darwin.Xpc.Object? assertion_identifier; + private InetAddress? tunnel_device_address; + private DiscoveryService? _discovery; + + public MacOSTunnel (XpcClient pairing_device) { + this.pairing_device = pairing_device; + } + + public async void close (Cancellable? cancellable) throws IOError { + var r = new PairingdRequest ("RemotePairing.ReleaseAssertionRequest"); + r.body.set_value ("assertionIdentifier", assertion_identifier); + try { + yield pairing_device.request (r.message, cancellable); + } catch (Error e) { + } + } + + public async void attach (Cancellable? cancellable) throws Error, IOError { + var r = new PairingdRequest ("RemotePairing.CreateAssertionCommand"); + r.body.set_int64 ("flags", 0); + var response = yield pairing_device.request (r.message, cancellable); + + var reader = new XpcObjectReader (response); + reader.read_member ("response"); + + assertion_identifier = reader + .read_member ("assertionIdentifier") + .get_object_value (Darwin.Xpc.Uuid.TYPE); + reader.end_member (); + + string tunnel_ip_address = reader + .read_member ("info") + .read_member ("tunnelIPAddress") + .get_string_value (); + tunnel_device_address = new InetAddress.from_string (tunnel_ip_address); + + _discovery = yield locate_discovery_service (tunnel_device_address, cancellable); + } + + public async IOStream open_tcp_connection (uint16 port, Cancellable? cancellable) throws Error, IOError { + SocketConnection connection; + try { + var service_address = new InetSocketAddress (tunnel_device_address, port); + + var client = new SocketClient (); + connection = yield client.connect_async (service_address, cancellable); + } catch (GLib.Error e) { + throw new Error.TRANSPORT ("%s", e.message); + } + + Tcp.enable_nodelay (connection.socket); + + return connection; + } + + private static async DiscoveryService locate_discovery_service (InetAddress tunnel_device_address, Cancellable? cancellable) + throws Error, IOError { + var path_buf = new char[4096]; + unowned string path = (string) path_buf; + + foreach (var item in XNU.query_active_tcp_connections ()) { + if (item.family != IPV6) + continue; + if (!item.foreign_address.equal (tunnel_device_address)) + continue; + if (Darwin.XNU.proc_pidpath (item.effective_pid, path_buf) <= 0) + continue; + if (path != "/usr/libexec/remoted") + continue; + + try { + var connectable = new InetSocketAddress (tunnel_device_address, item.foreign_port); + + var sc = new SocketClient (); + SocketConnection connection = yield sc.connect_async (connectable, cancellable); + Tcp.enable_nodelay (connection.socket); + + return yield DiscoveryService.open (connection, cancellable); + } catch (GLib.Error e) { + } + } + + throw new Error.NOT_SUPPORTED ("Unable to detect RSD port"); + } + } + + private class PairingdRequest { + public Darwin.Xpc.Dictionary message = new Darwin.Xpc.Dictionary (); + public Darwin.Xpc.Dictionary body = new Darwin.Xpc.Dictionary (); + + public PairingdRequest (string name) { + message.set_string ("mangledTypeName", name); + message.set_value ("value", body); + } + } + + public class MacOSFruitFinder : Object, FruitFinder { + public string? udid_from_iface (string ifname) throws Error { + var matching_dict = Darwin.IOKit.service_matching (Darwin.IOKit.ETHERNET_INTERFACE_CLASS); + if (matching_dict == null) + return null; + matching_dict.add (String.make (Darwin.IOKit.BSD_NAME_KEY), String.make (ifname)); + + var registry = IORegistry.open (); + foreach (var service in registry.matching_services (matching_dict)) { + var usb_serial = find_idevice (service.parent (Darwin.IOKit.IOSERVICE_PLANE)); + if (usb_serial != null) + return usb_serial; + } + + return null; + } + + private string? find_idevice (IORegistryEntry service) throws Error { + if (service.get_string_property ("CFBundleIdentifier") == "com.apple.driver.usb.cdc.ncm") + return find_idevice (service.parent (Darwin.IOKit.IOSERVICE_PLANE)); + + var props = service.get_properties (); + + var prod = props.get_string_value ("USB Product Name"); + if (prod != "iPhone" && prod != "iPad") + return null; + + return props.get_string_value ("USB Serial Number"); + } + } + + namespace XNU { + public PcbList query_active_tcp_connections () { + size_t size = 0; + Darwin.XNU.sysctlbyname ("net.inet.tcp.pcblist_n", null, &size); + + var pcbs = new uint8[size]; + Darwin.XNU.sysctlbyname ("net.inet.tcp.pcblist_n", pcbs, &size); + + return new PcbList (pcbs); + } + + public class PcbList { + private uint8[] pcbs; + + internal PcbList (owned uint8[] pcbs) { + this.pcbs = (owned) pcbs; + } + + public Iterator iterator () { + return new Iterator (this); + } + + public class Iterator { + private PcbList list; + private InetItem * cursor; + + internal Iterator (PcbList list) { + this.list = list; + + var gen = (Darwin.XNU.InetPcbGeneration *) list.pcbs; + cursor = (InetItem *) ((uint8 *) list.pcbs + gen->length); + } + + public Item? next_value () { + InetPcb * pcb = null; + while (true) { + if (cursor->length == 24) + return null; + + switch (cursor->kind) { + case InetItemKind.PCB: + pcb = (InetPcb *) cursor; + break; + case InetItemKind.SOCKET: + var item = new Item (*pcb, *((InetSocket *) cursor)); + advance (); + return item; + } + + advance (); + } + } + + private void advance () { + uint32 l = cursor->length; + if (l % 8 != 0) + l += 8 - (l % 8); + cursor = (InetItem *) ((uint8 *) cursor + l); + } + } + + public class Item { + public SocketFamily family { + get { + return ((pcb.version_flag & Darwin.XNU.InetVersionFlags.IPV6) != 0) + ? SocketFamily.IPV6 + : SocketFamily.IPV4; + } + } + + public InetAddress local_address { + get { + if (cached_local_address == null) + cached_local_address = parse_address (pcb.local_address); + return cached_local_address; + } + } + + public uint16 local_port { + get { + return uint16.from_big_endian (pcb.local_port); + } + } + + public InetAddress foreign_address { + get { + if (cached_foreign_address == null) + cached_foreign_address = parse_address (pcb.foreign_address); + return cached_foreign_address; + } + } + + public uint16 foreign_port { + get { + return uint16.from_big_endian (pcb.foreign_port); + } + } + + public Posix.pid_t effective_pid { + get { + return sock.effective_pid; + } + } + + private InetPcb pcb; + private InetSocket sock; + private InetAddress? cached_local_address; + private InetAddress? cached_foreign_address; + + public Item (InetPcb pcb, InetSocket sock) { + this.pcb = pcb; + this.sock = sock; + } + + private InetAddress parse_address (uint8[] bytes) { + if ((pcb.version_flag & Darwin.XNU.InetVersionFlags.IPV6) != 0) + return new InetAddress.from_bytes (bytes, IPV6); + var addr = (Darwin.XNU.InetAddr4in6 *) bytes; + return new InetAddress.from_bytes ((uint8[]) &addr->addr4.s_addr, IPV4); + } + } + } + + [SimpleType] + public struct InetItem { + public uint32 length; + public uint32 kind; + } + + public enum InetItemKind { + SOCKET = 0x001, + PCB = 0x010, + } + + [SimpleType] + public struct InetPcb { + public uint32 length; + public uint32 kind; + + public uint64 inpp; + public uint16 foreign_port; + public uint16 local_port; + public uint32 per_protocol_pcb_low; + public uint32 per_protocol_pcb_high; + public uint32 generation_count_low; + public uint32 generation_count_high; + public int flags; + public uint32 flow; + public uint8 version_flag; + public uint8 ip_ttl; + public uint8 ip_protocol; + public uint8 padding; + public uint8 foreign_address[16]; + public uint8 local_address[16]; + public InetDepend4 depend4; + public InetDepend6 depend6; + public uint32 flowhash; + public uint32 flags2; + } + + [SimpleType] + public struct InetSocket { + public uint32 length; + public uint32 kind; + + public uint64 so; + public int16 type; + public uint16 options_low; + public uint16 options_high; + public int16 linger; + public int16 state; + public uint16 pcb[4]; + public uint16 protocol_low; + public uint16 protocol_high; + public uint16 family_low; + public uint16 family_high; + public int16 qlen; + public int16 incqlen; + public int16 qlimit; + public int16 timeo; + public uint16 error; + public Posix.pid_t pgid; + public uint32 oobmark; + public Posix.uid_t uid; + public Posix.pid_t last_pid; + public Posix.pid_t effective_pid; + public uint64 gencnt; + public uint32 flags; + public uint32 flags1; + public int32 usecount; + public int32 retaincnt; + public uint32 filter_flags; + } + + [SimpleType] + public struct InetDepend4 { + public uint8 ip_tos; + } + + [SimpleType] + public struct InetDepend6 { + public uint8 hlim; + public int checksum; + public uint16 interface_index; + public int16 hops; + } + } +} diff --git a/src/fruity/xpc-test.vala b/src/fruity/xpc-test.vala new file mode 100644 index 000000000..93d365559 --- /dev/null +++ b/src/fruity/xpc-test.vala @@ -0,0 +1,363 @@ +namespace Frida.Fruity.XPC { + private const string TUNNEL_HOST = "[fddf:a718:85ed::1]"; + private const uint16 RSD_PORT = 63025; + + private Cancellable? cancellable = null; + + private int main (string[] args) { + Frida.init_with_runtime (GLIB); + + var loop = new MainLoop (Frida.get_main_context ()); + + if (args.length != 2) { + print_usage (args); + return 1; + } + switch (args[1]) { + case "mdns": + test_mdns.begin (); + break; + case "fruit-finder": + test_fruit_finder.begin (); + break; + case "wifi-xpc": + test_wifi_xpc.begin (); + break; + case "indirect-xpc": + test_indirect_xpc.begin (); + break; + case "direct-xpc": + test_direct_xpc.begin (); + break; + default: + print_usage (args); + return 1; + } + + loop.run (); + + return 0; + } + + private void print_usage (string[] args) { + printerr ("Usage: %s \n", args[0]); + } + + private async void test_mdns () { + try { + var s = new Socket (IPV6, DATAGRAM, UDP); + + var local_address = new InetSocketAddress.from_string ("fe80::fc8d:dbd8:aeb4:ba48%enp0s20f0u1c5i4", 0); + var remote_address = new InetSocketAddress.from_string ("ff02::fb", 5353); + + Cancellable? cancellable = null; + + s.bind (local_address, false); + var request = make_remoted_mdns_request (); + ssize_t n = s.send_to (remote_address, request.get_data (), cancellable); + printerr ("send() => %zd\n", n); + hexdump (request.get_data ()); + + SocketAddress sender; + var response_buf = new uint8[2048]; + n = s.receive_from (out sender, response_buf, cancellable); + printerr ("sender=%s\n", sender.to_string ()); + printerr ("receive_from() => %zd\n", n); + hexdump (response_buf[:n]); + + var response = new Buffer (new Bytes (response_buf[:n]), BIG_ENDIAN); + var query_id = response.read_uint16 (0); + var flags = response.read_uint16 (2); + var questions = response.read_uint16 (4); + var answer_rrs = response.read_uint16 (6); + var authority_rrs = response.read_uint16 (8); + var additional_rrs = response.read_uint16 (10); + printerr ("query_id=%u flags=0x%04x questions=%u answer_rrs=%u authority_rrs=%u additional_rrs=%u\n", + query_id, + flags, + questions, + answer_rrs, + authority_rrs, + additional_rrs); + } catch (GLib.Error e) { + printerr ("%s\n", e.message); + } + } + + private Bytes make_remoted_mdns_request () { + uint16 transaction_id = 0; + uint16 flags = 0; + uint16 num_questions = 1; + uint16 answer_rrs = 0; + uint16 authority_rrs = 0; + uint16 additional_rrs = 0; + string components[] = { "_remoted", "_tcp", "local" }; + uint16 record_type = 12; + uint16 dns_class = 1; + return new BufferBuilder (BIG_ENDIAN) + .append_uint16 (transaction_id) + .append_uint16 (flags) + .append_uint16 (num_questions) + .append_uint16 (answer_rrs) + .append_uint16 (authority_rrs) + .append_uint16 (additional_rrs) + .append_uint8 ((uint8) components[0].length) + .append_string (components[0], StringTerminator.NONE) + .append_uint8 ((uint8) components[1].length) + .append_string (components[1], StringTerminator.NONE) + .append_uint8 ((uint8) components[2].length) + .append_string (components[2], StringTerminator.NUL) + .append_uint16 (record_type) + .append_uint16 (dns_class) + .build (); + } + + private async void test_fruit_finder () { +#if MACOS + var finder = new MacOSFruitFinder (); + try { + string? udid = finder.udid_from_iface ("en18"); + printerr ("udid: %s\n", udid); + } catch (Error e) { + printerr ("%s\n", e.message); + assert_not_reached (); + } +#endif + } + + private async void test_wifi_xpc () { + try { + string device_address = "fdc1:9325:7cb8:4511:49:2e2e:99e8:aa72"; + + var pairing_service_address = new InetSocketAddress.from_string (device_address, 49152); + + var client = new SocketClient (); + var connection = yield client.connect_async (pairing_service_address, cancellable); + var pairing_service = yield PairingService.open (new PlainPairingTransport (connection), cancellable); + + TunnelConnection tunnel = yield pairing_service.open_tunnel (device_address, cancellable); + + var disco = yield DiscoveryService.open ( + yield tunnel.open_connection (tunnel.remote_rsd_port, cancellable), + cancellable); + + var app_service = yield AppService.open ( + yield tunnel.open_connection (disco.get_service ("com.apple.coredevice.appservice").port, cancellable), + cancellable); + + printerr ("=== Applications\n"); + foreach (AppService.ApplicationInfo app in yield app_service.enumerate_applications ()) { + printerr ("%s\n", app.to_string ()); + } + + printerr ("\n=== Processes\n"); + foreach (AppService.ProcessInfo p in yield app_service.enumerate_processes ()) { + printerr ("%s\n", p.to_string ()); + } + + printerr ("\n\n=== Yay. Sleeping indefinitely.\n"); + yield; + } catch (GLib.Error e) { + printerr ("Oh noes: %s\n", e.message); + } + } + + private async void test_indirect_xpc () { + try { + var device_id_request = new Promise (); + + var usbmux = yield UsbmuxClient.open (cancellable); + usbmux.device_attached.connect (d => { + if (!device_id_request.future.ready) + device_id_request.resolve (d.id); + }); + yield usbmux.enable_listen_mode (cancellable); + + DeviceId id = yield device_id_request.future.wait_async (cancellable); + + yield usbmux.close (cancellable); + + usbmux = yield UsbmuxClient.open (cancellable); + yield usbmux.connect_to_port (id, 49152, cancellable); + + var pairing_transport = new XpcPairingTransport (usbmux.connection); + var pairing_service = yield PairingService.open (pairing_transport, cancellable); + printerr ("Opened pairing_service=%p\n", pairing_service); + } catch (GLib.Error e) { + printerr ("Oh noes: %s\n", e.message); + } + } + + private async void test_direct_xpc () { + try { + PairingServiceDetails[]? services = null; + + var browser = PairingBrowser.make_default (); + browser.services_discovered.connect (s => { + printerr ("Found %u services\n", s.length); + if (services == null) { + services = s; + test_direct_xpc.callback (); + } + }); + yield; + + printerr ("\n=== Got:\n"); + foreach (var service in services) { + printerr ("\t%s\n", service.to_string ()); + yield dump_service (service); + } + printerr ("\n"); + + TestDevice device = yield pick_device (services, cancellable); + + var pairing_transport = new XpcPairingTransport ( + yield device.open_service ("com.apple.internal.dt.coredevice.untrusted.tunnelservice", cancellable)); + + var pairing_service = yield PairingService.open (pairing_transport, cancellable); + + TunnelConnection tunnel = yield pairing_service.open_tunnel (device.address, cancellable); + + var disco = yield DiscoveryService.open ( + yield tunnel.open_connection (tunnel.remote_rsd_port, cancellable), + cancellable); + + var app_service = yield AppService.open ( + yield tunnel.open_connection (disco.get_service ("com.apple.coredevice.appservice").port, cancellable), + cancellable); + + printerr ("=== Applications\n"); + foreach (AppService.ApplicationInfo app in yield app_service.enumerate_applications ()) { + printerr ("%s\n", app.to_string ()); + } + + printerr ("\n=== Processes\n"); + foreach (AppService.ProcessInfo p in yield app_service.enumerate_processes ()) { + printerr ("%s\n", p.to_string ()); + } + + printerr ("\n\n=== Yay. Sleeping indefinitely.\n"); + yield; + } catch (Error e) { + printerr ("Oh noes: %s\n", e.message); + } catch (IOError e) { + assert_not_reached (); + } + } + + private async TestDevice pick_device (PairingServiceDetails[] services, Cancellable? cancellable) throws Error, IOError { + foreach (PairingServiceDetails service in services) { + foreach (PairingServiceHost host in yield service.resolve (cancellable)) { + if (!("iPad" in host.name)) { + printerr ("Skipping: %s\n", host.to_string ()); + continue; + } + + foreach (InetSocketAddress socket_address in yield host.resolve (cancellable)) { + string candidate_address = socket_address_to_string (socket_address) + "%" + service.interface_name; + + SocketConnection connection; + try { + printerr ("Trying %s -> %s\n", service.to_string (), host.to_string ()); + printerr ("\t(i.e., candidate_address: %s)\n", candidate_address); + + InetSocketAddress? disco_address = new InetSocketAddress.from_string (candidate_address, 58783); + if (disco_address == null) { + printerr ("\tSkipping due to invalid address\n"); + continue; + } + + var client = new SocketClient (); + connection = yield client.connect_async (disco_address, cancellable); + } catch (GLib.Error e) { + printerr ("\tSkipping: %s\n", e.message); + continue; + } + + Tcp.enable_nodelay (connection.socket); + + try { + var disco = yield DiscoveryService.open (connection, cancellable); + + printerr ("Connected through interface %s\n", service.interface_name); + + return new TestDevice () { + address = candidate_address, + disco = disco, + }; + } catch (Error e) { + printerr ("\tSkipping: %s\n", e.message); + continue; + } + } + } + } + throw new Error.TRANSPORT ("Unable to connect to any of the services"); + } + + private class TestDevice { + public string address; + public DiscoveryService disco; + + public async SocketConnection open_service (string service_name, Cancellable? cancellable) throws Error, IOError { + SocketConnection connection; + try { + ServiceInfo service_info = disco.get_service (service_name); + NetworkAddress service_address = NetworkAddress.parse (address, service_info.port); + + var client = new SocketClient (); + connection = yield client.connect_async (service_address, cancellable); + } catch (GLib.Error e) { + throw new Error.TRANSPORT ("%s", e.message); + } + + Tcp.enable_nodelay (connection.socket); + + return connection; + } + } + + private async void dump_service (PairingServiceDetails service) { + try { + var hosts = yield service.resolve (cancellable); + uint i = 0; + foreach (var host in hosts) { + printerr ("\t\thosts[%u]: %s\n", i, host.to_string ()); + var addresses = yield host.resolve (cancellable); + uint j = 0; + foreach (var addr in addresses) { + printerr ("\t\t\taddresses[%u]: %s\n", j, socket_address_to_string (addr)); + j++; + } + i++; + } + } catch (Error e) { + printerr ("%s\n", e.message); + } catch (IOError e) { + assert_not_reached (); + } + } + + private string socket_address_to_string (SocketAddress addr) { + var native_size = addr.get_native_size (); + var native = new uint8[native_size]; + try { + addr.to_native (native, native_size); + } catch (GLib.Error e) { + assert_not_reached (); + } + + var desc = new StringBuilder.sized (32); + for (uint j = 0; j != 16; j += 2) { + if (desc.len != 0) + desc.append_c (':'); + uint8 b1 = native[8 + j]; + uint8 b2 = native[8 + j + 1]; + desc.append_printf ("%02x%02x", b1, b2); + } + + //var scope_id = (uint32 *) ((uint8 *) native + 8 + 16); + + return desc.str; + } +} diff --git a/src/fruity/xpc.vala b/src/fruity/xpc.vala new file mode 100644 index 000000000..e68d6892a --- /dev/null +++ b/src/fruity/xpc.vala @@ -0,0 +1,4588 @@ +[CCode (gir_namespace = "FridaFruity", gir_version = "1.0")] +namespace Frida.Fruity { + using OpenSSL; + using OpenSSL.Envelope; + + private const string PAIRING_REGTYPE = "_remoted._tcp"; + private const string PAIRING_DOMAIN = "local."; + + public interface TunnelFinder : Object { + public static TunnelFinder make_default () { +#if MACOS + return new MacOSTunnelFinder (); +#elif LINUX && !ANDROID + return new LinuxTunnelFinder (); +#else + return new NullTunnelFinder (); +#endif + } + + public abstract async Tunnel? find (string udid, Cancellable? cancellable) throws Error, IOError; + } + + public class NullTunnelFinder : Object, TunnelFinder { + public async Tunnel? find (string udid, Cancellable? cancellable) throws Error, IOError { + return null; + } + } + + public interface Tunnel : Object { + public abstract DiscoveryService discovery { + get; + } + + public abstract async void close (Cancellable? cancellable) throws IOError; + public abstract async IOStream open_tcp_connection (uint16 port, Cancellable? cancellable) throws Error, IOError; + } + + public interface FruitFinder : Object { + public static FruitFinder make_default () { +#if MACOS + return new MacOSFruitFinder (); +#elif LINUX && !ANDROID + return new LinuxFruitFinder (); +#else + return new NullFruitFinder (); +#endif + } + + public abstract string? udid_from_iface (string ifname) throws Error; + } + + public class NullFruitFinder : Object, FruitFinder { + public string? udid_from_iface (string ifname) throws Error { + return null; + } + } + + public interface PairingBrowser : Object { + public static PairingBrowser make_default () { +#if DARWIN + return new DarwinPairingBrowser (); +#elif LINUX && !ANDROID + return new LinuxPairingBrowser (); +#else + return new NullPairingBrowser (); +#endif + } + + public signal void services_discovered (PairingServiceDetails[] services); + } + + public class NullPairingBrowser : Object, PairingBrowser { + } + + public interface PairingServiceDetails : Object { + public abstract string name { + get; + } + + public abstract uint interface_index { + get; + } + + public abstract string interface_name { + get; + } + + public abstract async Gee.List resolve (Cancellable? cancellable = null) throws Error, IOError; + + public string to_string () { + return @"PairingServiceDetails { name: \"$name\", interface_index: $interface_index," + + @" interface_name: \"$interface_name\" }"; + } + } + + public interface PairingServiceHost : Object { + public abstract string name { + get; + } + + public abstract uint16 port { + get; + } + + public abstract Gee.List txt_record { + get; + } + + public abstract async Gee.List resolve (Cancellable? cancellable = null) throws Error, IOError; + + public string to_string () { + return @"PairingServiceHost { name: \"$name\", port: $port, txt_record: <$(txt_record.size) entries> }"; + } + } + + public class DiscoveryService : Object, AsyncInitable { + public IOStream stream { + get; + construct; + } + + private XpcConnection connection; + + private Promise handshake_promise = new Promise (); + private Variant handshake_body; + + public static async DiscoveryService open (IOStream stream, Cancellable? cancellable = null) throws Error, IOError { + var service = new DiscoveryService (stream); + + try { + yield service.init_async (Priority.DEFAULT, cancellable); + } catch (GLib.Error e) { + throw_api_error (e); + } + + return service; + } + + private DiscoveryService (IOStream stream) { + Object (stream: stream); + } + + private async bool init_async (int io_priority, Cancellable? cancellable) throws Error, IOError { + connection = new XpcConnection (stream); + connection.close.connect (on_close); + connection.message.connect (on_message); + connection.activate (); + + handshake_body = yield handshake_promise.future.wait_async (cancellable); + + return true; + } + + public void close () { + connection.cancel (); + } + + public string query_udid () throws Error { + var reader = new VariantReader (handshake_body); + reader + .read_member ("Properties") + .read_member ("UniqueDeviceID"); + return reader.get_string_value (); + } + + public ServiceInfo get_service (string identifier) throws Error { + var reader = new VariantReader (handshake_body); + reader.read_member ("Services"); + try { + reader.read_member (identifier); + } catch (Error e) { + throw new Error.NOT_SUPPORTED ("Service '%s' not found", identifier); + } + + var port = (uint16) uint.parse (reader.read_member ("Port").get_string_value ()); + + return new ServiceInfo () { + port = port, + }; + } + + private void on_close (Error? error) { + if (!handshake_promise.future.ready) { + handshake_promise.reject ( + (error != null) + ? error + : new Error.TRANSPORT ("XpcConnection closed while waiting for Handshake message")); + } + } + + private void on_message (XpcMessage msg) { + if (msg.body == null) + return; + + var reader = new VariantReader (msg.body); + try { + reader.read_member ("MessageType"); + unowned string message_type = reader.get_string_value (); + + if (message_type == "Handshake") + handshake_promise.resolve (msg.body); + } catch (Error e) { + } + } + + public string to_string () { + return @"DiscoveryService { handshake_body: $(variant_to_pretty_string (handshake_body)) }"; + } + } + + public class ServiceInfo { + public uint16 port; + } + + public class PairingService : Object, AsyncInitable { + public PairingTransport transport { + get; + construct; + } + + public DeviceOptions device_options { + get; + private set; + } + + public DeviceInfo? device_info { + get; + private set; + } + + private Gee.Map> requests = + new Gee.HashMap> (Numeric.uint64_hash, Numeric.uint64_equal); + private uint64 next_control_sequence_number = 0; + private uint64 next_encrypted_sequence_number = 0; + + private File config_file; + private string host_identifier; + private Key pair_record_key; + private ChaCha20Poly1305? client_cipher; + private ChaCha20Poly1305? server_cipher; + + public static async PairingService open (PairingTransport transport, Cancellable? cancellable = null) + throws Error, IOError { + var service = new PairingService (transport); + + try { + yield service.init_async (Priority.DEFAULT, cancellable); + } catch (GLib.Error e) { + throw_api_error (e); + } + + return service; + } + + private PairingService (PairingTransport transport) { + Object (transport: transport); + } + + construct { + transport.close.connect (on_close); + transport.message.connect (on_message); + +#if DARWIN + config_file = File.new_for_path ("/var/db/lockdown/RemotePairing/user_%u/selfIdentity.plist".printf ( + (uint) Posix.getuid ())); +#else + config_file = File.new_build_filename (Environment.get_user_config_dir (), "frida", "remote-xpc.plist"); +#endif + + Bytes? key = null; + try { + uint8[] raw_identity; + FileUtils.get_data (config_file.get_path (), out raw_identity); + Plist identity = new Plist.from_data (raw_identity); + + unowned string identifier = identity.get_string ("identifier"); + key = identity.get_bytes ("privateKey"); + + host_identifier = identifier; + } catch (GLib.Error e) { + host_identifier = make_host_identifier (); + } + if (key == null) { + uint8 dummy_key[32] = { 0, }; + key = new Bytes (dummy_key); + } + + pair_record_key = new Key.from_raw_private_key (ED25519, null, key.get_data ()); + } + + private async bool init_async (int io_priority, Cancellable? cancellable) throws Error, IOError { + yield transport.open (cancellable); + + yield attempt_pair_verify (cancellable); + + Bytes? shared_key = yield verify_manual_pairing (cancellable); + if (shared_key == null) { + if (!device_options.allows_pair_setup) + throw new Error.NOT_SUPPORTED ("Device not paired and pairing not allowed on current transport"); + shared_key = yield setup_manual_pairing (cancellable); + } + + client_cipher = new ChaCha20Poly1305 (derive_chacha_key (shared_key, "ClientEncrypt-main")); + server_cipher = new ChaCha20Poly1305 (derive_chacha_key (shared_key, "ServerEncrypt-main")); + + return true; + } + + public void close () { + transport.cancel (); + } + + public async TunnelConnection open_tunnel (string device_address, Cancellable? cancellable = null) throws Error, IOError { + Key local_keypair = make_keypair (RSA); + + string request = Json.to_string ( + new Json.Builder () + .begin_object () + .set_member_name ("request") + .begin_object () + .set_member_name ("_0") + .begin_object () + .set_member_name ("createListener") + .begin_object () + .set_member_name ("transportProtocolType") + .add_string_value ("quic") + .set_member_name ("key") + .add_string_value (Base64.encode (key_to_der (local_keypair))) + .end_object () + .end_object () + .end_object () + .end_object () + .get_root (), false); + + string response = yield request_encrypted (request, cancellable); + + Json.Reader reader; + try { + reader = new Json.Reader (Json.from_string (response)); + } catch (GLib.Error e) { + throw new Error.PROTOCOL ("Invalid response JSON"); + } + + reader.read_member ("response"); + reader.read_member ("_1"); + reader.read_member ("createListener"); + + reader.read_member ("devicePublicKey"); + string? device_pubkey = reader.get_string_value (); + reader.end_member (); + + reader.read_member ("port"); + uint16 port = (uint16) reader.get_int_value (); + reader.end_member (); + + GLib.Error? error = reader.get_error (); + if (error != null) + throw new Error.PROTOCOL ("Invalid response: %s", error.message); + + Key remote_pubkey = key_from_der (Base64.decode (device_pubkey)); + + return yield TunnelConnection.open ( + new InetSocketAddress.from_string (device_address, port), + new TunnelKey ((owned) local_keypair), + new TunnelKey ((owned) remote_pubkey), + cancellable); + } + + private async void attempt_pair_verify (Cancellable? cancellable) throws Error, IOError { + Bytes payload = transport.make_object_builder () + .begin_dictionary () + .set_member_name ("request") + .begin_dictionary () + .set_member_name ("_0") + .begin_dictionary () + .set_member_name ("handshake") + .begin_dictionary () + .set_member_name ("_0") + .begin_dictionary () + .set_member_name ("wireProtocolVersion") + .add_int64_value (19) + .set_member_name ("hostOptions") + .begin_dictionary () + .set_member_name ("attemptPairVerify") + .add_bool_value (true) + .end_dictionary () + .end_dictionary () + .end_dictionary () + .end_dictionary () + .end_dictionary () + .end_dictionary () + .build (); + + ObjectReader response = yield request_plain (payload, cancellable); + + response + .read_member ("response") + .read_member ("_1") + .read_member ("handshake") + .read_member ("_0"); + + response.read_member ("deviceOptions"); + + bool allows_pair_setup = response.read_member ("allowsPairSetup").get_bool_value (); + response.end_member (); + + bool allows_pinless_pairing = response.read_member ("allowsPinlessPairing").get_bool_value (); + response.end_member (); + + bool allows_promptless_automation_pairing_upgrade = + response.read_member ("allowsPromptlessAutomationPairingUpgrade").get_bool_value (); + response.end_member (); + + bool allows_sharing_sensitive_info = response.read_member ("allowsSharingSensitiveInfo").get_bool_value (); + response.end_member (); + + bool allows_incoming_tunnel_connections = + response.read_member ("allowsIncomingTunnelConnections").get_bool_value (); + response.end_member (); + + device_options = new DeviceOptions () { + allows_pair_setup = allows_pair_setup, + allows_pinless_pairing = allows_pinless_pairing, + allows_promptless_automation_pairing_upgrade = allows_promptless_automation_pairing_upgrade, + allows_sharing_sensitive_info = allows_sharing_sensitive_info, + allows_incoming_tunnel_connections = allows_incoming_tunnel_connections, + }; + + if (response.has_member ("peerDeviceInfo")) { + response.read_member ("peerDeviceInfo"); + + string name = response.read_member ("name").get_string_value (); + response.end_member (); + + string model = response.read_member ("model").get_string_value (); + response.end_member (); + + string udid = response.read_member ("udid").get_string_value (); + response.end_member (); + + uint64 ecid = response.read_member ("ecid").get_uint64_value (); + response.end_member (); + + Plist kvs; + try { + kvs = new Plist.from_binary (response.read_member ("deviceKVSData").get_data_value ().get_data ()); + response.end_member (); + } catch (PlistError e) { + throw new Error.PROTOCOL ("%s", e.message); + } + + device_info = new DeviceInfo () { + name = name, + model = model, + udid = udid, + ecid = ecid, + kvs = kvs, + }; + } + } + + private async Bytes? verify_manual_pairing (Cancellable? cancellable) throws Error, IOError { + Key host_keypair = make_keypair (X25519); + uint8[] raw_host_pubkey = get_raw_public_key (host_keypair).get_data (); + + Bytes start_params = new PairingParamsBuilder () + .add_state (1) + .add_public_key (host_keypair) + .build (); + + Bytes start_payload = transport.make_object_builder () + .begin_dictionary () + .set_member_name ("kind") + .add_string_value ("verifyManualPairing") + .set_member_name ("startNewSession") + .add_bool_value (true) + .set_member_name ("data") + .add_data_value (start_params) + .end_dictionary () + .build (); + + var start_response = yield request_pairing_data (start_payload, cancellable); + uint8[] raw_device_pubkey = start_response.read_member ("public-key").get_data_value ().get_data (); + start_response.end_member (); + var device_pubkey = new Key.from_raw_public_key (X25519, null, raw_device_pubkey); + + Bytes shared_key = derive_shared_key (host_keypair, device_pubkey); + + Bytes operation_key = derive_chacha_key (shared_key, + "Pair-Verify-Encrypt-Info", + "Pair-Verify-Encrypt-Salt"); + + var cipher = new ChaCha20Poly1305 (operation_key); + + // TODO: Verify signature using peer's public key. + /* var start_inner_response = */ new VariantReader (PairingParamsParser.parse (cipher.decrypt ( + new Bytes.static ("\x00\x00\x00\x00PV-Msg02".data[:12]), + start_response.read_member ("encrypted-data").get_data_value ()).get_data ())); + + var message = new ByteArray.sized (100); + message.append (raw_host_pubkey); + message.append (host_identifier.data); + message.append (raw_device_pubkey); + Bytes signature = compute_message_signature (ByteArray.free_to_bytes ((owned) message), pair_record_key); + + Bytes inner_params = new PairingParamsBuilder () + .add_identifier (host_identifier) + .add_signature (signature) + .build (); + + Bytes outer_params = new PairingParamsBuilder () + .add_state (3) + .add_encrypted_data ( + cipher.encrypt ( + new Bytes.static ("\x00\x00\x00\x00PV-Msg03".data[:12]), + inner_params)) + .build (); + + Bytes finish_payload = transport.make_object_builder () + .begin_dictionary () + .set_member_name ("kind") + .add_string_value ("verifyManualPairing") + .set_member_name ("startNewSession") + .add_bool_value (false) + .set_member_name ("data") + .add_data_value (outer_params) + .end_dictionary () + .build (); + + ObjectReader finish_response = yield request_pairing_data (finish_payload, cancellable); + if (finish_response.has_member ("error")) { + yield post_plain (transport.make_object_builder () + .begin_dictionary () + .set_member_name ("event") + .begin_dictionary () + .set_member_name ("_0") + .begin_dictionary () + .set_member_name ("pairVerifyFailed") + .begin_dictionary () + .end_dictionary () + .end_dictionary () + .end_dictionary () + .end_dictionary () + .build (), cancellable); + return null; + } + + return shared_key; + } + + private async Bytes setup_manual_pairing (Cancellable? cancellable) throws Error, IOError { + Bytes start_params = new PairingParamsBuilder () + .add_method (0) + .add_state (1) + .build (); + + Bytes start_payload = transport.make_object_builder () + .begin_dictionary () + .set_member_name ("kind") + .add_string_value ("setupManualPairing") + .set_member_name ("startNewSession") + .add_bool_value (true) + .set_member_name ("sendingHost") + .add_string_value (Environment.get_host_name ()) + .set_member_name ("data") + .add_data_value (start_params) + .end_dictionary () + .build (); + + var start_response = yield request_pairing_data (start_payload, cancellable); + if (start_response.has_member ("retry-delay")) { + uint16 retry_delay = start_response.read_member ("retry-delay").get_uint16_value (); + throw new Error.INVALID_OPERATION ("Rate limit exceeded, try again in %u seconds", retry_delay); + } + + Bytes remote_pubkey = start_response.read_member ("public-key").get_data_value (); + start_response.end_member (); + + Bytes salt = start_response.read_member ("salt").get_data_value (); + start_response.end_member (); + + var srp_session = new SRPClientSession ("Pair-Setup", "000000"); + srp_session.process (remote_pubkey, salt); + Bytes shared_key = srp_session.key; + + Bytes verify_params = new PairingParamsBuilder () + .add_state (3) + .add_raw_public_key (srp_session.public_key) + .add_proof (srp_session.key_proof) + .build (); + + Bytes verify_payload = transport.make_object_builder () + .begin_dictionary () + .set_member_name ("kind") + .add_string_value ("setupManualPairing") + .set_member_name ("startNewSession") + .add_bool_value (false) + .set_member_name ("sendingHost") + .add_string_value (Environment.get_host_name ()) + .set_member_name ("data") + .add_data_value (verify_params) + .end_dictionary () + .build (); + + var verify_response = yield request_pairing_data (verify_payload, cancellable); + Bytes remote_proof = verify_response.read_member ("proof").get_data_value (); + + srp_session.verify_proof (remote_proof); + + Bytes operation_key = derive_chacha_key (shared_key, + "Pair-Setup-Encrypt-Info", + "Pair-Setup-Encrypt-Salt"); + + var cipher = new ChaCha20Poly1305 (operation_key); + + Key new_pair_record_key = make_keypair (ED25519); + Bytes new_pair_record_pubkey = get_raw_public_key (new_pair_record_key); + + uint8 raw_irk[16]; + Rng.generate (raw_irk); + Bytes irk = new Bytes (raw_irk); + + Bytes signing_key = derive_chacha_key (shared_key, + "Pair-Setup-Controller-Sign-Info", + "Pair-Setup-Controller-Sign-Salt"); + + var message = new ByteArray.sized (100); + message.append (signing_key.get_data ()); + message.append (host_identifier.data); + message.append (new_pair_record_pubkey.get_data ()); + Bytes signature = compute_message_signature (ByteArray.free_to_bytes ((owned) message), new_pair_record_key); + + Bytes info = new OpackBuilder () + .begin_dictionary () + .set_member_name ("name") + .add_string_value (Environment.get_host_name ()) + .set_member_name ("accountID") + .add_string_value (host_identifier) + .set_member_name ("remotepairing_serial_number") + .add_string_value ("AAAAAAAAAAAA") + .set_member_name ("altIRK") + .add_data_value (irk) + .set_member_name ("model") + .add_string_value ("computer-model") + .set_member_name ("mac") + .add_data_value (new Bytes ({ 0x11, 0x22, 0x33, 0x44, 0x55, 0x66 })) + .set_member_name ("btAddr") + .add_string_value ("11:22:33:44:55:66") + .end_dictionary () + .build (); + + Bytes inner_params = new PairingParamsBuilder () + .add_identifier (host_identifier) + .add_raw_public_key (new_pair_record_pubkey) + .add_signature (signature) + .add_info (info) + .build (); + + Bytes outer_params = new PairingParamsBuilder () + .add_state (5) + .add_encrypted_data ( + cipher.encrypt ( + new Bytes.static ("\x00\x00\x00\x00PS-Msg05".data[:12]), + inner_params)) + .build (); + + Bytes finish_payload = transport.make_object_builder () + .begin_dictionary () + .set_member_name ("kind") + .add_string_value ("setupManualPairing") + .set_member_name ("startNewSession") + .add_bool_value (false) + .set_member_name ("sendingHost") + .add_string_value (Environment.get_host_name ()) + .set_member_name ("data") + .add_data_value (outer_params) + .end_dictionary () + .build (); + + var finish_response = yield request_pairing_data (finish_payload, cancellable); + + Bytes encrypted_response = finish_response.read_member ("encrypted-data").get_data_value (); + Bytes raw_response = cipher.decrypt (new Bytes.static ("\x00\x00\x00\x00PS-Msg06".data[:12]), encrypted_response); + // TODO: Wire up if/when needed. + /* Variant response = */ PairingParamsParser.parse (raw_response.get_data ()); + + var config = new Plist (); + config.set_string ("identifier", host_identifier); + config.set_bytes ("publicKey", new_pair_record_pubkey); + config.set_bytes ("privateKey", get_raw_private_key (new_pair_record_key)); + config.set_bytes ("irk", irk); + try { + config_file.get_parent ().make_directory_with_parents (cancellable); + } catch (GLib.Error e) { + } + try { + FileUtils.set_contents (config_file.get_path (), config.to_xml ()); + } catch (GLib.Error e) { + throw new Error.NOT_SUPPORTED ("%s", e.message); + } + + pair_record_key = (owned) new_pair_record_key; + + return shared_key; + } + + private async ObjectReader request_pairing_data (Bytes payload, Cancellable? cancellable) throws Error, IOError { + Bytes wrapper = transport.make_object_builder () + .begin_dictionary () + .set_member_name ("event") + .begin_dictionary () + .set_member_name ("_0") + .begin_dictionary () + .set_member_name ("pairingData") + .begin_dictionary () + .set_member_name ("_0") + .add_raw_value (payload) + .end_dictionary () + .end_dictionary () + .end_dictionary () + .end_dictionary () + .build (); + + ObjectReader response = yield request_plain (wrapper, cancellable); + + response + .read_member ("event") + .read_member ("_0"); + + if (response.has_member ("pairingRejectedWithError")) { + string description = response + .read_member ("pairingRejectedWithError") + .read_member ("wrappedError") + .read_member ("userInfo") + .read_member ("NSLocalizedDescription") + .get_string_value (); + throw new Error.PROTOCOL ("%s", description); + } + + Bytes raw_data = response + .read_member ("pairingData") + .read_member ("_0") + .read_member ("data") + .get_data_value (); + Variant data = PairingParamsParser.parse (raw_data.get_data ()); + return new VariantReader (data); + } + + private async ObjectReader request_plain (Bytes payload, Cancellable? cancellable) throws Error, IOError { + uint64 seqno = next_control_sequence_number++; + var promise = new Promise (); + requests[seqno] = promise; + + try { + yield post_plain_with_sequence_number (seqno, payload, cancellable); + } catch (GLib.Error e) { + if (requests.unset (seqno)) + promise.reject (e); + } + + ObjectReader response = yield promise.future.wait_async (cancellable); + + return response + .read_member ("plain") + .read_member ("_0"); + } + + private async void post_plain (Bytes payload, Cancellable? cancellable) throws Error, IOError { + uint64 seqno = next_control_sequence_number++; + yield post_plain_with_sequence_number (seqno, payload, cancellable); + } + + private async void post_plain_with_sequence_number (uint64 seqno, Bytes payload, Cancellable? cancellable) + throws Error, IOError { + transport.post (transport.make_object_builder () + .begin_dictionary () + .set_member_name ("sequenceNumber") + .add_uint64_value (seqno) + .set_member_name ("originatedBy") + .add_string_value ("host") + .set_member_name ("message") + .begin_dictionary () + .set_member_name ("plain") + .begin_dictionary () + .set_member_name ("_0") + .add_raw_value (payload) + .end_dictionary () + .end_dictionary () + .end_dictionary () + .build ()); + } + + private async string request_encrypted (string json, Cancellable? cancellable) throws Error, IOError { + uint64 seqno = next_control_sequence_number++; + var promise = new Promise (); + requests[seqno] = promise; + + Bytes iv = new BufferBuilder (LITTLE_ENDIAN) + .append_uint64 (next_encrypted_sequence_number++) + .append_uint32 (0) + .build (); + + Bytes raw_request = transport.make_object_builder () + .begin_dictionary () + .set_member_name ("sequenceNumber") + .add_uint64_value (seqno) + .set_member_name ("originatedBy") + .add_string_value ("host") + .set_member_name ("message") + .begin_dictionary () + .set_member_name ("streamEncrypted") + .begin_dictionary () + .set_member_name ("_0") + .add_data_value (client_cipher.encrypt (iv, new Bytes.static (json.data))) + .end_dictionary () + .end_dictionary () + .end_dictionary () + .build (); + + transport.post (raw_request); + + ObjectReader response = yield promise.future.wait_async (cancellable); + + Bytes encrypted_response = response + .read_member ("streamEncrypted") + .read_member ("_0") + .get_data_value (); + + Bytes decrypted_response = server_cipher.decrypt (iv, encrypted_response); + + unowned string s = (string) decrypted_response.get_data (); + if (!s.validate ((ssize_t) decrypted_response.get_size ())) + throw new Error.PROTOCOL ("Invalid UTF-8"); + + return s; + } + + private void on_close (Error? error) { + var e = (error != null) + ? error + : new Error.TRANSPORT ("Connection closed while waiting for response"); + foreach (Promise promise in requests.values) + promise.reject (e); + requests.clear (); + } + + private void on_message (ObjectReader reader) { + try { + string origin = reader.read_member ("originatedBy").get_string_value (); + if (origin != "device") + return; + reader.end_member (); + + uint64 seqno = reader.read_member ("sequenceNumber").get_uint64_value (); + reader.end_member (); + + reader.read_member ("message"); + + Promise promise; + if (!requests.unset (seqno, out promise)) + return; + + promise.resolve (reader); + } catch (Error e) { + } + } + + private static Key make_keypair (KeyType type) { + var ctx = new KeyContext.for_key_type (type); + ctx.keygen_init (); + + Key? keypair = null; + ctx.keygen (ref keypair); + + return keypair; + } + + private static uint8[] key_to_der (Key key) { + var sink = new BasicIO (BasicIOMethod.memory ()); + key.to_der (sink); + unowned uint8[] der_data = get_basic_io_content (sink); + uint8[] der_data_owned = der_data; + return der_data_owned; + } + + private static Key key_from_der (uint8[] der) throws Error { + var source = new BasicIO.from_static_memory_buffer (der); + Key? key = new Key.from_der (source); + if (key == null) + throw new Error.PROTOCOL ("Invalid key"); + return key; + } + + private static unowned uint8[] get_basic_io_content (BasicIO bio) { + unowned uint8[] data; + long n = bio.get_mem_data (out data); + data.length = (int) n; + return data; + } + + private static Bytes derive_shared_key (Key local_keypair, Key remote_pubkey) { + var ctx = new KeyContext.for_key (local_keypair); + ctx.derive_init (); + ctx.derive_set_peer (remote_pubkey); + + size_t size = 0; + ctx.derive (null, ref size); + + var shared_key = new uint8[size]; + ctx.derive (shared_key, ref size); + + return new Bytes.take ((owned) shared_key); + } + + private static Bytes derive_chacha_key (Bytes shared_key, string info, string? salt = null) { + var kdf = KeyDerivationFunction.fetch (null, KeyDerivationAlgorithm.HKDF); + + var kdf_ctx = new KeyDerivationContext (kdf); + + size_t return_size = OpenSSL.ParamReturnSize.UNMODIFIED; + + OpenSSL.Param kdf_params[] = { + { KeyDerivationParameter.DIGEST, UTF8_STRING, OpenSSL.ShortName.sha512.data, return_size }, + { KeyDerivationParameter.KEY, OCTET_STRING, shared_key.get_data (), return_size }, + { KeyDerivationParameter.INFO, OCTET_STRING, info.data, return_size }, + { (salt != null) ? KeyDerivationParameter.SALT : null, OCTET_STRING, (salt != null) ? salt.data : null, + return_size }, + { null, INTEGER, null, return_size }, + }; + + var derived_key = new uint8[32]; + kdf_ctx.derive (derived_key, kdf_params); + + return new Bytes.take ((owned) derived_key); + } + + private static Bytes compute_message_signature (Bytes message, Key key) { + var ctx = new MessageDigestContext (); + ctx.digest_sign_init (null, null, null, key); + + unowned uint8[] data = message.get_data (); + + size_t size = 0; + ctx.digest_sign (null, ref size, data); + + var signature = new uint8[size]; + ctx.digest_sign (signature, ref size, data); + + return new Bytes.take ((owned) signature); + } + + private class ChaCha20Poly1305 { + private Bytes key; + + private Cipher cipher = Cipher.fetch (null, OpenSSL.ShortName.chacha20_poly1305); + private CipherContext? cached_ctx; + + private const size_t TAG_SIZE = 16; + + public ChaCha20Poly1305 (Bytes key) { + this.key = key; + } + + public Bytes encrypt (Bytes iv, Bytes message) { + size_t cleartext_size = message.get_size (); + var buf = new uint8[cleartext_size + TAG_SIZE]; + + unowned CipherContext ctx = get_context (); + cached_ctx.encrypt_init (cipher, key.get_data (), iv.get_data ()); + + int size = buf.length; + ctx.encrypt_update (buf, ref size, message.get_data ()); + + int extra_size = buf.length - size; + ctx.encrypt_final (buf[size:], ref extra_size); + assert (extra_size == 0); + + ctx.ctrl (AEAD_GET_TAG, (int) TAG_SIZE, (void *) buf[size:]); + + return new Bytes.take ((owned) buf); + } + + public Bytes decrypt (Bytes iv, Bytes message) throws Error { + size_t message_size = message.get_size (); + if (message_size < 1 + TAG_SIZE) + throw new Error.PROTOCOL ("Encrypted message is too short"); + unowned uint8[] message_data = message.get_data (); + + var buf = new uint8[message_size]; + + unowned CipherContext ctx = get_context (); + cached_ctx.decrypt_init (cipher, key.get_data (), iv.get_data ()); + + int size = (int) message_size; + int res = ctx.decrypt_update (buf, ref size, message_data); + if (res != 1) + throw new Error.PROTOCOL ("Failed to decrypt: %d", res); + + int extra_size = buf.length - size; + res = ctx.decrypt_final (buf[size:], ref extra_size); + if (res != 1) + throw new Error.PROTOCOL ("Failed to decrypt: %d", res); + assert (extra_size == 0); + + size_t cleartext_size = message_size - TAG_SIZE; + buf[cleartext_size] = 0; + buf.length = (int) cleartext_size; + + return new Bytes.take ((owned) buf); + } + + private unowned CipherContext get_context () { + if (cached_ctx == null) + cached_ctx = new CipherContext (); + else + cached_ctx.reset (); + return cached_ctx; + } + } + + private class SRPClientSession { + public Bytes public_key { + owned get { + var buf = new uint8[local_pubkey.num_bytes ()]; + local_pubkey.to_big_endian (buf); + return new Bytes.take ((owned) buf); + } + } + + public Bytes key { + get { + return _key; + } + } + + public Bytes key_proof { + get { + return _key_proof; + } + } + + private string username; + private string password; + + private BigNumber prime = BigNumber.get_rfc3526_prime_3072 (); + private BigNumber generator; + private BigNumber multiplier; + + private BigNumber local_privkey; + private BigNumber local_pubkey; + + private BigNumber? remote_pubkey; + private Bytes? salt; + + private BigNumber? password_hash; + private BigNumber? password_verifier; + + private BigNumber? common_secret; + private BigNumber? premaster_secret; + private Bytes? _key; + private Bytes? _key_proof; + private Bytes? _key_proof_hash; + + private BigNumberContext bn_ctx = new BigNumberContext.secure (); + + public SRPClientSession (string username, string password) { + this.username = username; + this.password = password; + + uint8 raw_gen = 5; + generator = new BigNumber.from_native ((uint8[]) &raw_gen); + multiplier = new HashBuilder () + .add_number_padded (prime) + .add_number_padded (generator) + .build_number (); + + uint8 raw_local_privkey[128]; + Rng.generate (raw_local_privkey); + local_privkey = new BigNumber.from_big_endian (raw_local_privkey); + + local_pubkey = new BigNumber (); + BigNumber.mod_exp (local_pubkey, generator, local_privkey, prime, bn_ctx); + } + + public void process (Bytes raw_remote_pubkey, Bytes salt) throws Error { + remote_pubkey = new BigNumber.from_big_endian (raw_remote_pubkey.get_data ()); + var rem = new BigNumber (); + BigNumber.mod (rem, remote_pubkey, prime, bn_ctx); + if (rem.is_zero ()) + throw new Error.INVALID_ARGUMENT ("Malformed remote public key"); + + this.salt = salt; + + password_hash = compute_password_hash (salt); + password_verifier = compute_password_verifier (password_hash); + + common_secret = compute_common_secret (remote_pubkey); + premaster_secret = compute_premaster_secret (common_secret, remote_pubkey, password_hash, + password_verifier); + _key = compute_session_key (premaster_secret); + _key_proof = compute_session_key_proof (_key, remote_pubkey, salt); + _key_proof_hash = compute_session_key_proof_hash (_key_proof, _key); + } + + public void verify_proof (Bytes proof) throws Error { + size_t size = proof.get_size (); + if (size != _key_proof_hash.get_size ()) + throw new Error.INVALID_ARGUMENT ("Invalid proof size"); + + if (Crypto.memcmp (proof.get_data (), _key_proof_hash.get_data (), size) != 0) + throw new Error.INVALID_ARGUMENT ("Invalid proof"); + } + + private BigNumber compute_password_hash (Bytes salt) { + return new HashBuilder () + .add_bytes (salt) + .add_bytes (new HashBuilder () + .add_string (username) + .add_string (":") + .add_string (password) + .build_digest ()) + .build_number (); + } + + private BigNumber compute_password_verifier (BigNumber password_hash) { + var verifier = new BigNumber (); + BigNumber.mod_exp (verifier, generator, password_hash, prime, bn_ctx); + return verifier; + } + + private BigNumber compute_common_secret (BigNumber remote_pubkey) { + return new HashBuilder () + .add_number_padded (local_pubkey) + .add_number_padded (remote_pubkey) + .build_number (); + } + + private BigNumber compute_premaster_secret (BigNumber common_secret, BigNumber remote_pubkey, + BigNumber password_hash, BigNumber password_verifier) { + var val = new BigNumber (); + + BigNumber.mul (val, multiplier, password_verifier, bn_ctx); + var baze = new BigNumber (); + BigNumber.sub (baze, remote_pubkey, val); + + var exp = new BigNumber (); + BigNumber.mul (val, common_secret, password_hash, bn_ctx); + BigNumber.add (exp, local_privkey, val); + + BigNumber.mod_exp (val, baze, exp, prime, bn_ctx); + + return val; + } + + private static Bytes compute_session_key (BigNumber premaster_secret) { + return new HashBuilder () + .add_number (premaster_secret) + .build_digest (); + } + + private Bytes compute_session_key_proof (Bytes session_key, BigNumber remote_pubkey, Bytes salt) { + Bytes prime_hash = new HashBuilder ().add_number (prime).build_digest (); + Bytes generator_hash = new HashBuilder ().add_number (generator).build_digest (); + uint8 prime_and_generator_xored[64]; + unowned uint8[] left = prime_hash.get_data (); + unowned uint8[] right = generator_hash.get_data (); + for (var i = 0; i != prime_and_generator_xored.length; i++) + prime_and_generator_xored[i] = left[i] ^ right[i]; + + return new HashBuilder () + .add_data (prime_and_generator_xored) + .add_bytes (new HashBuilder ().add_string (username).build_digest ()) + .add_bytes (salt) + .add_number (local_pubkey) + .add_number (remote_pubkey) + .add_bytes (session_key) + .build_digest (); + } + + private Bytes compute_session_key_proof_hash (Bytes key_proof, Bytes key) { + return new HashBuilder () + .add_number (local_pubkey) + .add_bytes (key_proof) + .add_bytes (key) + .build_digest (); + } + + private class HashBuilder { + private Checksum checksum = new Checksum (SHA512); + + public unowned HashBuilder add_number (BigNumber val) { + var buf = new uint8[val.num_bytes ()]; + val.to_big_endian (buf); + return add_data (buf); + } + + public unowned HashBuilder add_number_padded (BigNumber val) { + uint8 buf[384]; + val.to_big_endian_padded (buf); + return add_data (buf); + } + + public unowned HashBuilder add_string (string val) { + return add_data (val.data); + } + + public unowned HashBuilder add_bytes (Bytes val) { + return add_data (val.get_data ()); + } + + public unowned HashBuilder add_data (uint8[] val) { + checksum.update (val, val.length); + return this; + } + + public Bytes build_digest () { + var buf = new uint8[64]; + size_t len = buf.length; + checksum.get_digest (buf, ref len); + return new Bytes.take ((owned) buf); + } + + public BigNumber build_number () { + uint8 buf[64]; + size_t len = buf.length; + checksum.get_digest (buf, ref len); + return new BigNumber.from_big_endian (buf); + } + } + } + } + + public interface PairingTransport : Object { + public signal void close (Error? error); + public signal void message (ObjectReader reader); + + public abstract async void open (Cancellable? cancellable) throws Error, IOError; + public abstract void cancel (); + + public abstract ObjectBuilder make_object_builder (); + public abstract void post (Bytes message); + } + + public class XpcPairingTransport : Object, PairingTransport { + public IOStream stream { + get; + construct; + } + + private XpcConnection connection; + + private Cancellable io_cancellable = new Cancellable (); + + public XpcPairingTransport (IOStream stream) { + Object (stream: stream); + } + + construct { + connection = new XpcConnection (stream); + connection.close.connect (on_close); + connection.message.connect (on_message); + } + + public async void open (Cancellable? cancellable) throws Error, IOError { + connection.activate (); + + yield connection.wait_until_ready (cancellable); + } + + public void cancel () { + io_cancellable.cancel (); + + connection.cancel (); + } + + public ObjectBuilder make_object_builder () { + return new XpcObjectBuilder (); + } + + public void post (Bytes msg) { + connection.post.begin ( + new XpcBodyBuilder () + .begin_dictionary () + .set_member_name ("mangledTypeName") + .add_string_value ("RemotePairing.ControlChannelMessageEnvelope") + .set_member_name ("value") + .add_raw_value (msg) + .end_dictionary () + .build (), + io_cancellable); + } + + private void on_close (Error? error) { + close (error); + } + + private void on_message (XpcMessage msg) { + if (msg.body == null) + return; + + var reader = new VariantReader (msg.body); + try { + string type_name = reader.read_member ("mangledTypeName").get_string_value (); + if (type_name != "RemotePairingDevice.ControlChannelMessageEnvelope") + return; + reader.end_member (); + + reader.read_member ("value"); + + message (reader); + } catch (Error e) { + } + } + } + + public class PlainPairingTransport : Object, PairingTransport { + public IOStream stream { + get; + construct; + } + + private BufferedInputStream input; + private OutputStream output; + + private ByteArray pending_output = new ByteArray (); + private bool writing = false; + + private Cancellable io_cancellable = new Cancellable (); + + public PlainPairingTransport (IOStream stream) { + Object (stream: stream); + } + + construct { + input = (BufferedInputStream) Object.new (typeof (BufferedInputStream), + "base-stream", stream.get_input_stream (), + "close-base-stream", false, + "buffer-size", 128 * 1024); + output = stream.get_output_stream (); + } + + public async void open (Cancellable? cancellable) throws Error, IOError { + process_incoming_messages.begin (); + } + + public void cancel () { + io_cancellable.cancel (); + } + + public ObjectBuilder make_object_builder () { + return new JsonObjectBuilder (); + } + + public void post (Bytes msg) { + Bytes raw_msg = new BufferBuilder (BIG_ENDIAN) + .append_string ("RPPairing", StringTerminator.NONE) + .append_uint16 ((uint16) msg.get_size ()) + .append_bytes (msg) + .build (); + pending_output.append (raw_msg.get_data ()); + + if (!writing) { + writing = true; + + var source = new IdleSource (); + source.set_callback (() => { + process_pending_output.begin (); + return false; + }); + source.attach (MainContext.get_thread_default ()); + } + } + + private async void process_incoming_messages () { + try { + while (true) { + size_t header_size = 11; + if (input.get_available () < header_size) + yield fill_until_n_bytes_available (header_size); + + uint8 raw_magic[9]; + input.peek (raw_magic); + string magic = ((string) raw_magic).make_valid (raw_magic.length); + if (magic != "RPPairing") + throw new Error.PROTOCOL ("Invalid message magic: '%s'", magic); + + uint16 body_size = 0; + unowned uint8[] size_buf = ((uint8[]) &body_size)[:2]; + input.peek (size_buf, raw_magic.length); + body_size = uint16.from_big_endian (body_size); + if (body_size < 2) + throw new Error.PROTOCOL ("Invalid message size"); + + size_t full_size = header_size + body_size; + if (input.get_available () < full_size) + yield fill_until_n_bytes_available (full_size); + + var raw_json = new uint8[body_size + 1]; + input.peek (raw_json[:body_size], header_size); + + unowned string json = (string) raw_json; + if (!json.validate ()) + throw new Error.PROTOCOL ("Invalid UTF-8"); + + var reader = new JsonObjectReader (json); + + message (reader); + + input.skip (full_size, io_cancellable); + } + } catch (GLib.Error e) { + } + + close (null); + } + + private async void process_pending_output () { + while (pending_output.len > 0) { + uint8[] batch = pending_output.steal (); + + size_t bytes_written; + try { + yield output.write_all_async (batch, Priority.DEFAULT, io_cancellable, out bytes_written); + } catch (GLib.Error e) { + break; + } + } + + writing = false; + } + + private async void fill_until_n_bytes_available (size_t minimum) throws Error, IOError { + size_t available = input.get_available (); + while (available < minimum) { + if (input.get_buffer_size () < minimum) + input.set_buffer_size (minimum); + + ssize_t n; + try { + n = yield input.fill_async ((ssize_t) (input.get_buffer_size () - available), Priority.DEFAULT, + io_cancellable); + } catch (GLib.Error e) { + throw new Error.TRANSPORT ("Connection closed"); + } + + if (n == 0) + throw new Error.TRANSPORT ("Connection closed"); + + available += n; + } + } + } + + public class DeviceOptions { + public bool allows_pair_setup; + public bool allows_pinless_pairing; + public bool allows_promptless_automation_pairing_upgrade; + public bool allows_sharing_sensitive_info; + public bool allows_incoming_tunnel_connections; + + public string to_string () { + return "DeviceOptions { " + + @"allows_pair_setup: $allows_pair_setup, " + + @"allows_pinless_pairing: $allows_pinless_pairing " + + @"allows_promptless_automation_pairing_upgrade: $allows_promptless_automation_pairing_upgrade " + + @"allows_sharing_sensitive_info: $allows_sharing_sensitive_info " + + @"allows_incoming_tunnel_connections: $allows_incoming_tunnel_connections " + + "}"; + } + } + + public class DeviceInfo { + public string name; + public string model; + public string udid; + public uint64 ecid; + public Plist kvs; + + public string to_string () { + return @"DeviceInfo { name: \"$name\", model: \"$model\", udid: \"$udid\" }"; + } + } + + private class PairingParamsBuilder { + private BufferBuilder builder = new BufferBuilder (LITTLE_ENDIAN); + + public unowned PairingParamsBuilder add_method (uint8 method) { + begin_param (METHOD, 1) + .append_uint8 (method); + + return this; + } + + public unowned PairingParamsBuilder add_identifier (string identifier) { + begin_param (IDENTIFIER, identifier.data.length) + .append_data (identifier.data); + + return this; + } + + public unowned PairingParamsBuilder add_public_key (Key key) { + return add_raw_public_key (get_raw_public_key (key)); + } + + public unowned PairingParamsBuilder add_raw_public_key (Bytes key) { + return add_blob (PUBLIC_KEY, key); + } + + public unowned PairingParamsBuilder add_proof (Bytes proof) { + return add_blob (PROOF, proof); + } + + public unowned PairingParamsBuilder add_encrypted_data (Bytes bytes) { + return add_blob (ENCRYPTED_DATA, bytes); + } + + public unowned PairingParamsBuilder add_state (uint8 state) { + begin_param (STATE, 1) + .append_uint8 (state); + + return this; + } + + public unowned PairingParamsBuilder add_signature (Bytes signature) { + return add_blob (SIGNATURE, signature); + } + + public unowned PairingParamsBuilder add_info (Bytes info) { + return add_blob (INFO, info); + } + + private unowned PairingParamsBuilder add_blob (PairingParamType type, Bytes blob) { + unowned uint8[] data = blob.get_data (); + + uint cursor = 0; + do { + uint n = uint.min (data.length - cursor, uint8.MAX); + begin_param (type, n) + .append_data (data[cursor:cursor + n]); + cursor += n; + } while (cursor != data.length); + + return this; + } + + private unowned BufferBuilder begin_param (PairingParamType type, size_t size) { + return builder + .append_uint8 (type) + .append_uint8 ((uint8) size); + } + + public Bytes build () { + return builder.build (); + } + } + + private class PairingParamsParser { + private Buffer buf; + private size_t cursor = 0; + private EnumClass param_type_class; + + public static Variant parse (uint8[] data) throws Error { + var parser = new PairingParamsParser (new Bytes.static (data)); + return parser.read_params (); + } + + private PairingParamsParser (Bytes bytes) { + this.buf = new Buffer (bytes, LITTLE_ENDIAN); + this.param_type_class = (EnumClass) typeof (PairingParamType).class_ref (); + } + + private Variant read_params () throws Error { + var byte_array = new VariantType.array (VariantType.BYTE); + + var parameters = new Gee.HashMap (); + size_t size = buf.bytes.get_size (); + while (cursor != size) { + var raw_type = read_raw_uint8 (); + unowned EnumValue? type_enum_val = param_type_class.get_value (raw_type); + if (type_enum_val == null) + throw new Error.INVALID_ARGUMENT ("Unsupported pairing parameter type (0x%x)", raw_type); + var type = (PairingParamType) raw_type; + unowned string key = type_enum_val.value_nick; + + var val_size = read_raw_uint8 (); + Bytes val_bytes = read_raw_bytes (val_size); + + Variant val; + switch (type) { + case STATE: + case ERROR: + if (val_bytes.length != 1) { + throw new Error.INVALID_ARGUMENT ("Invalid value for '%s': length=%d", + key, val_bytes.length); + } + val = new Variant.byte (val_bytes[0]); + break; + case RETRY_DELAY: { + uint16 delay; + switch (val_bytes.length) { + case 1: + delay = val_bytes[0]; + break; + case 2: + delay = new Buffer (val_bytes, LITTLE_ENDIAN).read_uint16 (0); + break; + default: + throw new Error.INVALID_ARGUMENT ("Invalid value for 'retry-delay'"); + } + val = new Variant.uint16 (delay); + break; + } + default: { + var val_bytes_copy = new Bytes (val_bytes.get_data ()); + val = Variant.new_from_data (byte_array, val_bytes_copy.get_data (), true, val_bytes_copy); + break; + } + } + + Variant? existing_val = parameters[key]; + if (existing_val != null) { + if (!existing_val.is_of_type (byte_array)) + throw new Error.INVALID_ARGUMENT ("Unable to merge '%s' keys: unsupported type", key); + Bytes part1 = existing_val.get_data_as_bytes (); + Bytes part2 = val.get_data_as_bytes (); + var combined = new ByteArray.sized ((uint) (part1.get_size () + part2.get_size ())); + combined.append (part1.get_data ()); + combined.append (part2.get_data ()); + val = Variant.new_from_data (byte_array, combined.data, true, (owned) combined); + } + + parameters[key] = val; + } + + var builder = new VariantBuilder (VariantType.VARDICT); + foreach (var e in parameters.entries) + builder.add ("{sv}", e.key, e.value); + return builder.end (); + } + + private uint8 read_raw_uint8 () throws Error { + check_available (sizeof (uint8)); + var result = buf.read_uint8 (cursor); + cursor += sizeof (uint8); + return result; + } + + private Bytes read_raw_bytes (size_t n) throws Error { + check_available (n); + Bytes result = buf.bytes[cursor:cursor + n]; + cursor += n; + return result; + } + + private void check_available (size_t required) throws Error { + size_t available = buf.bytes.get_size () - cursor; + if (available < required) + throw new Error.INVALID_ARGUMENT ("Invalid pairing parameters: truncated"); + } + } + + private enum PairingParamType { + METHOD, + IDENTIFIER, + SALT, + PUBLIC_KEY, + PROOF, + ENCRYPTED_DATA, + STATE, + ERROR, + RETRY_DELAY /* = 8 */, + SIGNATURE = 10, + INFO = 17, + } + + public class OpackBuilder { + protected BufferBuilder builder = new BufferBuilder (LITTLE_ENDIAN); + private Gee.Deque scopes = new Gee.ArrayQueue (); + + public OpackBuilder () { + push_scope (new Scope (ROOT)); + } + + public unowned OpackBuilder begin_dictionary () { + begin_value (); + + size_t type_offset = builder.offset; + builder.append_uint8 (0x00); + + push_scope (new CollectionScope (type_offset)); + + return this; + } + + public unowned OpackBuilder set_member_name (string name) { + return add_string_value (name); + } + + public unowned OpackBuilder end_dictionary () { + CollectionScope scope = pop_scope (); + + size_t n = scope.num_values / 2; + if (n < 0xf) { + builder.write_uint8 (scope.type_offset, 0xe0 | n); + } else { + builder + .write_uint8 (scope.type_offset, 0xef) + .append_uint8 (0x03); + } + + return this; + } + + public unowned OpackBuilder add_string_value (string val) { + begin_value (); + + size_t len = val.length; + + if (len > uint32.MAX) { + builder + .append_uint8 (0x6f) + .append_string (val, StringTerminator.NUL); + + return this; + } + + if (len <= 0x20) + builder.append_uint8 ((uint8) (0x40 + len)); + else if (len <= uint8.MAX) + builder.append_uint8 (0x61).append_uint8 ((uint8) len); + else if (len <= uint16.MAX) + builder.append_uint8 (0x62).append_uint16 ((uint16) len); + else if (len <= 0xffffff) + builder.append_uint8 (0x63).append_uint8 ((uint8) (len & 0xff)).append_uint16 ((uint16) (len >> 8)); + else + builder.append_uint8 (0x64).append_uint32 ((uint32) len); + + builder.append_string (val, StringTerminator.NONE); + + return this; + } + + public unowned OpackBuilder add_data_value (Bytes val) { + begin_value (); + + size_t size = val.get_size (); + if (size <= 0x20) + builder.append_uint8 ((uint8) (0x70 + size)); + else if (size <= uint8.MAX) + builder.append_uint8 (0x91).append_uint8 ((uint8) size); + else if (size <= uint16.MAX) + builder.append_uint8 (0x92).append_uint16 ((uint16) size); + else if (size <= 0xffffff) + builder.append_uint8 (0x93).append_uint8 ((uint8) (size & 0xff)).append_uint16 ((uint16) (size >> 8)); + else + builder.append_uint8 (0x94).append_uint32 ((uint32) size); + + builder.append_bytes (val); + + return this; + } + + private unowned OpackBuilder begin_value () { + peek_scope ().num_values++; + return this; + } + + public Bytes build () { + return builder.build (); + } + + private void push_scope (Scope scope) { + scopes.offer_tail (scope); + } + + private Scope peek_scope () { + return scopes.peek_tail (); + } + + private T pop_scope () { + return (T) scopes.poll_tail (); + } + + private class Scope { + public Kind kind; + public size_t num_values = 0; + + public enum Kind { + ROOT, + COLLECTION, + } + + public Scope (Kind kind) { + this.kind = kind; + } + } + + private class CollectionScope : Scope { + public size_t type_offset; + + public CollectionScope (size_t type_offset) { + base (COLLECTION); + this.type_offset = type_offset; + } + } + } + + public sealed class TunnelConnection : Object, AsyncInitable { + public InetSocketAddress address { + get; + construct; + } + + public TunnelKey local_keypair { + get; + construct; + } + + public TunnelKey remote_pubkey { + get; + construct; + } + + public uint16 remote_rsd_port { + get { + return _remote_rsd_port; + } + } + + private Promise established = new Promise (); + + private Stream? control_stream; + private string? local_ipv6_address; + private string? local_ipv6_netmask; + private string? remote_ipv6_address; + private uint16 _remote_rsd_port; + private uint16 mtu; + private bool netif_added = false; + private LWIP.NetworkInterface netif; + + private Gee.Map streams = new Gee.HashMap (Numeric.int64_hash, Numeric.int64_equal); + private Gee.Queue rx_datagrams = new Gee.ArrayQueue (); + private Gee.Queue tx_datagrams = new Gee.ArrayQueue (); + + private SocketSource? rx_source; + private uint8[] rx_buf = new uint8[MAX_UDP_PAYLOAD_SIZE]; + private uint8[] tx_buf = new uint8[MAX_UDP_PAYLOAD_SIZE]; + private Source? write_idle; + private Source? expiry_timer; + + private Socket socket; + private uint8[] raw_local_address; + private NGTcp2.Connection? connection; + private NGTcp2.Crypto.ConnectionRef connection_ref; + private OpenSSL.SSLContext ssl_ctx; + private OpenSSL.SSL ssl; + + private MainContext main_context; + + private Cancellable io_cancellable = new Cancellable (); + + private const string ALPN = "\x1bRemotePairingTunnelProtocol"; + private const size_t PREFERRED_MTU = 1420; + private const size_t MAX_UDP_PAYLOAD_SIZE = 1452; + private const size_t MAX_QUIC_DATAGRAM_SIZE = 14000; + private const NGTcp2.Duration KEEP_ALIVE_TIMEOUT = 15ULL * NGTcp2.SECONDS; + + public static async TunnelConnection open (InetSocketAddress address, TunnelKey local_keypair, TunnelKey remote_pubkey, + Cancellable? cancellable = null) throws Error, IOError { + var connection = new TunnelConnection (address, local_keypair, remote_pubkey); + + try { + yield connection.init_async (Priority.DEFAULT, cancellable); + } catch (GLib.Error e) { + throw_api_error (e); + } + + return connection; + } + + private TunnelConnection (InetSocketAddress address, TunnelKey local_keypair, TunnelKey remote_pubkey) { + Object ( + address: address, + local_keypair: local_keypair, + remote_pubkey: remote_pubkey + ); + } + + static construct { + LWIP.Runtime.init (() => {}); + } + + construct { + connection_ref.get_conn = conn_ref => { + TunnelConnection * self = conn_ref.user_data; + return self->connection; + }; + connection_ref.user_data = this; + + ssl_ctx = new OpenSSL.SSLContext (OpenSSL.SSLMethod.tls_client ()); + NGTcp2.Crypto.Quictls.configure_client_context (ssl_ctx); + ssl_ctx.use_certificate (make_certificate (local_keypair.handle)); + ssl_ctx.use_private_key (local_keypair.handle); + + ssl = new OpenSSL.SSL (ssl_ctx); + ssl.set_app_data (&connection_ref); + ssl.set_connect_state (); + ssl.set_alpn_protos (ALPN.data); + ssl.set_quic_transport_version (OpenSSL.TLSExtensionType.quic_transport_parameters); + + main_context = MainContext.ref_thread_default (); + } + + public override void dispose () { + cancel (); + + base.dispose (); + } + + private async bool init_async (int io_priority, Cancellable? cancellable) throws Error, IOError { + uint8[] raw_remote_address; + try { + socket = new Socket (IPV6, DATAGRAM, UDP); + socket.connect (address, cancellable); + + raw_local_address = address_to_native (socket.get_local_address ()); + raw_remote_address = address_to_native (address); + } catch (GLib.Error e) { + throw new Error.TRANSPORT ("%s", e.message); + } + + var dcid = make_connection_id (NGTcp2.MIN_INITIAL_DCIDLEN); + var scid = make_connection_id (NGTcp2.MIN_INITIAL_DCIDLEN); + + var path = NGTcp2.Path () { + local = NGTcp2.Address () { addr = raw_local_address }, + remote = NGTcp2.Address () { addr = raw_remote_address }, + }; + + var callbacks = NGTcp2.Callbacks () { + get_new_connection_id = on_get_new_connection_id, + extend_max_local_streams_bidi = (conn, max_streams, user_data) => { + TunnelConnection * self = user_data; + return self->on_extend_max_local_streams_bidi (max_streams); + }, + stream_close = (conn, flags, stream_id, app_error_code, user_data, stream_user_data) => { + TunnelConnection * self = user_data; + return self->on_stream_close (flags, stream_id, app_error_code); + }, + recv_stream_data = (conn, flags, stream_id, offset, data, user_data, stream_user_data) => { + TunnelConnection * self = user_data; + return self->on_recv_stream_data (flags, stream_id, offset, data); + }, + recv_datagram = (conn, flags, data, user_data) => { + TunnelConnection * self = user_data; + return self->on_recv_datagram (flags, data); + }, + rand = on_rand, + client_initial = NGTcp2.Crypto.client_initial_cb, + recv_crypto_data = NGTcp2.Crypto.recv_crypto_data_cb, + encrypt = NGTcp2.Crypto.encrypt_cb, + decrypt = NGTcp2.Crypto.decrypt_cb, + hp_mask = NGTcp2.Crypto.hp_mask_cb, + recv_retry = NGTcp2.Crypto.recv_retry_cb, + update_key = NGTcp2.Crypto.update_key_cb, + delete_crypto_aead_ctx = NGTcp2.Crypto.delete_crypto_aead_ctx_cb, + delete_crypto_cipher_ctx = NGTcp2.Crypto.delete_crypto_cipher_ctx_cb, + get_path_challenge_data = NGTcp2.Crypto.get_path_challenge_data_cb, + version_negotiation = NGTcp2.Crypto.version_negotiation_cb, + }; + + var settings = NGTcp2.Settings.make_default (); + settings.initial_ts = make_timestamp (); + settings.max_tx_udp_payload_size = MAX_UDP_PAYLOAD_SIZE; + settings.handshake_timeout = 5ULL * NGTcp2.SECONDS; + + var transport_params = NGTcp2.TransportParams.make_default (); + transport_params.max_datagram_frame_size = MAX_QUIC_DATAGRAM_SIZE; + transport_params.max_idle_timeout = 30ULL * NGTcp2.SECONDS; + transport_params.initial_max_data = 1048576; + transport_params.initial_max_stream_data_bidi_local = 1048576; + + NGTcp2.Connection.make_client (out connection, dcid, scid, path, NGTcp2.ProtocolVersion.V1, callbacks, + settings, transport_params, null, this); + connection.set_tls_native_handle (ssl); + connection.set_keep_alive_timeout (KEEP_ALIVE_TIMEOUT); + + rx_source = socket.create_source (IOCondition.IN, io_cancellable); + rx_source.set_callback (on_socket_readable); + rx_source.attach (main_context); + + process_pending_writes (); + + yield established.future.wait_async (cancellable); + + return true; + } + + private void on_control_stream_opened () { + var zeroed_padding_packet = new uint8[1024]; + send_datagram (new Bytes.take ((owned) zeroed_padding_packet)); + + send_request (Json.to_string ( + new Json.Builder () + .begin_object () + .set_member_name ("type") + .add_string_value ("clientHandshakeRequest") + .set_member_name ("mtu") + .add_int_value (PREFERRED_MTU) + .end_object () + .get_root (), false)); + } + + private void on_control_stream_response (string json) throws Error { + Json.Reader reader; + try { + reader = new Json.Reader (Json.from_string (json)); + } catch (GLib.Error e) { + throw new Error.PROTOCOL ("Invalid response JSON"); + } + + reader.read_member ("clientParameters"); + + reader.read_member ("address"); + string? address = reader.get_string_value (); + reader.end_member (); + + reader.read_member ("netmask"); + string? netmask = reader.get_string_value (); + reader.end_member (); + + reader.read_member ("mtu"); + int64 raw_mtu = reader.get_int_value (); + reader.end_member (); + + reader.end_member (); + + reader.read_member ("serverAddress"); + string? server_address = reader.get_string_value (); + reader.end_member (); + + reader.read_member ("serverRSDPort"); + int64 server_rsd_port = reader.get_int_value (); + reader.end_member (); + + GLib.Error? error = reader.get_error (); + if (error != null) + throw new Error.PROTOCOL ("Invalid response: %s", error.message); + + local_ipv6_address = address; + local_ipv6_netmask = netmask; + remote_ipv6_address = server_address; + _remote_rsd_port = (uint16) server_rsd_port; + mtu = (uint16) raw_mtu; + + LWIP.Runtime.schedule (setup_network_interface); + netif_added = true; + + established.resolve (true); + } + + private void setup_network_interface () { + LWIP.NetworkInterface.add_noaddr (ref netif, this, on_netif_init); + netif.set_up (); + } + + private void ensure_network_interface_removed () { + if (!netif_added) + return; + netif_added = false; + + ref (); + LWIP.Runtime.schedule (remove_network_interface); + } + + private void remove_network_interface () { + netif.remove (); + + unref (); + } + + public async IOStream open_connection (uint16 port, Cancellable? cancellable = null) throws Error, IOError { + return yield TcpConnection.open (this, remote_ipv6_address, port, cancellable); + } + + private static LWIP.ErrorCode on_netif_init (LWIP.NetworkInterface netif) { + TunnelConnection * self = netif.state; + + netif.mtu = self->mtu; + netif.output_ip6 = on_netif_output_ip6; + + int8 chosen_index = -1; + netif.add_ip6_address (LWIP.IP6Address.parse (self->local_ipv6_address), &chosen_index); + netif.ip6_addr_set_state (chosen_index, PREFERRED); + + return OK; + } + + private static LWIP.ErrorCode on_netif_output_ip6 (LWIP.NetworkInterface netif, LWIP.PacketBuffer pbuf, + LWIP.IP6Address address) { + TunnelConnection * self = netif.state; + + var buffer = new uint8[pbuf.tot_len]; + unowned uint8[] packet = pbuf.get_contiguous (buffer, pbuf.tot_len); + var datagram = new Bytes (packet[:pbuf.tot_len]); + + var source = new IdleSource (); + source.set_callback (() => { + self->send_datagram (datagram); + return Source.REMOVE; + }); + source.attach (self->main_context); + + return OK; + } + + public void cancel () { + connection = null; + + io_cancellable.cancel (); + + if (rx_source != null) { + rx_source.destroy (); + rx_source = null; + } + + if (write_idle != null) { + write_idle.destroy (); + write_idle = null; + } + + if (expiry_timer != null) { + expiry_timer.destroy (); + expiry_timer = null; + } + + ensure_network_interface_removed (); + } + + private void send_request (string json) { + unowned uint8[] body = json.data; + Bytes request = new BufferBuilder (BIG_ENDIAN) + .append_string ("CDTunnel", StringTerminator.NONE) + .append_uint16 ((uint16) body.length) + .append_data (body) + .build (); + control_stream.send (request.get_data ()); + } + + private void on_stream_data_available (Stream stream, uint8[] data, out size_t consumed) { + if (stream != control_stream || established.future.ready) { + consumed = data.length; + return; + } + + consumed = 0; + + if (data.length < 12) + return; + + var buf = new Buffer (new Bytes.static (data), BIG_ENDIAN); + + try { + string magic = buf.read_fixed_string (0, 8); + if (magic != "CDTunnel") + throw new Error.PROTOCOL ("Invalid magic"); + + size_t body_size = buf.read_uint16 (8); + size_t body_available = data.length - 10; + if (body_available < body_size) + return; + + var raw_json = new uint8[body_size + 1]; + Memory.copy (raw_json, data + 10, body_size); + + unowned string json = (string) raw_json; + if (!json.validate ()) + throw new Error.PROTOCOL ("Invalid UTF-8"); + + on_control_stream_response (json); + + consumed = 10 + body_size; + } catch (Error e) { + if (!established.future.ready) + established.reject (e); + } + } + + private void send_datagram (Bytes datagram) { + if (connection == null) + return; + tx_datagrams.offer (datagram); + process_pending_writes (); + } + + private bool on_socket_readable (DatagramBased datagram_based, IOCondition condition) { + try { + SocketAddress remote_address; + ssize_t n = socket.receive_from (out remote_address, rx_buf, io_cancellable); + + uint8[] raw_remote_address = address_to_native (remote_address); + + var path = NGTcp2.Path () { + local = NGTcp2.Address () { addr = raw_local_address }, + remote = NGTcp2.Address () { addr = raw_remote_address }, + }; + + unowned uint8[] data = rx_buf[:n]; + + connection.read_packet (path, null, data, make_timestamp ()); + } catch (GLib.Error e) { + return Source.REMOVE; + } finally { + process_pending_writes (); + } + + return Source.CONTINUE; + } + + private void process_pending_writes () { + if (write_idle != null) + return; + + var source = new IdleSource (); + source.set_callback (() => { + write_idle = null; + do_process_pending_writes (); + return Source.REMOVE; + }); + source.attach (main_context); + write_idle = source; + } + + private void do_process_pending_writes () { + var ts = make_timestamp (); + + var pi = NGTcp2.PacketInfo (); + Gee.Iterator stream_iter = streams.values.iterator (); + while (true) { + ssize_t n = -1; + + Bytes? datagram = tx_datagrams.peek (); + if (datagram != null) { + int accepted = -1; + n = connection.write_datagram (null, null, tx_buf, &accepted, NGTcp2.WriteStreamFlags.MORE, 0, + datagram.get_data (), ts); + if (accepted > 0) + tx_datagrams.poll (); + } else { + Stream? stream = null; + unowned uint8[]? data = null; + + while (stream == null && stream_iter.next ()) { + Stream s = stream_iter.get (); + uint64 len = s.tx_buf.len; + uint64 limit = 0; + if (len != 0 && (limit = connection.get_max_stream_data_left (s.id)) != 0) { + stream = s; + data = s.tx_buf.data[:(int) uint64.min (len, limit)]; + break; + } + } + + ssize_t datalen = 0; + n = connection.write_stream (null, &pi, tx_buf, &datalen, NGTcp2.WriteStreamFlags.MORE, + (stream != null) ? stream.id : -1, data, ts); + if (datalen > 0) + stream.tx_buf.remove_range (0, (uint) datalen); + } + + if (n == 0) + break; + if (n == NGTcp2.ErrorCode.WRITE_MORE) + continue; + if (n < 0) + break; + + try { + socket.send (tx_buf[:n], io_cancellable); + } catch (GLib.Error e) { + continue; + } + } + + if (expiry_timer != null) { + expiry_timer.destroy (); + expiry_timer = null; + } + + NGTcp2.Timestamp expiry = connection.get_expiry (); + if (expiry == uint64.MAX) + return; + + NGTcp2.Timestamp now = make_timestamp (); + + uint delta_msec; + if (expiry > now) { + uint64 delta_nsec = expiry - now; + delta_msec = (uint) (delta_nsec / 1000000ULL); + } else { + delta_msec = 1; + } + + var source = new TimeoutSource (delta_msec); + source.set_callback (on_expiry); + source.attach (main_context); + expiry_timer = source; + } + + private bool on_expiry () { + int res = connection.handle_expiry (make_timestamp ()); + if (res != 0) { + cancel (); + return Source.REMOVE; + } + + process_pending_writes (); + + return Source.REMOVE; + } + + private static int on_get_new_connection_id (NGTcp2.Connection conn, out NGTcp2.ConnectionID cid, uint8[] token, + size_t cidlen, void * user_data) { + cid = make_connection_id (cidlen); + + OpenSSL.Rng.generate (token[:NGTcp2.STATELESS_RESET_TOKENLEN]); + + return 0; + } + + private int on_extend_max_local_streams_bidi (uint64 max_streams) { + if (control_stream == null) { + control_stream = open_bidi_stream (); + + var source = new IdleSource (); + source.set_callback (() => { + on_control_stream_opened (); + return Source.REMOVE; + }); + source.attach (main_context); + } + + return 0; + } + + private int on_stream_close (uint32 flags, int64 stream_id, uint64 app_error_code) { + return 0; + } + + private int on_recv_stream_data (uint32 flags, int64 stream_id, uint64 offset, uint8[] data) { + Stream? stream = streams[stream_id]; + if (stream != null) + stream.on_recv (data); + + return 0; + } + + private int on_recv_datagram (uint32 flags, uint8[] data) { + if (netif_added) { + lock (rx_datagrams) + rx_datagrams.offer (new Bytes (data)); + LWIP.Runtime.schedule (process_next_rx_datagram); + } + + return 0; + } + + private void process_next_rx_datagram () { + Bytes datagram; + lock (rx_datagrams) + datagram = rx_datagrams.poll (); + + var pbuf = LWIP.PacketBuffer.alloc (RAW, (uint16) datagram.get_size (), POOL); + pbuf.take (datagram.get_data ()); + + if (netif.input (pbuf, netif) != OK) + pbuf.free (); + } + + private static void on_rand (uint8[] dest, NGTcp2.RNGContext rand_ctx) { + OpenSSL.Rng.generate (dest); + } + + private static uint8[] address_to_native (SocketAddress address) throws GLib.Error { + var size = address.get_native_size (); + var buf = new uint8[size]; + address.to_native (buf, size); + return buf; + } + + private static NGTcp2.ConnectionID make_connection_id (size_t len) { + var cid = NGTcp2.ConnectionID () { + datalen = len, + }; + + NGTcp2.ConnectionID * mutable_cid = &cid; + OpenSSL.Rng.generate (mutable_cid->data[:len]); + + return cid; + } + + private static NGTcp2.Timestamp make_timestamp () { + return get_monotonic_time () * NGTcp2.MICROSECONDS; + } + + private static X509 make_certificate (Key keypair) { + var cert = new X509 (); + cert.get_serial_number ().set_uint64 (1); + cert.get_not_before ().adjust (0); + cert.get_not_after ().adjust (5260000); + + unowned X509.Name name = cert.get_subject_name (); + cert.set_issuer_name (name); + cert.set_pubkey (keypair); + + var mc = new MessageDigestContext (); + mc.digest_sign_init (null, null, null, keypair); + cert.sign_ctx (mc); + + return cert; + } + + private Stream open_bidi_stream () { + int64 id; + connection.open_bidi_stream (out id, null); + + var stream = new Stream (this, id); + streams[id] = stream; + + return stream; + } + + private class Stream { + public int64 id; + + private weak TunnelConnection parent; + + public ByteArray rx_buf = new ByteArray.sized (256); + public ByteArray tx_buf = new ByteArray.sized (128); + + public Stream (TunnelConnection parent, int64 id) { + this.parent = parent; + this.id = id; + } + + public void send (uint8[] data) { + tx_buf.append (data); + parent.process_pending_writes (); + } + + public void on_recv (uint8[] data) { + rx_buf.append (data); + + size_t consumed; + parent.on_stream_data_available (this, rx_buf.data, out consumed); + + if (consumed != 0) + rx_buf.remove_range (0, (uint) consumed); + } + } + + private class TcpConnection : IOStream, AsyncInitable { + public TunnelConnection tunnel_connection { + get; + construct; + } + + public string address { + get; + construct; + } + + public uint16 port { + get; + construct; + } + + public State state { + get { + return _state; + } + } + + public override InputStream input_stream { + get { + return _input_stream; + } + } + + public override OutputStream output_stream { + get { + return _output_stream; + } + } + + public IOCondition pending_io { + get { + lock (state) + return events; + } + } + + private Promise established = new Promise (); + + private State _state = CREATED; + private TcpInputStream _input_stream; + private TcpOutputStream _output_stream; + + private LWIP.TcpPcb? pcb; + private IOCondition events = 0; + private ByteArray rx_buf = new ByteArray.sized (64 * 1024); + private ByteArray tx_buf = new ByteArray.sized (64 * 1024); + private size_t rx_bytes_to_acknowledge = 0; + private size_t tx_space_available = 0; + + private Gee.Map sources = new Gee.HashMap (); + + private MainContext main_context; + + public enum State { + CREATED, + OPENING, + OPENED, + CLOSED + } + + public static async TcpConnection open (TunnelConnection tunnel_connection, string address, uint16 port, + Cancellable? cancellable) throws Error, IOError { + var connection = new TcpConnection (tunnel_connection, address, port); + + try { + yield connection.init_async (Priority.DEFAULT, cancellable); + } catch (GLib.Error e) { + throw_api_error (e); + } + + return connection; + } + + private TcpConnection (TunnelConnection tunnel_connection, string address, uint16 port) { + Object ( + tunnel_connection: tunnel_connection, + address: address, + port: port + ); + } + + construct { + _input_stream = new TcpInputStream (this); + _output_stream = new TcpOutputStream (this); + + main_context = MainContext.ref_thread_default (); + } + + public override void dispose () { + stop (); + + base.dispose (); + } + + private async bool init_async (int io_priority, Cancellable? cancellable) throws Error, IOError { + _state = OPENING; + LWIP.Runtime.schedule (do_start); + + try { + yield established.future.wait_async (cancellable); + } catch (GLib.Error e) { + stop (); + throw_api_error (e); + } + + return true; + } + + private void do_start () { + pcb = new LWIP.TcpPcb (V6); + pcb.set_user_data (this); + pcb.set_recv_callback ((user_data, pcb, pbuf, err) => { + TcpConnection * self = user_data; + if (self != null) + self->on_recv (pbuf, err); + return OK; + }); + pcb.set_sent_callback ((user_data, pcb, len) => { + TcpConnection * self = user_data; + if (self != null) + self->on_sent (len); + return OK; + }); + pcb.set_error_callback ((user_data, err) => { + TcpConnection * self = user_data; + if (self != null) + self->on_error (err); + }); + pcb.nagle_disable (); + pcb.bind_netif (tunnel_connection.netif); + + pcb.connect (LWIP.IP6Address.parse (address), port, (user_data, pcb, err) => { + TcpConnection * self = user_data; + if (self != null) + self->on_connect (); + return OK; + }); + } + + private void stop () { + if (_state == CLOSED) + return; + + if (state != CREATED) { + ref (); + LWIP.Runtime.schedule (do_stop); + } + + _state = CLOSED; + } + + private void do_stop () { + if (pcb != null) { + pcb.set_user_data (null); + if (pcb.close () != OK) + pcb.abort (); + pcb = null; + } + + unref (); + } + + private void on_connect () { + lock (state) + tx_space_available = pcb.query_available_send_buffer_space (); + update_events (); + + schedule_on_frida_thread (() => { + _state = OPENED; + + if (!established.future.ready) + established.resolve (true); + + return Source.REMOVE; + }); + } + + private void on_recv (LWIP.PacketBuffer? pbuf, LWIP.ErrorCode err) { + if (pbuf == null) { + schedule_on_frida_thread (() => { + _state = CLOSED; + update_events (); + return Source.REMOVE; + }); + return; + } + + var buffer = new uint8[pbuf.tot_len]; + unowned uint8[] chunk = pbuf.get_contiguous (buffer, pbuf.tot_len); + lock (state) + rx_buf.append (chunk[:pbuf.tot_len]); + update_events (); + + pbuf.free (); + } + + private void on_sent (uint16 len) { + lock (state) + tx_space_available = pcb.query_available_send_buffer_space () - tx_buf.len; + update_events (); + } + + private void on_error (LWIP.ErrorCode err) { + schedule_on_frida_thread (() => { + _state = CLOSED; + update_events (); + + if (!established.future.ready) + established.reject (new Error.TRANSPORT ("%s", strerror (err.to_errno ()))); + + return Source.REMOVE; + }); + } + + public override bool close (GLib.Cancellable? cancellable) throws IOError { + stop (); + return true; + } + + public override async bool close_async (int io_priority, GLib.Cancellable? cancellable) throws IOError { + stop (); + return true; + } + + public void shutdown_rx () throws IOError { + LWIP.Runtime.schedule (do_shutdown_rx); + } + + private void do_shutdown_rx () { + if (pcb == null) + return; + pcb.shutdown (true, false); + } + + public void shutdown_tx () throws IOError { + LWIP.Runtime.schedule (do_shutdown_tx); + } + + private void do_shutdown_tx () { + if (pcb == null) + return; + pcb.shutdown (false, true); + } + + public ssize_t recv (uint8[] buffer) throws IOError { + ssize_t n; + lock (state) { + n = ssize_t.min (buffer.length, rx_buf.len); + if (n != 0) { + Memory.copy (buffer, rx_buf.data, n); + rx_buf.remove_range (0, (uint) n); + rx_bytes_to_acknowledge += n; + } + } + if (n == 0) { + if (_state == CLOSED) + return 0; + throw new IOError.WOULD_BLOCK ("Resource temporarily unavailable"); + } + + update_events (); + + LWIP.Runtime.schedule (do_acknowledge_rx_bytes); + + return n; + } + + private void do_acknowledge_rx_bytes () { + if (pcb == null) + return; + + size_t n; + lock (state) { + n = rx_bytes_to_acknowledge; + rx_bytes_to_acknowledge = 0; + } + + size_t remainder = n; + while (remainder != 0) { + uint16 chunk = (uint16) size_t.min (remainder, uint16.MAX); + pcb.notify_received (chunk); + remainder -= chunk; + } + } + + public ssize_t send (uint8[] buffer) throws IOError { + ssize_t n; + lock (state) { + n = ssize_t.min (buffer.length, (ssize_t) tx_space_available); + if (n != 0) { + tx_buf.append (buffer[:n]); + tx_space_available -= n; + } + } + if (n == 0) + throw new IOError.WOULD_BLOCK ("Resource temporarily unavailable"); + + update_events (); + + LWIP.Runtime.schedule (do_send); + + return n; + } + + private void do_send () { + if (pcb == null) + return; + + size_t available_space = pcb.query_available_send_buffer_space (); + + uint8[]? data = null; + lock (state) { + size_t n = size_t.min (tx_buf.len, available_space); + if (n != 0) { + data = tx_buf.data[:n]; + tx_buf.remove_range (0, (uint) n); + } + } + if (data == null) + return; + + pcb.write (data, COPY); + pcb.output (); + + available_space = pcb.query_available_send_buffer_space (); + lock (state) + tx_space_available = available_space - tx_buf.len; + update_events (); + } + + public void register_source (Source source, IOCondition condition) { + lock (state) + sources[source] = condition | IOCondition.ERR | IOCondition.HUP; + } + + public void unregister_source (Source source) { + lock (state) + sources.unset (source); + } + + private void update_events () { + lock (state) { + IOCondition new_events = 0; + + if (rx_buf.len != 0 || _state == CLOSED) + new_events |= IN; + + if (tx_space_available != 0) + new_events |= OUT; + + events = new_events; + + foreach (var entry in sources.entries) { + unowned Source source = entry.key; + IOCondition c = entry.value; + if ((new_events & c) != 0) + source.set_ready_time (0); + } + } + + notify_property ("pending-io"); + } + + private void schedule_on_frida_thread (owned SourceFunc function) { + var source = new IdleSource (); + source.set_callback ((owned) function); + source.attach (main_context); + } + } + + private class TcpInputStream : InputStream, PollableInputStream { + public weak TcpConnection connection { + get; + construct; + } + + public TcpInputStream (TcpConnection connection) { + Object (connection: connection); + } + + public override bool close (Cancellable? cancellable) throws IOError { + connection.shutdown_rx (); + return true; + } + + public override async bool close_async (int io_priority, Cancellable? cancellable) throws GLib.IOError { + return close (cancellable); + } + + public override ssize_t read (uint8[] buffer, Cancellable? cancellable) throws IOError { + if (!is_readable ()) { + bool done = false; + var mutex = Mutex (); + var cond = Cond (); + + ulong io_handler = connection.notify["pending-io"].connect ((obj, pspec) => { + if (is_readable ()) { + mutex.lock (); + done = true; + cond.signal (); + mutex.unlock (); + } + }); + ulong cancellation_handler = 0; + if (cancellable != null) { + cancellation_handler = cancellable.connect (() => { + mutex.lock (); + done = true; + cond.signal (); + mutex.unlock (); + }); + } + + mutex.lock (); + while (!done) + cond.wait (mutex); + mutex.unlock (); + + if (cancellation_handler != 0) + cancellable.disconnect (cancellation_handler); + connection.disconnect (io_handler); + + cancellable.set_error_if_cancelled (); + } + + return connection.recv (buffer); + } + + public bool can_poll () { + return true; + } + + public bool is_readable () { + return (connection.pending_io & IOCondition.IN) != 0; + } + + public PollableSource create_source (Cancellable? cancellable) { + return new PollableSource.full (this, new TcpIOSource (connection, IOCondition.IN), cancellable); + } + + public ssize_t read_nonblocking_fn (uint8[] buffer) throws GLib.Error { + return connection.recv (buffer); + } + } + + private class TcpOutputStream : OutputStream, PollableOutputStream { + public weak TcpConnection connection { + get; + construct; + } + + public TcpOutputStream (TcpConnection connection) { + Object (connection: connection); + } + + public override bool close (Cancellable? cancellable) throws IOError { + connection.shutdown_tx (); + return true; + } + + public override async bool close_async (int io_priority, Cancellable? cancellable) throws GLib.IOError { + return close (cancellable); + } + + public override bool flush (GLib.Cancellable? cancellable) throws GLib.Error { + return true; + } + + public override async bool flush_async (int io_priority, GLib.Cancellable? cancellable) throws GLib.Error { + return true; + } + + public override ssize_t write (uint8[] buffer, Cancellable? cancellable) throws IOError { + assert_not_reached (); + } + + public bool can_poll () { + return true; + } + + public bool is_writable () { + return (connection.pending_io & IOCondition.OUT) != 0; + } + + public PollableSource create_source (Cancellable? cancellable) { + return new PollableSource.full (this, new TcpIOSource (connection, IOCondition.OUT), cancellable); + } + + public ssize_t write_nonblocking_fn (uint8[]? buffer) throws GLib.Error { + return connection.send (buffer); + } + + public PollableReturn writev_nonblocking_fn (OutputVector[] vectors, out size_t bytes_written) throws GLib.Error { + assert_not_reached (); + } + } + + private class TcpIOSource : Source { + public TcpConnection connection; + public IOCondition condition; + + public TcpIOSource (TcpConnection connection, IOCondition condition) { + this.connection = connection; + this.condition = condition; + + connection.register_source (this, condition); + } + + ~TcpIOSource () { + connection.unregister_source (this); + } + + protected override bool prepare (out int timeout) { + timeout = -1; + return (connection.pending_io & condition) != 0; + } + + protected override bool check () { + return (connection.pending_io & condition) != 0; + } + + protected override bool dispatch (SourceFunc? callback) { + set_ready_time (-1); + + if (callback == null) + return Source.REMOVE; + + return callback (); + } + + protected static bool closure_callback (Closure closure) { + var return_value = Value (typeof (bool)); + + closure.invoke (ref return_value, {}); + + return return_value.get_boolean (); + } + } + } + + public sealed class TunnelKey { + public Key handle; + + public TunnelKey (owned Key handle) { + this.handle = (owned) handle; + } + } + + public class AppService : TrustedService { + public static async AppService open (IOStream stream, Cancellable? cancellable = null) throws Error, IOError { + var service = new AppService (stream); + + try { + yield service.init_async (Priority.DEFAULT, cancellable); + } catch (GLib.Error e) { + throw_api_error (e); + } + + return service; + } + + private AppService (IOStream stream) { + Object (stream: stream); + } + + public async Gee.List enumerate_applications (Cancellable? cancellable = null) throws Error, IOError { + Bytes input = new XpcObjectBuilder () + .begin_dictionary () + .set_member_name ("includeDefaultApps") + .add_bool_value (true) + .set_member_name ("includeRemovableApps") + .add_bool_value (true) + .set_member_name ("includeInternalApps") + .add_bool_value (true) + .set_member_name ("includeHiddenApps") + .add_bool_value (true) + .set_member_name ("includeAppClips") + .add_bool_value (true) + .end_dictionary () + .build (); + var response = yield invoke ("com.apple.coredevice.feature.listapps", input, cancellable); + + var applications = new Gee.ArrayList (); + uint n = response.count_elements (); + for (uint i = 0; i != n; i++) { + response.read_element (i); + + string bundle_identifier = response + .read_member ("bundleIdentifier") + .get_string_value (); + response.end_member (); + + string? bundle_version = null; + if (response.has_member ("bundleVersion")) { + bundle_version = response + .read_member ("bundleVersion") + .get_string_value (); + response.end_member (); + } + + string name = response + .read_member ("name") + .get_string_value (); + response.end_member (); + + string? version = null; + if (response.has_member ("version")) { + version = response + .read_member ("version") + .get_string_value (); + response.end_member (); + } + + string path = response + .read_member ("path") + .get_string_value (); + response.end_member (); + + bool is_first_party = response + .read_member ("isFirstParty") + .get_bool_value (); + response.end_member (); + + bool is_developer_app = response + .read_member ("isDeveloperApp") + .get_bool_value (); + response.end_member (); + + bool is_removable = response + .read_member ("isRemovable") + .get_bool_value (); + response.end_member (); + + bool is_internal = response + .read_member ("isInternal") + .get_bool_value (); + response.end_member (); + + bool is_hidden = response + .read_member ("isHidden") + .get_bool_value (); + response.end_member (); + + bool is_app_clip = response + .read_member ("isAppClip") + .get_bool_value (); + response.end_member (); + + applications.add (new ApplicationInfo () { + bundle_identifier = bundle_identifier, + bundle_version = bundle_version, + name = name, + version = version, + path = path, + is_first_party = is_first_party, + is_developer_app = is_developer_app, + is_removable = is_removable, + is_internal = is_internal, + is_hidden = is_hidden, + is_app_clip = is_app_clip, + }); + + response.end_element (); + } + + return applications; + } + + public async Gee.List enumerate_processes (Cancellable? cancellable = null) throws Error, IOError { + var response = yield invoke ("com.apple.coredevice.feature.listprocesses", null, cancellable); + + var processes = new Gee.ArrayList (); + uint n = response + .read_member ("processTokens") + .count_elements (); + for (uint i = 0; i != n; i++) { + response.read_element (i); + + int64 pid = response + .read_member ("processIdentifier") + .get_int64_value (); + response.end_member (); + + string url = response + .read_member ("executableURL") + .read_member ("relative") + .get_string_value (); + response + .end_member () + .end_member (); + + if (!url.has_prefix ("file://")) + throw new Error.PROTOCOL ("Unsupported URL: %s", url); + + string path = url[7:]; + + processes.add (new ProcessInfo () { + pid = (uint) pid, + path = path, + }); + + response.end_element (); + } + + return processes; + } + + public class ApplicationInfo { + public string bundle_identifier; + public string? bundle_version; + public string name; + public string? version; + public string path; + public bool is_first_party; + public bool is_developer_app; + public bool is_removable; + public bool is_internal; + public bool is_hidden; + public bool is_app_clip; + + public string to_string () { + var summary = new StringBuilder.sized (128); + + summary + .append ("ApplicationInfo {") + .append (@"\n\tbundle_identifier: \"$bundle_identifier\","); + if (bundle_version != null) + summary.append (@"\n\tbundle_version: \"$bundle_version\","); + summary.append (@"\n\tname: \"$name\","); + if (version != null) + summary.append (@"\n\tversion: \"$version\","); + summary + .append (@"\n\tpath: \"$path\",") + .append (@"\n\tis_first_party: $is_first_party,") + .append (@"\n\tis_developer_app: $is_developer_app,") + .append (@"\n\tis_removable: $is_removable,") + .append (@"\n\tis_internal: $is_internal,") + .append (@"\n\tis_hidden: $is_hidden,") + .append (@"\n\tis_app_clip: $is_app_clip,") + .append ("\n}"); + + return summary.str; + } + } + + public class ProcessInfo { + public uint pid; + public string path; + + public string to_string () { + return "ProcessInfo { pid: %u, path: \"%s\" }".printf (pid, path); + } + } + } + + public abstract class TrustedService : Object, AsyncInitable { + public IOStream stream { + get; + construct; + } + + private XpcConnection connection; + + private async bool init_async (int io_priority, Cancellable? cancellable) throws Error, IOError { + connection = new XpcConnection (stream); + connection.activate (); + + return true; + } + + public void close () { + connection.cancel (); + } + + protected async VariantReader invoke (string feature_identifier, Bytes? input = null, Cancellable? cancellable) + throws Error, IOError { + var request = new XpcBodyBuilder () + .begin_dictionary () + .set_member_name ("CoreDevice.featureIdentifier") + .add_string_value (feature_identifier) + .set_member_name ("CoreDevice.action") + .begin_dictionary () + .end_dictionary () + .set_member_name ("CoreDevice.input"); + + if (input != null) + request.add_raw_value (input); + else + request.add_null_value (); + + add_standard_request_values (request); + request.end_dictionary (); + + XpcMessage raw_response = yield connection.request (request.build (), cancellable); + + var response = new VariantReader (raw_response.body); + response.read_member ("CoreDevice.output"); + return response; + } + + public static void add_standard_request_values (ObjectBuilder builder) { + builder + .set_member_name ("CoreDevice.invocationIdentifier") + .add_string_value (Uuid.string_random ().up ()) + .set_member_name ("CoreDevice.CoreDeviceDDIProtocolVersion") + .add_int64_value (0) + .set_member_name ("CoreDevice.coreDeviceVersion") + .begin_dictionary () + .set_member_name ("originalComponentsCount") + .add_int64_value (2) + .set_member_name ("components") + .begin_array () + .add_uint64_value (348) + .add_uint64_value (1) + .add_uint64_value (0) + .add_uint64_value (0) + .add_uint64_value (0) + .end_array () + .set_member_name ("stringValue") + .add_string_value ("348.1") + .end_dictionary () + .set_member_name ("CoreDevice.deviceIdentifier") + .add_string_value (make_host_identifier ()); + } + } + + public sealed class XpcConnection : Object { + public signal void close (Error? error); + public signal void message (XpcMessage msg); + + public IOStream stream { + get; + construct; + } + + public State state { + get; + private set; + default = INACTIVE; + } + + private Error? pending_error; + + private Promise ready = new Promise (); + private XpcMessage? root_helo; + private XpcMessage? reply_helo; + private Gee.Map pending_responses = + new Gee.HashMap (Numeric.uint64_hash, Numeric.uint64_equal); + + private NGHttp2.Session session; + private Stream root_stream; + private Stream reply_stream; + private uint next_message_id = 1; + + private bool is_processing_messages; + + private ByteArray? send_queue; + private Source? send_source; + + private Cancellable io_cancellable = new Cancellable (); + + public enum State { + INACTIVE, + ACTIVE, + CLOSED, + } + + public XpcConnection (IOStream stream) { + Object (stream: stream); + } + + construct { + NGHttp2.SessionCallbacks callbacks; + NGHttp2.SessionCallbacks.make (out callbacks); + + callbacks.set_send_callback ((session, data, flags, user_data) => { + XpcConnection * self = user_data; + return self->on_send (data, flags); + }); + callbacks.set_on_frame_send_callback ((session, frame, user_data) => { + XpcConnection * self = user_data; + return self->on_frame_send (frame); + }); + callbacks.set_on_frame_not_send_callback ((session, frame, lib_error_code, user_data) => { + XpcConnection * self = user_data; + return self->on_frame_not_send (frame, lib_error_code); + }); + callbacks.set_on_data_chunk_recv_callback ((session, flags, stream_id, data, user_data) => { + XpcConnection * self = user_data; + return self->on_data_chunk_recv (flags, stream_id, data); + }); + callbacks.set_on_frame_recv_callback ((session, frame, user_data) => { + XpcConnection * self = user_data; + return self->on_frame_recv (frame); + }); + callbacks.set_on_stream_close_callback ((session, stream_id, error_code, user_data) => { + XpcConnection * self = user_data; + return self->on_stream_close (stream_id, error_code); + }); + + NGHttp2.Option option; + NGHttp2.Option.make (out option); + option.set_no_auto_window_update (true); + option.set_peer_max_concurrent_streams (100); + option.set_no_http_messaging (true); + // option.set_no_http_semantics (true); + option.set_no_closed_streams (true); + + NGHttp2.Session.make_client (out session, callbacks, this, option); + } + + public void activate () { + do_activate.begin (); + } + + private async void do_activate () { + try { + is_processing_messages = true; + process_incoming_messages.begin (); + + session.submit_settings (NGHttp2.Flag.NONE, { + { MAX_CONCURRENT_STREAMS, 100 }, + { INITIAL_WINDOW_SIZE, 1048576 }, + }); + + session.set_local_window_size (NGHttp2.Flag.NONE, 0, 1048576); + + root_stream = make_stream (); + + Bytes header_request = new XpcMessageBuilder (HEADER) + .add_body (new XpcBodyBuilder () + .begin_dictionary () + .end_dictionary () + .build () + ) + .build (); + yield root_stream.submit_data (header_request, io_cancellable); + + Bytes ping_request = new XpcMessageBuilder (PING) + .build (); + yield root_stream.submit_data (ping_request, io_cancellable); + + reply_stream = make_stream (); + + Bytes open_reply_channel_request = new XpcMessageBuilder (HEADER) + .add_flags (HEADER_OPENS_REPLY_CHANNEL) + .build (); + yield reply_stream.submit_data (open_reply_channel_request, io_cancellable); + } catch (GLib.Error e) { + if (e is Error && pending_error == null) + pending_error = (Error) e; + cancel (); + } + } + + public void cancel () { + io_cancellable.cancel (); + } + + public async PeerInfo wait_until_ready (Cancellable? cancellable = null) throws Error, IOError { + yield ready.future.wait_async (cancellable); + + return new PeerInfo () { + metadata = root_helo.body, + }; + } + + public async XpcMessage request (Bytes body, Cancellable? cancellable = null) throws Error, IOError { + uint64 request_id = make_message_id (); + + Bytes raw_request = new XpcMessageBuilder (MSG) + .add_flags (WANTS_REPLY) + .add_id (request_id) + .add_body (body) + .build (); + + bool waiting = false; + + var pending = new PendingResponse (() => { + if (waiting) + request.callback (); + return Source.REMOVE; + }); + pending_responses[request_id] = pending; + + try { + yield root_stream.submit_data (raw_request, cancellable); + } catch (Error e) { + if (pending_responses.unset (request_id)) + pending.complete_with_error (e); + } + + if (!pending.completed) { + var cancel_source = new CancellableSource (cancellable); + cancel_source.set_callback (() => { + if (pending_responses.unset (request_id)) + pending.complete_with_error (new IOError.CANCELLED ("Operation was cancelled")); + return false; + }); + cancel_source.attach (MainContext.get_thread_default ()); + + waiting = true; + yield; + waiting = false; + + cancel_source.destroy (); + } + + cancellable.set_error_if_cancelled (); + + if (pending.error != null) + throw_api_error (pending.error); + + return pending.result; + } + + private class PendingResponse { + private SourceFunc? handler; + + public bool completed { + get { + return result != null || error != null; + } + } + + public XpcMessage? result { + get; + private set; + } + + public GLib.Error? error { + get; + private set; + } + + public PendingResponse (owned SourceFunc handler) { + this.handler = (owned) handler; + } + + public void complete_with_result (XpcMessage result) { + if (completed) + return; + this.result = result; + handler (); + handler = null; + } + + public void complete_with_error (GLib.Error error) { + if (completed) + return; + this.error = error; + handler (); + handler = null; + } + } + + public async void post (Bytes body, Cancellable? cancellable = null) throws Error, IOError { + Bytes raw_request = new XpcMessageBuilder (MSG) + .add_id (make_message_id ()) + .add_body (body) + .build (); + + yield root_stream.submit_data (raw_request, cancellable); + } + + private void on_header (XpcMessage msg, Stream sender) { + if (sender == root_stream) { + if (root_helo == null) + root_helo = msg; + } else if (sender == reply_stream) { + if (reply_helo == null) + reply_helo = msg; + } + + if (!ready.future.ready && root_helo != null && reply_helo != null) + ready.resolve (true); + } + + private void on_reply (XpcMessage msg, Stream sender) { + if (sender != reply_stream) + return; + + PendingResponse response; + if (!pending_responses.unset (msg.id, out response)) + return; + + if (msg.body != null) + response.complete_with_result (msg); + else + response.complete_with_error (new Error.NOT_SUPPORTED ("Request not supported")); + } + + private void maybe_send_pending () { + while (session.want_write ()) { + bool would_block = send_source != null && send_queue == null; + if (would_block) + break; + + session.send (); + } + } + + private async void process_incoming_messages () { + InputStream input = stream.get_input_stream (); + + var buffer = new uint8[4096]; + + while (is_processing_messages) { + try { + ssize_t n = yield input.read_async (buffer, Priority.DEFAULT, io_cancellable); + if (n == 0) { + is_processing_messages = false; + continue; + } + + ssize_t result = session.mem_recv (buffer[:n]); + if (result < 0) + throw new Error.PROTOCOL ("%s", NGHttp2.strerror (result)); + + session.consume_connection (n); + } catch (GLib.Error e) { + if (e is Error && pending_error == null) + pending_error = (Error) e; + is_processing_messages = false; + } + } + + Error error = (pending_error != null) + ? pending_error + : new Error.TRANSPORT ("Connection closed"); + + foreach (var r in pending_responses.values.to_array ()) + r.complete_with_error (error); + pending_responses.clear (); + + if (!ready.future.ready) + ready.reject (error); + + state = CLOSED; + + close (pending_error); + pending_error = null; + } + + private ssize_t on_send (uint8[] data, int flags) { + if (send_source == null) { + send_queue = new ByteArray.sized (1024); + + var source = new IdleSource (); + source.set_callback (() => { + do_send.begin (); + return Source.REMOVE; + }); + source.attach (MainContext.get_thread_default ()); + send_source = source; + } + + if (send_queue == null) + return NGHttp2.ErrorCode.WOULDBLOCK; + + send_queue.append (data); + return data.length; + } + + private async void do_send () { + uint8[] buffer = send_queue.steal (); + send_queue = null; + + try { + size_t bytes_written; + yield stream.get_output_stream ().write_all_async (buffer, Priority.DEFAULT, io_cancellable, + out bytes_written); + } catch (GLib.Error e) { + } + + send_source = null; + + maybe_send_pending (); + } + + private int on_frame_send (NGHttp2.Frame frame) { + if (frame.hd.type == DATA) + find_stream_by_id (frame.hd.stream_id).on_data_frame_send (); + return 0; + } + + private int on_frame_not_send (NGHttp2.Frame frame, NGHttp2.ErrorCode lib_error_code) { + if (frame.hd.type == DATA) + find_stream_by_id (frame.hd.stream_id).on_data_frame_not_send (lib_error_code); + return 0; + } + + private int on_data_chunk_recv (uint8 flags, int32 stream_id, uint8[] data) { + return find_stream_by_id (stream_id).on_data_frame_recv_chunk (data); + } + + private int on_frame_recv (NGHttp2.Frame frame) { + if (frame.hd.type == DATA) + return find_stream_by_id (frame.hd.stream_id).on_data_frame_recv_end (frame); + return 0; + } + + private int on_stream_close (int32 stream_id, uint32 error_code) { + io_cancellable.cancel (); + return 0; + } + + private Stream make_stream () { + int stream_id = session.submit_headers (NGHttp2.Flag.NONE, -1, null, {}, null); + maybe_send_pending (); + + return new Stream (this, stream_id); + } + + private Stream? find_stream_by_id (int32 id) { + if (root_stream.id == id) + return root_stream; + if (reply_stream.id == id) + return reply_stream; + return null; + } + + private uint make_message_id () { + uint id = next_message_id; + next_message_id += 2; + return id; + } + + private class Stream { + public int32 id; + + private weak XpcConnection parent; + + private Gee.Deque submissions = new Gee.ArrayQueue (); + private SubmitOperation? current_submission = null; + private ByteArray incoming_message = new ByteArray (); + + public Stream (XpcConnection parent, int32 id) { + this.parent = parent; + this.id = id; + } + + public async void submit_data (Bytes bytes, Cancellable? cancellable) throws Error, IOError { + bool waiting = false; + + var op = new SubmitOperation (bytes, () => { + if (waiting) + submit_data.callback (); + return Source.REMOVE; + }); + + var cancel_source = new CancellableSource (cancellable); + cancel_source.set_callback (() => { + op.state = CANCELLED; + op.callback (); + return Source.REMOVE; + }); + cancel_source.attach (MainContext.get_thread_default ()); + + submissions.offer_tail (op); + maybe_submit_data (); + + if (op.state < SubmitOperation.State.SUBMITTED) { + waiting = true; + yield; + waiting = false; + } + + cancel_source.destroy (); + + if (op.state == CANCELLED && current_submission != op) + submissions.remove (op); + + cancellable.set_error_if_cancelled (); + + if (op.state == ERROR) + throw new Error.TRANSPORT ("%s", NGHttp2.strerror (op.error_code)); + } + + private void maybe_submit_data () { + if (current_submission != null) + return; + + SubmitOperation? op = submissions.peek_head (); + if (op == null) + return; + current_submission = op; + + var data_prd = NGHttp2.DataProvider (); + data_prd.source.ptr = op; + data_prd.read_callback = on_data_provider_read; + int result = parent.session.submit_data (NGHttp2.DataFlag.NO_END_STREAM, id, data_prd); + if (result < 0) { + while (true) { + op = submissions.poll_head (); + if (op == null) + break; + op.state = ERROR; + op.error_code = (NGHttp2.ErrorCode) result; + op.callback (); + } + current_submission = null; + return; + } + + parent.maybe_send_pending (); + } + + private static ssize_t on_data_provider_read (NGHttp2.Session session, int32 stream_id, uint8[] buf, + ref uint32 data_flags, NGHttp2.DataSource source, void * user_data) { + var op = (SubmitOperation) source.ptr; + + unowned uint8[] data = op.bytes.get_data (); + + uint remaining = data.length - op.cursor; + uint n = uint.min (remaining, buf.length); + Memory.copy (buf, (uint8 *) data + op.cursor, n); + + op.cursor += n; + + if (op.cursor == data.length) + data_flags |= NGHttp2.DataFlag.EOF; + + return n; + } + + public void on_data_frame_send () { + submissions.poll_head ().complete (SUBMITTED); + current_submission = null; + + maybe_submit_data (); + } + + public void on_data_frame_not_send (NGHttp2.ErrorCode lib_error_code) { + submissions.poll_head ().complete (ERROR, lib_error_code); + current_submission = null; + + maybe_submit_data (); + } + + private class SubmitOperation { + public Bytes bytes; + public SourceFunc callback; + + public State state = PENDING; + public NGHttp2.ErrorCode error_code; + public uint cursor = 0; + + public enum State { + PENDING, + SUBMITTING, + SUBMITTED, + ERROR, + CANCELLED, + } + + public SubmitOperation (Bytes bytes, owned SourceFunc callback) { + this.bytes = bytes; + this.callback = (owned) callback; + } + + public void complete (State new_state, NGHttp2.ErrorCode err = -1) { + if (state != PENDING) + return; + state = new_state; + error_code = err; + callback (); + } + } + + public int on_data_frame_recv_chunk (uint8[] data) { + incoming_message.append (data); + return 0; + } + + public int on_data_frame_recv_end (NGHttp2.Frame frame) { + XpcMessage? msg; + size_t size; + try { + msg = XpcMessage.try_parse (incoming_message.data, out size); + } catch (Error e) { + return -1; + } + if (msg == null) + return 0; + incoming_message.remove_range (0, (uint) size); + + switch (msg.type) { + case HEADER: + parent.on_header (msg, this); + break; + case MSG: + if ((msg.flags & MessageFlags.IS_REPLY) != 0) + parent.on_reply (msg, this); + else if ((msg.flags & (MessageFlags.WANTS_REPLY | MessageFlags.IS_REPLY)) == 0) + parent.message (msg); + break; + case PING: + break; + } + + return 0; + } + } + } + + public class PeerInfo { + public Variant? metadata; + } + + public class XpcMessageBuilder : Object { + private MessageType message_type; + private MessageFlags message_flags = NONE; + private uint64 message_id = 0; + private Bytes? body = null; + + public XpcMessageBuilder (MessageType message_type) { + this.message_type = message_type; + } + + public unowned XpcMessageBuilder add_flags (MessageFlags flags) { + message_flags = flags; + return this; + } + + public unowned XpcMessageBuilder add_id (uint64 id) { + message_id = id; + return this; + } + + public unowned XpcMessageBuilder add_body (Bytes b) { + body = b; + return this; + } + + public Bytes build () { + var builder = new BufferBuilder (LITTLE_ENDIAN) + .append_uint32 (XpcMessage.MAGIC) + .append_uint8 (XpcMessage.PROTOCOL_VERSION) + .append_uint8 (message_type) + .append_uint16 (message_flags) + .append_uint64 ((body != null) ? body.length : 0) + .append_uint64 (message_id); + + if (body != null) + builder.append_bytes (body); + + return builder.build (); + } + } + + public class XpcMessage { + public MessageType type; + public MessageFlags flags; + public uint64 id; + public Variant? body; + + public const uint32 MAGIC = 0x29b00b92; + public const uint8 PROTOCOL_VERSION = 1; + public const size_t HEADER_SIZE = 24; + public const size_t MAX_SIZE = (128 * 1024 * 1024) - 1; + + public static XpcMessage parse (uint8[] data) throws Error { + size_t size; + var msg = try_parse (data, out size); + if (msg == null) + throw new Error.INVALID_ARGUMENT ("XpcMessage is truncated"); + return msg; + } + + public static XpcMessage? try_parse (uint8[] data, out size_t size) throws Error { + if (data.length < HEADER_SIZE) { + size = HEADER_SIZE; + return null; + } + + var buf = new Buffer (new Bytes.static (data), LITTLE_ENDIAN); + + var magic = buf.read_uint32 (0); + if (magic != MAGIC) + throw new Error.INVALID_ARGUMENT ("Invalid message: bad magic (0x%08x)", magic); + + var protocol_version = buf.read_uint8 (4); + if (protocol_version != PROTOCOL_VERSION) + throw new Error.INVALID_ARGUMENT ("Invalid message: unsupported protocol version (%u)", protocol_version); + + var raw_message_type = buf.read_uint8 (5); + var message_type_class = (EnumClass) typeof (MessageType).class_ref (); + if (message_type_class.get_value (raw_message_type) == null) + throw new Error.INVALID_ARGUMENT ("Invalid message: unsupported message type (0x%x)", raw_message_type); + var message_type = (MessageType) raw_message_type; + + MessageFlags message_flags = (MessageFlags) buf.read_uint16 (6); + + Variant? body = null; + uint64 message_size = buf.read_uint64 (8); + size = HEADER_SIZE + (size_t) message_size; + if (message_size != 0) { + if (message_size > MAX_SIZE) { + throw new Error.INVALID_ARGUMENT ("Invalid message: too large (%" + int64.FORMAT_MODIFIER + "u)", + message_size); + } + if (data.length - HEADER_SIZE < message_size) + return null; + body = XpcBodyParser.parse (data[HEADER_SIZE:HEADER_SIZE + message_size]); + } + + uint64 message_id = buf.read_uint64 (16); + + return new XpcMessage (message_type, message_flags, message_id, body); + } + + private XpcMessage (MessageType type, MessageFlags flags, uint64 id, Variant? body) { + this.type = type; + this.flags = flags; + this.id = id; + this.body = body; + } + + public string to_string () { + var description = new StringBuilder.sized (128); + + description.append_printf (("XpcMessage {" + + "\n\ttype: %s," + + "\n\tflags: %s," + + "\n\tid: %" + int64.FORMAT_MODIFIER + "u,"), + type.to_nick ().up (), + flags.print (), + id); + + if (body != null) { + description.append ("\n\tbody: "); + print_variant (body, description, 1); + description.append_c (','); + } + + description.append ("\n}"); + + return description.str; + } + } + + public enum MessageType { + HEADER, + MSG, + PING; + + public static MessageType from_nick (string nick) throws Error { + return Marshal.enum_from_nick (nick); + } + + public string to_nick () { + return Marshal.enum_to_nick (this); + } + } + + [Flags] + public enum MessageFlags { + NONE = 0, + WANTS_REPLY = (1 << 0), + IS_REPLY = (1 << 1), + HEADER_OPENS_STREAM_TX = (1 << 4), + HEADER_OPENS_STREAM_RX = (1 << 5), + HEADER_OPENS_REPLY_CHANNEL = (1 << 6); + + public string print () { + uint remainder = this; + if (remainder == 0) + return "NONE"; + + var result = new StringBuilder.sized (128); + + var klass = (FlagsClass) typeof (MessageFlags).class_ref (); + foreach (FlagsValue fv in klass.values) { + if ((remainder & fv.value) != 0) { + if (result.len != 0) + result.append (" | "); + result.append (fv.value_nick.up ().replace ("-", "_")); + remainder &= ~fv.value; + } + } + + if (remainder != 0) { + if (result.len != 0) + result.append (" | "); + result.append_printf ("0x%04x", remainder); + } + + return result.str; + } + } + + public class XpcBodyBuilder : XpcObjectBuilder { + public XpcBodyBuilder () { + base (); + + builder + .append_uint32 (SerializedXpcObject.MAGIC) + .append_uint32 (SerializedXpcObject.VERSION); + } + } + + public class XpcObjectBuilder : Object, ObjectBuilder { + protected BufferBuilder builder = new BufferBuilder (LITTLE_ENDIAN); + private Gee.Deque scopes = new Gee.ArrayQueue (); + + public XpcObjectBuilder () { + push_scope (new Scope (ROOT)); + } + + public unowned ObjectBuilder begin_dictionary () { + begin_object (DICTIONARY); + + size_t size_offset = builder.offset; + builder.append_uint32 (0); + + size_t num_entries_offset = builder.offset; + builder.append_uint32 (0); + + push_scope (new DictionaryScope (size_offset, num_entries_offset)); + + return this; + } + + public unowned ObjectBuilder set_member_name (string name) { + builder + .append_string (name) + .align (4); + + return this; + } + + public unowned ObjectBuilder end_dictionary () { + DictionaryScope scope = pop_scope (); + + uint32 size = (uint32) (builder.offset - scope.num_entries_offset); + builder.write_uint32 (scope.size_offset, size); + + builder.write_uint32 (scope.num_entries_offset, scope.num_objects); + + return this; + } + + public unowned ObjectBuilder begin_array () { + begin_object (ARRAY); + + size_t size_offset = builder.offset; + builder.append_uint32 (0); + + size_t num_elements_offset = builder.offset; + builder.append_uint32 (0); + + push_scope (new ArrayScope (size_offset, num_elements_offset)); + + return this; + } + + public unowned ObjectBuilder end_array () { + ArrayScope scope = pop_scope (); + + uint32 size = (uint32) (builder.offset - scope.num_elements_offset); + builder.write_uint32 (scope.size_offset, size); + + builder.write_uint32 (scope.num_elements_offset, scope.num_objects); + + return this; + } + + public unowned ObjectBuilder add_null_value () { + begin_object (NULL); + return this; + } + + public unowned ObjectBuilder add_bool_value (bool val) { + begin_object (BOOL).append_uint32 ((uint32) val); + return this; + } + + public unowned ObjectBuilder add_int64_value (int64 val) { + begin_object (INT64).append_int64 (val); + return this; + } + + public unowned ObjectBuilder add_uint64_value (uint64 val) { + begin_object (UINT64).append_uint64 (val); + return this; + } + + public unowned ObjectBuilder add_data_value (Bytes val) { + begin_object (DATA) + .append_uint32 (val.length) + .append_bytes (val) + .align (4); + return this; + } + + public unowned ObjectBuilder add_string_value (string val) { + begin_object (STRING) + .append_uint32 (val.length + 1) + .append_string (val) + .align (4); + return this; + } + + public unowned ObjectBuilder add_uuid_value (string val) { + var uuid = new ByteArray.sized (16); + int len = val.length; + for (int i = 0; i != len;) { + if (val[i] == '-') { + i++; + continue; + } + var byte = (uint8) uint.parse (val[i:i + 2], 16); + uuid.append ({ byte }); + i += 2; + } + assert (uuid.len == 16); + + begin_object (UUID).append_data (uuid.data); + return this; + } + + public unowned ObjectBuilder add_raw_value (Bytes val) { + peek_scope ().num_objects++; + builder.append_bytes (val); + return this; + } + + private unowned BufferBuilder begin_object (ObjectType type) { + peek_scope ().num_objects++; + return builder.append_uint32 (type); + } + + public Bytes build () { + return builder.build (); + } + + private void push_scope (Scope scope) { + scopes.offer_tail (scope); + } + + private Scope peek_scope () { + return scopes.peek_tail (); + } + + private T pop_scope () { + return (T) scopes.poll_tail (); + } + + private class Scope { + public Kind kind; + public uint32 num_objects = 0; + + public enum Kind { + ROOT, + DICTIONARY, + ARRAY, + } + + public Scope (Kind kind) { + this.kind = kind; + } + } + + private class DictionaryScope : Scope { + public size_t size_offset; + public size_t num_entries_offset; + + public DictionaryScope (size_t size_offset, size_t num_entries_offset) { + base (DICTIONARY); + this.size_offset = size_offset; + this.num_entries_offset = num_entries_offset; + } + } + + private class ArrayScope : Scope { + public size_t size_offset; + public size_t num_elements_offset; + + public ArrayScope (size_t size_offset, size_t num_elements_offset) { + base (DICTIONARY); + this.size_offset = size_offset; + this.num_elements_offset = num_elements_offset; + } + } + } + + private enum ObjectType { + NULL = 0x1000, + BOOL = 0x2000, + INT64 = 0x3000, + UINT64 = 0x4000, + DATA = 0x8000, + STRING = 0x9000, + UUID = 0xa000, + ARRAY = 0xe000, + DICTIONARY = 0xf000, + } + + private class XpcBodyParser { + public static Variant parse (uint8[] data) throws Error { + if (data.length < 12) + throw new Error.INVALID_ARGUMENT ("Invalid xpc_object: truncated"); + + var buf = new Buffer (new Bytes.static (data), LITTLE_ENDIAN); + + var magic = buf.read_uint32 (0); + if (magic != SerializedXpcObject.MAGIC) + throw new Error.INVALID_ARGUMENT ("Invalid xpc_object: bad magic (0x%08x)", magic); + + var version = buf.read_uint8 (4); + if (version != SerializedXpcObject.VERSION) + throw new Error.INVALID_ARGUMENT ("Invalid xpc_object: unsupported version (%u)", version); + + var parser = new XpcObjectParser (buf, 8); + return parser.read_object (); + } + } + + private class XpcObjectParser { + private Buffer buf; + private size_t cursor; + private EnumClass object_type_class; + + public XpcObjectParser (Buffer buf, uint cursor) { + this.buf = buf; + this.cursor = cursor; + this.object_type_class = (EnumClass) typeof (ObjectType).class_ref (); + } + + public Variant read_object () throws Error { + var raw_type = read_raw_uint32 (); + if (object_type_class.get_value ((int) raw_type) == null) + throw new Error.INVALID_ARGUMENT ("Invalid xpc_object: unsupported type (0x%x)", raw_type); + var type = (ObjectType) raw_type; + + switch (type) { + case NULL: + return new Variant.maybe (VariantType.VARIANT, null); + case BOOL: + return new Variant.boolean (read_raw_uint32 () != 0); + case INT64: + return new Variant.int64 (read_raw_int64 ()); + case UINT64: + return new Variant.uint64 (read_raw_uint64 ()); + case DATA: + return read_data (); + case STRING: + return read_string (); + case UUID: + return read_uuid (); + case ARRAY: + return read_array (); + case DICTIONARY: + return read_dictionary (); + default: + assert_not_reached (); + } + } + + private Variant read_data () throws Error { + var size = read_raw_uint32 (); + + var bytes = read_raw_bytes (size); + align (4); + + return Variant.new_from_data (new VariantType.array (VariantType.BYTE), bytes.get_data (), true, bytes); + } + + private Variant read_string () throws Error { + var size = read_raw_uint32 (); + + var str = buf.read_string (cursor); + cursor += size; + align (4); + + return new Variant.string (str); + } + + private Variant read_uuid () throws Error { + uint8[] uuid = read_raw_bytes (16).get_data (); + return new Variant.string ("%02X%02X%02X%02X-%02X%02X-%02X%02X-%02X%02X-%02X%02X%02X%02X%02X%02X".printf ( + uuid[0], uuid[1], uuid[2], uuid[3], + uuid[4], uuid[5], + uuid[6], uuid[7], + uuid[8], uuid[9], + uuid[10], uuid[11], uuid[12], uuid[13], uuid[14], uuid[15])); + } + + private Variant read_array () throws Error { + var builder = new VariantBuilder (new VariantType.array (VariantType.VARIANT)); + + var size = read_raw_uint32 (); + size_t num_elements_offset = cursor; + var num_elements = read_raw_uint32 (); + + for (uint32 i = 0; i != num_elements; i++) + builder.add ("v", read_object ()); + + cursor = num_elements_offset; + skip (size); + + return builder.end (); + } + + private Variant read_dictionary () throws Error { + var builder = new VariantBuilder (VariantType.VARDICT); + + var size = read_raw_uint32 (); + size_t num_entries_offset = cursor; + var num_entries = read_raw_uint32 (); + + for (uint32 i = 0; i != num_entries; i++) { + string key = buf.read_string (cursor); + skip (key.length + 1); + align (4); + + Variant val = read_object (); + + builder.add ("{sv}", key, val); + } + + cursor = num_entries_offset; + skip (size); + + return builder.end (); + } + + private uint32 read_raw_uint32 () throws Error { + check_available (sizeof (uint32)); + var result = buf.read_uint32 (cursor); + cursor += sizeof (uint32); + return result; + } + + private int64 read_raw_int64 () throws Error { + check_available (sizeof (int64)); + var result = buf.read_int64 (cursor); + cursor += sizeof (int64); + return result; + } + + private uint64 read_raw_uint64 () throws Error { + check_available (sizeof (uint64)); + var result = buf.read_uint64 (cursor); + cursor += sizeof (uint64); + return result; + } + + private Bytes read_raw_bytes (size_t n) throws Error { + check_available (n); + Bytes result = buf.bytes[cursor:cursor + n]; + cursor += n; + return result; + } + + private void skip (size_t n) throws Error { + check_available (n); + cursor += n; + } + + private void align (size_t n) throws Error { + size_t remainder = cursor % n; + if (remainder != 0) + skip (n - remainder); + } + + private void check_available (size_t required) throws Error { + size_t available = buf.bytes.get_size () - cursor; + if (available < required) + throw new Error.INVALID_ARGUMENT ("Invalid xpc_object: truncated"); + } + } + + namespace SerializedXpcObject { + public const uint32 MAGIC = 0x42133742; + public const uint32 VERSION = 5; + } + + private Bytes get_raw_public_key (Key key) { + size_t size = 0; + key.get_raw_public_key (null, ref size); + + var result = new uint8[size]; + key.get_raw_public_key (result, ref size); + + return new Bytes.take ((owned) result); + } + + private Bytes get_raw_private_key (Key key) { + size_t size = 0; + key.get_raw_private_key (null, ref size); + + var result = new uint8[size]; + key.get_raw_private_key (result, ref size); + + return new Bytes.take ((owned) result); + } + + // https://gist.github.com/phako/96b36b5070beaf7eee27 + public void hexdump (uint8[] data) { + var builder = new StringBuilder.sized (16); + var i = 0; + + foreach (var c in data) { + if (i % 16 == 0) + printerr ("%08x | ", i); + + printerr ("%02x ", c); + + if (((char) c).isprint ()) + builder.append_c ((char) c); + else + builder.append ("."); + + i++; + if (i % 16 == 0) { + printerr ("| %s\n", builder.str); + builder.erase (); + } + } + + if (i % 16 != 0) + printerr ("%s| %s\n", string.nfill ((16 - (i % 16)) * 3, ' '), builder.str); + } + + private string variant_to_pretty_string (Variant v) { + var sink = new StringBuilder.sized (128); + print_variant (v, sink); + return (owned) sink.str; + } + + private void print_variant (Variant v, StringBuilder sink, uint depth = 0, bool initial = true) { + VariantType type = v.get_type (); + + if (type.is_basic ()) { + sink.append (v.print (false)); + return; + } + + if (type.equal (VariantType.VARDICT)) { + sink.append ("{\n"); + + var iter = new VariantIter (v); + string key; + Variant val; + while (iter.next ("{sv}", out key, out val)) { + append_indent (depth + 1, sink); + + if ("." in key || "-" in key) { + sink + .append_c ('"') + .append (key) + .append_c ('"'); + } else { + sink.append (key); + } + sink.append (": "); + + print_variant (val, sink, depth + 1, false); + + sink.append (",\n"); + } + + append_indent (depth, sink); + sink.append ("}"); + } else if (type.is_array () && !type.equal (new VariantType.array (VariantType.BYTE))) { + sink.append ("[\n"); + + var iter = new VariantIter (v); + Variant? val; + while ((val = iter.next_value ()) != null) { + append_indent (depth + 1, sink); + print_variant (val, sink, depth + 1, false); + sink.append (",\n"); + } + + append_indent (depth, sink); + sink.append ("]"); + } else { + sink.append (v.print (false)); + } + } + + private void append_indent (uint depth, StringBuilder sink) { + for (uint i = 0; i != depth; i++) + sink.append_c ('\t'); + } + + private string make_host_identifier () { + var checksum = new Checksum (MD5); + + const uint8 uuid_version = 3; + const uint8 dns_namespace[] = { 0x6b, 0xa7, 0xb8, 0x10, 0x9d, 0xad, 0x11, 0xd1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8 }; + checksum.update (dns_namespace, dns_namespace.length); + + unowned uint8[] host_name = Environment.get_host_name ().data; + checksum.update (host_name, host_name.length); + + uint8 uuid[16]; + size_t len = uuid.length; + checksum.get_digest (uuid, ref len); + + uuid[6] = (uuid_version << 4) | (uuid[6] & 0xf); + uuid[8] = 0x80 | (uuid[8] & 0x3f); + + var result = new StringBuilder.sized (36); + for (var i = 0; i != uuid.length; i++) { + result.append_printf ("%02X", uuid[i]); + switch (i) { + case 3: + case 5: + case 7: + case 9: + result.append_c ('-'); + break; + } + } + + return result.str; + } +} diff --git a/src/host-session-service.vala b/src/host-session-service.vala index b8e7183c9..636dd72d6 100644 --- a/src/host-session-service.vala +++ b/src/host-session-service.vala @@ -177,12 +177,16 @@ namespace Frida { } } - public interface ChannelProvider : Object { + public interface HostChannelProvider : Object { public abstract async IOStream open_channel (string address, Cancellable? cancellable = null) throws Error, IOError; } + public interface HostServiceProvider : Object { + public abstract async Service open_service (string address, Cancellable? cancellable = null) throws Error, IOError; + } + public interface Pairable : Object { - public abstract async void unpair (Cancellable? cancellable) throws Error, IOError; + public abstract async void unpair (Cancellable? cancellable = null) throws Error, IOError; } public interface HostSessionBackend : Object { @@ -1537,7 +1541,7 @@ namespace Frida { #if HAVE_FRUITY_BACKEND || HAVE_DROIDY_BACKEND internal async DBusConnection establish_direct_connection (TransportBroker broker, AgentSessionId id, - ChannelProvider channel_provider, Cancellable? cancellable) throws Error, IOError { + HostChannelProvider channel_provider, Cancellable? cancellable) throws Error, IOError { uint16 port; string token; try { diff --git a/src/meson.build b/src/meson.build index f3eb20285..e41277028 100644 --- a/src/meson.build +++ b/src/meson.build @@ -481,6 +481,7 @@ endif if have_fruity_backend backend_sources += [ 'fruity' / 'fruity-host-session.vala', + 'fruity' / 'xpc.vala', 'fruity' / 'dtx.vala', 'fruity' / 'lockdown.vala', 'fruity' / 'installation-proxy.vala', @@ -492,6 +493,7 @@ if have_fruity_backend 'fruity' / 'plist.vala', 'fruity' / 'plist-service.vala', ] + if host_os_family == 'windows' backend_sources += 'fruity' / 'fruity-host-session-windows.c' elif host_os_family == 'darwin' @@ -499,6 +501,41 @@ if have_fruity_backend else backend_sources += 'fruity' / 'fruity-host-session-unix.c' endif + + if host_os_family == 'darwin' + backend_sources += 'fruity' / 'xpc-darwin.vala' + endif + if host_os == 'macos' + backend_sources += [ + 'fruity' / 'xpc-macos.vala', + 'fruity' / 'iokit.vala', + ] + elif host_os == 'linux' + backend_sources += 'fruity' / 'xpc-linux.vala' + endif + + backend_deps += openssl_dep + backend_vala_args += [ + '--pkg=libnghttp2', + '--pkg=libngtcp2', + '--pkg=libngtcp2_crypto_quictls', + '--pkg=lwip', + '--pkg=openssl', + ] + if host_os_family == 'darwin' + backend_vala_args += [ + '--pkg=darwin-xnu', + '--pkg=darwin-iokit', + '--pkg=darwin-xpc', + '--pkg=darwin-gcd', + '--pkg=darwin-net', + '--pkg=darwin-dns-sd', + '--pkg=corefoundation', + ] + endif + if host_os_family == 'linux' + backend_vala_args += '--pkg=linux' + endif endif if have_droidy_backend @@ -590,3 +627,11 @@ subdir('api') if 'core' in get_option('devkits') subdir('devkit') endif + +if have_fruity_backend + executable('xpc-test', + 'fruity' / 'xpc-test.vala', + vala_args: [backend_vala_args, backend_vala_args_private, base_vala_args, gum_vala_args], + dependencies: core_dep, + ) +endif diff --git a/tests/test-host-session.vala b/tests/test-host-session.vala index 6a55e7333..e206ec223 100644 --- a/tests/test-host-session.vala +++ b/tests/test-host-session.vala @@ -60,6 +60,11 @@ namespace Frida.HostSessionTest { var h = new Harness ((h) => Fruity.Manual.lockdown.begin (h as Harness)); h.run (); }); + + GLib.Test.add_func ("/HostSession/Fruity/Manual/xpc", () => { + var h = new Harness ((h) => Fruity.Manual.xpc.begin (h as Harness)); + h.run (); + }); #endif #if HAVE_DROIDY_BACKEND @@ -3517,16 +3522,17 @@ namespace Frida.HostSessionTest { #endif Variant? icon = prov.icon; - assert_nonnull (icon); - var dict = new VariantDict (icon); - int64 width, height; - assert_true (dict.lookup ("width", "x", out width)); - assert_true (dict.lookup ("height", "x", out height)); - assert_true (width == 16); - assert_true (height == 16); - VariantIter image; - assert_true (dict.lookup ("image", "ay", out image)); - assert_true (image.n_children () == width * height * 4); + if (icon != null) { + var dict = new VariantDict (icon); + int64 width, height; + assert_true (dict.lookup ("width", "x", out width)); + assert_true (dict.lookup ("height", "x", out height)); + assert_true (width == 16); + assert_true (height == 16); + VariantIter image; + assert_true (dict.lookup ("image", "ay", out image)); + assert_true (image.n_children () == width * height * 4); + } try { Cancellable? cancellable = null; @@ -3741,6 +3747,41 @@ namespace Frida.HostSessionTest { h.done (); } + private static async void xpc (Harness h) { + if (!GLib.Test.slow ()) { + stdout.printf (" "); + h.done (); + return; + } + + var device_id = ""; + + var device_manager = new DeviceManager (); + + try { + var timer = new Timer (); + var device = yield device_manager.get_device_by_id (device_id); + printerr ("[*] Got device in %u ms\n", (uint) (timer.elapsed () * 1000.0)); + + timer.reset (); + var appservice = yield device.open_service ("xpc:com.apple.coredevice.appservice"); + printerr ("[*] Opened service in %u ms\n", (uint) (timer.elapsed () * 1000.0)); + + var parameters = new HashTable (str_hash, str_equal); + parameters["CoreDevice.featureIdentifier"] = "com.apple.coredevice.feature.listprocesses"; + parameters["CoreDevice.action"] = new HashTable (str_hash, str_equal); + parameters["CoreDevice.input"] = new HashTable (str_hash, str_equal); + timer.reset (); + var response = yield appservice.request (parameters); + printerr ("[*] Made request in %u ms\n", (uint) (timer.elapsed () * 1000.0)); + printerr ("Got response: %s\n", response.print (true)); + } catch (GLib.Error e) { + printerr ("\nFAIL: %s\n\n", e.message); + } + + h.done (); + } + } namespace Plist { diff --git a/vapi/corefoundation.vapi b/vapi/corefoundation.vapi new file mode 100644 index 000000000..e4271ee70 --- /dev/null +++ b/vapi/corefoundation.vapi @@ -0,0 +1,104 @@ +[CCode (cheader_filename = "CoreFoundation/CFDictionary.h", gir_namespace = "Darwin", gir_version = "1.0")] +namespace CoreFoundation { + [Compact] + [CCode (cname = "struct __CFDictionary", ref_function = "CFRetain", unref_function = "CFRelease")] + public class Dictionary : Type { + [CCode (cname = "CFDictionaryGetCount")] + public Index count (); + + [CCode (cname = "CFDictionaryGetValue")] + public void * @get (void * key); + + public string get_string_value (string key) { + return ((String) @get (String.make (key))).to_string (); + } + } + + [Compact] + [CCode (cname = "struct __CFDictionary", ref_function = "CFRetain", unref_function = "CFRelease")] + public class MutableDictionary : Dictionary { + [CCode (cname = "CFDictionaryRemoveAllValues")] + public void clear (); + + [CCode (cname = "CFDictionaryAddValue")] + public void add (void * key, void * value); + + [CCode (cname = "CFDictionaryRemoveValue")] + public void remove (void * key); + } + + // TODO: Inherit CFTypeRef? CFStringRef vs __CFString + [Compact] + [CCode (cname = "const struct __CFString", ref_function = "CFRetain", unref_function = "CFRelease")] + public class String : Type { + public Index length { + get { return _length (); } + } + + public static String make (string str) { + return from_cstring (null, str, StringEncoding.UTF8); + } + + public string to_string () { + var max_length = max_size_for_encoding (length, StringEncoding.UTF8) + 1; + var buffer = new char[max_length]; + to_cstring (buffer, max_length, StringEncoding.UTF8); + return (string) buffer; + } + + [CCode (cname = "CFStringCreateWithCString")] + private static String from_cstring (Allocator? allocator, uint8 * c_str, StringEncoding encoding); + + [CCode (cname = "CFStringGetCString")] + private bool to_cstring (uint8 * buffer, Index buffer_size, StringEncoding encoding); + + [CCode (cname = "CFStringGetLength")] + private Index _length (); + + [CCode (cname = "CFStringGetMaximumSizeForEncoding")] + private static Index max_size_for_encoding (Index length, StringEncoding encoding); + } + + [CCode (cname = "CFStringEncoding", cprefix = "kCFStringEncoding", has_type_id = false)] + public enum StringEncoding { + MacRoman, + WindowsLatin1, + ISOLatin1, + NextStepLatin, + ASCII, + Unicode, + UTF8, + NonLossyASCII, + UTF16, + UTF16BE, + UTF16LE, + UTF32, + UTF32BE, + UTF32LE, + } + + [Compact] + [CCode (cname = "const void", ref_function = "CFRetain", unref_function = "CFRelease")] + public class Type { + [CCode (cname = "CFShow")] + public void show (); + + public string to_string () { + return description ().to_string (); + } + + [CCode (cname = "CFCopyDescription")] + private String description (); + } + + [Compact] + [CCode (cname = "CFAllocatorRef")] + public class Allocator { + [CCode (cname = "CFAllocatorGetDefault")] + public static Allocator get_default (); + } + + [CCode (cname = "CFIndex", has_type_id = false)] + public struct Index : long { + } +} diff --git a/vapi/darwin-dns-sd.vapi b/vapi/darwin-dns-sd.vapi new file mode 100644 index 000000000..372eb0e14 --- /dev/null +++ b/vapi/darwin-dns-sd.vapi @@ -0,0 +1,134 @@ +[CCode (cheader_filename = "dns_sd.h", gir_namespace = "Darwin", gir_version = "1.0")] +namespace Darwin.DNSSD { + [CCode (cname = "DNSServiceRef", ref_function = "", unref_function = "")] + public class DNSService { + [CCode (cname = "DNSServiceCreateConnection")] + public static ErrorType create_connection (out DNSService service); + + [CCode (cname = "DNSServiceRefDeallocate")] + public void deallocate (); + + [CCode (cname = "DNSServiceSetDispatchQueue")] + public ErrorType set_dispatch_queue (Darwin.GCD.DispatchQueue queue); + + [CCode (cname = "DNSServiceBrowse")] + public static ErrorType browse (ref DNSService sd_ref, Flags flags, uint32 interface_index, string regtype, string? domain, + BrowseReply callback); + + [CCode (cname = "DNSServiceResolve")] + public static ErrorType resolve (ref DNSService sd_ref, Flags flags, uint32 interface_index, string name, string regtype, + string domain, ResolveReply callback); + + [CCode (cname = "DNSServiceGetAddrInfo")] + public static ErrorType get_addr_info (ref DNSService sd_ref, Flags flags, uint32 interface_index, Protocol protocol, + string hostname, GetAddrInfoReply callback); + + [CCode (cname = "DNSServiceBrowseReply")] + public delegate void BrowseReply (DNSService sd_ref, Flags flags, uint32 interface_index, ErrorType error_code, + string service_name, string regtype, string reply_domain); + + [CCode (cname = "DNSServiceResolveReply")] + public delegate void ResolveReply (DNSService sd_ref, Flags flags, uint32 interface_index, ErrorType error_code, + string fullname, string hosttarget, uint16 port, [CCode (array_length_pos = 7.9, array_length_type = "uint16_t")] uint8[] txt_record); + + [CCode (cname = "DNSServiceGetAddrInfoReply")] + public delegate void GetAddrInfoReply (DNSService sd_ref, Flags flags, uint32 interface_index, ErrorType error_code, + string hostname, void * address, uint32 ttl); + + [CCode (cname = "DNSServiceErrorType", cprefix = "kDNSServiceErr_", has_type_id = false)] + public enum ErrorType { + NoError, + Unknown, + NoSuchName, + NoMemory, + BadParam, + BadReference, + BadState, + BadFlags, + Unsupported, + NotInitialized, + AlreadyRegistered, + NameConflict, + Invalid, + Firewall, + Incompatible, + BadInterfaceIndex, + Refused, + NoSuchRecord, + NoAuth, + NoSuchKey, + NATTraversal, + DoubleNAT, + BadTime, + BadSig, + BadKey, + Transient, + ServiceNotRunning, + NATPortMappingUnsupported, + NATPortMappingDisabled, + NoRouter, + PollingMode, + Timeout, + DefunctConnection, + PolicyDenied, + NotPermitted, + } + + [CCode (cname = "DNSServiceFlags", cprefix = "kDNSServiceFlags", has_type_id = false)] + [Flags] + public enum Flags { + MoreComing, + QueueRequest, + AutoTrigger, + Add, + Default, + NoAutoRename, + Shared, + Unique, + BrowseDomains, + RegistrationDomains, + LongLivedQuery, + AllowRemoteQuery, + ForceMulticast, + Force, + KnownUnique, + ReturnIntermediates, + ShareConnection, + SuppressUnusable, + Timeout, + IncludeP2P, + WakeOnResolve, + BackgroundTrafficClass, + IncludeAWDL, + EnableDNSSEC, + Validate, + Secure, + Insecure, + Bogus, + Indeterminate, + UnicastResponse, + ValidateOptional, + WakeOnlyService, + ThresholdOne, + ThresholdFinder, + ThresholdReached, + PrivateOne, + PrivateTwo, + PrivateThree, + PrivateFour, + PrivateFive, + [CCode (cname = "kDNSServiceFlagAnsweredFromCache")] + AnsweredFromCache, + AllowExpiredAnswers, + ExpiredAnswer, + } + + [CCode (cname = "DNSServiceProtocol", cprefix = "kDNSServiceProtocol_", has_type_id = false)] + public enum Protocol { + IPv4 = 0x01, + IPv6 = 0x02, + UDP = 0x10, + TCP = 0x20, + } + } +} diff --git a/vapi/darwin-gcd.vapi b/vapi/darwin-gcd.vapi new file mode 100644 index 000000000..fd4c03878 --- /dev/null +++ b/vapi/darwin-gcd.vapi @@ -0,0 +1,30 @@ +[CCode (cheader_filename = "dispatch/dispatch.h", gir_namespace = "Darwin", gir_version = "1.0")] +namespace Darwin.GCD { + [Compact] + [CCode (cname = "void", ref_function = "_frida_dispatch_retain", unref_function = "dispatch_release", + cheader_filename = "frida-darwin.h")] + public class DispatchQueue { + [CCode (cname = "dispatch_queue_create")] + public DispatchQueue (string label, DispatchQueueAttr attr); + + [CCode (cname = "dispatch_async_f")] + public void dispatch_async ([CCode (delegate_target_pos = 0.9)] DispatchFunction work); + + [CCode (cname = "dispatch_sync_f")] + public void dispatch_sync ([CCode (delegate_target_pos = 0.9)] DispatchFunction work); + } + + [CCode (cname = "dispatch_function_t")] + public delegate void DispatchFunction (); + + [Compact] + [CCode (cname = "dispatch_queue_attr_t", cprefix = "DISPATCH_QUEUE_")] + public class DispatchQueueAttr { + public static DispatchQueueAttr SERIAL; + public static DispatchQueueAttr SERIAL_INACTIVE; + public static DispatchQueueAttr CONCURRENT; + public static DispatchQueueAttr CONCURRENT_INACTIVE; + public static DispatchQueueAttr SERIAL_WITH_AUTORELEASE_POOL; + public static DispatchQueueAttr CONCURRENT_WITH_AUTORELEASE_POOL; + } +} diff --git a/vapi/darwin-iokit.vapi b/vapi/darwin-iokit.vapi new file mode 100644 index 000000000..992cd1c63 --- /dev/null +++ b/vapi/darwin-iokit.vapi @@ -0,0 +1,58 @@ +[CCode (cheader_filename = "IOKit/IOKitLib.h", gir_namespace = "Darwin", gir_version = "1.0")] +namespace Darwin.IOKit { + using CoreFoundation; + using Darwin.XNU; + + [CCode (cname = "IOMasterPort")] + public KernReturn main_port (MachPort bootstrap_port, out MachPort main_port); + + [CCode (cname = "IOServiceMatching")] + public MutableDictionary service_matching (string name); + + [CCode (cname = "IOServiceGetMatchingServices")] + public KernReturn get_matching_services (MachPort main_port, owned MutableDictionary matching_dict, + out IOIterator iterator); + + [CCode (cname = "kIOEthernetInterfaceClass", cheader_filename = "IOKit/network/IOEthernetInterface.h")] + public const string ETHERNET_INTERFACE_CLASS; + + [CCode (cname = "kIOBSDNameKey", cheader_filename = "IOKit/IOBSD.h")] + public const string BSD_NAME_KEY; + + [CCode (cname = "kIOServicePlane", cheader_filename = "IOKit/IOKitKeys.h")] + public const string IOSERVICE_PLANE; + + [CCode (cname = "io_registry_entry_t", destroy_function = "IOObjectRelease", has_type_id = false)] + public struct IORegistryEntry : IOObject { + [CCode (cname = "IORegistryEntryCreateCFProperties")] + public KernReturn create_cf_properties (out MutableDictionary properties, Allocator? allocator, uint options); + + [CCode (cname = "IORegistryEntryCreateCFProperty")] + public CoreFoundation.Type create_cf_property (String key, Allocator? allocator, uint options); + + [CCode (cname = "IORegistryEntryGetParentEntry")] + public KernReturn get_parent_entry (string plane, out IOObject parent); + } + + [CCode (cname = "io_iterator_t", destroy_function = "IOObjectRelease")] + public struct IOIterator : MachPort { + [CCode (cname = "IOIteratorReset")] + public void reset (); + + [CCode (cname = "IOIteratorIsValid")] + public bool is_valid (); + + [CCode (cname = "IOIteratorNext")] + public IOObject next (); + } + + [CCode (cname = "io_service_t", destroy_function = "IOObjectRelease", has_type_id = false)] + public struct IOService : IOObject { + } + + [CCode (cname = "io_object_t", destroy_function = "IOObjectRelease", has_type_id = false)] + public struct IOObject : MachPort { + [CCode (cname = "IO_OBJECT_NULL")] + public const IOObject NULL; + } +} diff --git a/vapi/darwin-net.vapi b/vapi/darwin-net.vapi new file mode 100644 index 000000000..881c33aff --- /dev/null +++ b/vapi/darwin-net.vapi @@ -0,0 +1,6 @@ +[CCode (cheader_filename = "net/if.h", lower_case_cprefix = "", gir_namespace = "Darwin", gir_version = "1.0")] +namespace Darwin.Net { + public const int IFNAMSIZ; + + public unowned string? if_indextoname (uint ifindex, [CCode (array_length = false)] char[] ifname); +} diff --git a/vapi/darwin-xnu.vapi b/vapi/darwin-xnu.vapi new file mode 100644 index 000000000..caea3f81b --- /dev/null +++ b/vapi/darwin-xnu.vapi @@ -0,0 +1,104 @@ +[CCode (lower_case_cprefix = "", gir_namespace = "Darwin", gir_version = "1.0")] +namespace Darwin.XNU { + [CCode (cname = "kern_return_t", cheader_filename = "mach/mach_error.h", cprefix = "KERN_", has_type_id = false)] + public enum KernReturn { + SUCCESS, + INVALID_ADDRESS, + PROTECTION_FAILURE, + NO_SPACE, + INVALID_ARGUMENT, + FAILURE, + RESOURCE_SHORTAGE, + NOT_RECEIVER, + NO_ACCESS, + MEMORY_FAILURE, + MEMORY_ERROR, + ALREADY_IN_SET, + NOT_IN_SET, + NAME_EXISTS, + ABORTED, + INVALID_NAME, + INVALID_TASK, + INVALID_RIGHT, + INVALID_VALUE, + UREFS_OVERFLOW, + INVALID_CAPABILITY, + RIGHT_EXISTS, + INVALID_HOST, + MEMORY_PRESENT, + MEMORY_DATA_MOVED, + MEMORY_RESTART_COPY, + INVALID_PROCESSOR_SET, + POLICY_LIMIT, + INVALID_POLICY, + INVALID_OBJECT, + ALREADY_WAITING, + DEFAULT_SET, + EXCEPTION_PROTECTED, + INVALID_LEDGER, + INVALID_MEMORY_CONTROL, + INVALID_SECURITY, + NOT_DEPRESSED, + TERMINATED, + LOCK_SET_DESTROYED, + LOCK_UNSTABLE, + LOCK_OWNED, + LOCK_OWNED_SELF, + SEMAPHORE_DESTROYED, + RPC_SERVER_TERMINATED, + RPC_TERMINATE_ORPHAN, + RPC_CONTINUE_ORPHAN, + NOT_SUPPORTED, + NODE_DOWN, + NOT_WAITING, + OPERATION_TIMED_OUT, + CODESIGN_ERROR, + POLICY_STATIC, + INSUFFICIENT_BUFFER_SIZE, + DENIED, + MISSING_KC, + INVALID_KC, + NOT_FOUND, + RETURN_MAX, + } + + [CCode (cheader_filename = "mach/mach_error.h")] + public static unowned string mach_error_string (KernReturn kr); + + [CCode (cname = "mach_port_t", has_type_id = false)] + public struct MachPort : uint { + [CCode (cname = "MACH_PORT_NULL")] + public const MachPort NULL; + } + + [CCode (cheader_filename = "libproc.h")] + public int proc_pidpath (int pid, char[] buffer); + + [CCode (cheader_filename = "sys/sysctl.h")] + public int sysctlbyname (string name, void * oldp, size_t * oldlenp, void * newp = null, size_t newlen = 0); + + [CCode (cname = "struct xinpgen", cheader_filename = "netinet/in_pcb.h")] + public struct InetPcbGeneration { + [CCode (cname = "xig_len")] + public int32 length; + [CCode (cname = "xig_count")] + public uint count; + [CCode (cname = "xig_gen")] + public uint64 generation_count; + [CCode (cname = "xig_sogen")] + public uint64 socket_generation_count; + } + + [CCode (cname = "struct in_addr_4in6", cheader_filename = "netinet/in_pcb.h")] + public struct InetAddr4in6 { + [CCode (cname = "ia46_addr4")] + public Posix.InAddr addr4; + } + + [CCode (cprefix = "INP_", cheader_filename = "netinet/in_pcb.h")] + public enum InetVersionFlags { + IPV4, + IPV6, + V4MAPPEDV6, + } +} diff --git a/vapi/darwin-xpc.vapi b/vapi/darwin-xpc.vapi new file mode 100644 index 000000000..012775759 --- /dev/null +++ b/vapi/darwin-xpc.vapi @@ -0,0 +1,161 @@ +[CCode (cheader_filename = "xpc/xpc.h", lower_case_cprefix = "xpc_", gir_namespace = "Darwin", gir_version = "1.0")] +namespace Darwin.Xpc { + [Compact] + [CCode (cname = "gpointer", ref_function = "xpc_retain", unref_function = "xpc_release")] + public class Connection { + public static Connection create (string? name, GCD.DispatchQueue targetq); + public static Connection create_mach_service (string? name, GCD.DispatchQueue targetq, uint64 flags = 0); + + [CCode (cname = "_frida_xpc_connection_set_event_handler", cheader_filename = "frida-darwin.h")] + public void set_event_handler (Handler handler); + + public void activate (); + public void cancel (); + + public void send_message (Object message); + [CCode (cname = "_frida_xpc_connection_send_message_with_reply", cheader_filename = "frida-darwin.h")] + public void send_message_with_reply (Object message, GCD.DispatchQueue replyq, owned Handler handler); + } + + [CCode (cname = "FridaXpcHandler")] + public delegate void Handler (Object object); + + [Compact] + [CCode (cname = "gpointer", ref_function = "xpc_retain", unref_function = "xpc_release")] + public class Object { + public Type type { + [CCode (cname = "xpc_get_type")] + get; + } + + [CCode (cname = "_frida_xpc_object_to_string", cheader_filename = "frida-darwin.h")] + public string to_string (); + } + + [Compact] + [CCode (cname = "gpointer")] + public class Type { + public string name { + get; + } + } + + [Compact] + [CCode (cname = "gpointer")] + public class Bool : Object { + [CCode (cname = "XPC_TYPE_BOOL")] + public static Type TYPE; + + [CCode (cname = "xpc_bool_create")] + public Bool (bool val); + + public bool get_value (); + } + + [Compact] + [CCode (cname = "gpointer")] + public class Int64 : Object { + [CCode (cname = "XPC_TYPE_INT64")] + public static Type TYPE; + + [CCode (cname = "xpc_int64_create")] + public Int64 (int64 val); + + public int64 get_value (); + } + + [Compact] + [CCode (cname = "gpointer")] + public class UInt64 : Object { + [CCode (cname = "XPC_TYPE_UINT64")] + public static Type TYPE; + + [CCode (cname = "xpc_uint64_create")] + public UInt64 (uint64 val); + + public uint64 get_value (); + } + + [Compact] + [CCode (cname = "gpointer")] + public class String : Object { + [CCode (cname = "XPC_TYPE_STRING")] + public static Type TYPE; + + [CCode (cname = "xpc_string_create")] + public String (string val); + + public unowned string get_string_ptr (); + } + + [Compact] + [CCode (cname = "gpointer")] + public class Uuid : Object { + [CCode (cname = "XPC_TYPE_UUID")] + public static Type TYPE; + + [CCode (array_length = false)] + public unowned uint8[] get_bytes (); + } + + [Compact] + [CCode (cname = "gpointer")] + public class Array : Object { + [CCode (cname = "XPC_TYPE_ARRAY")] + public static Type TYPE; + + public size_t count { + get; + } + + [CCode (cname = "xpc_array_create_empty")] + public Array (); + + public unowned Object? get_value (size_t index); + public void set_value (size_t index, Object val); + } + + [Compact] + [CCode (cname = "gpointer")] + public class Dictionary : Object { + [CCode (cname = "XPC_TYPE_DICTIONARY")] + public static Type TYPE; + + [CCode (cname = "xpc_dictionary_create_empty")] + public Dictionary (); + + public bool get_bool (string key); + public void set_bool (string key, bool val); + + public int64 get_int64 (string key); + public void set_int64 (string key, int64 val); + + public uint64 get_uint64 (string key); + public void set_uint64 (string key, uint64 val); + + public unowned string? get_string (string key); + public void set_string (string key, string val); + + public unowned Dictionary? get_dictionary (string key); + + public unowned Object? get_value (string key); + public void set_value (string key, Object val); + + [CCode (cname = "_frida_xpc_dictionary_apply", cheader_filename = "frida-darwin.h")] + public bool apply (DictionaryApplier applier); + + public Connection? create_connection (string key); + } + + [CCode (cname = "FridaXpcDictionaryApplier")] + public delegate bool DictionaryApplier (string key, Object val); + + [Compact] + [CCode (cname = "gpointer")] + public class Error : Dictionary { + [CCode (cname = "XPC_TYPE_ERROR")] + public static Type TYPE; + + public const string KEY_DESCRIPTION; + } +} diff --git a/vapi/libnghttp2.vapi b/vapi/libnghttp2.vapi new file mode 100644 index 000000000..b734c25c2 --- /dev/null +++ b/vapi/libnghttp2.vapi @@ -0,0 +1,358 @@ +[CCode (cheader_filename = "nghttp2/nghttp2.h", cprefix = "nghttp2_", gir_namespace = "NGHttp2", gir_version = "1.0")] +namespace NGHttp2 { + [Compact] + [CCode (cname = "nghttp2_session", cprefix = "nghttp2_session_", free_function = "nghttp2_session_del")] + public class Session { + [CCode (cname = "nghttp2_session_client_new2")] + public static int make_client (out Session session, SessionCallbacks callbacks, void * user_data, Option? option = null); + + [CCode (cname = "nghttp2_submit_settings")] + public int submit_settings (uint8 flags, SettingsEntry[] entries); + + [CCode (cname = "nghttp2_submit_window_update")] + public int submit_window_update (uint8 flags, int32 stream_id, int32 window_size_increment); + + public int set_local_window_size (uint8 flags, int32 stream_id, int32 window_size); + + [CCode (cname = "nghttp2_submit_request")] + public int32 submit_request (PrioritySpec? pri_spec, NV[] nvs, DataProvider? data_prd, void * stream_user_data); + + [CCode (cname = "nghttp2_submit_headers")] + public int32 submit_headers (uint8 flags, int32 stream_id, PrioritySpec? pri_spec, NV[] nvs, void * stream_user_data); + + [CCode (cname = "nghttp2_submit_data")] + public int submit_data (uint8 flags, int32 stream_id, DataProvider data_prd); + + public int send (); + + public ssize_t mem_recv (uint8[] input); + + public bool want_read (); + + public bool want_write (); + + public int consume_connection (size_t size); + } + + [Compact] + [CCode (cname = "nghttp2_session_callbacks", cprefix = "nghttp2_session_callbacks_", + free_function = "nghttp2_session_callbacks_del")] + public class SessionCallbacks { + [CCode (cname = "nghttp2_session_callbacks_new")] + public static int make (out SessionCallbacks callbacks); + + public void set_send_callback (SendCallback callback); + public void set_on_frame_recv_callback (OnFrameRecvCallback callback); + public void set_on_data_chunk_recv_callback (OnDataChunkRecvCallback callback); + public void set_before_frame_send_callback (BeforeFrameSendCallback callback); + public void set_on_frame_send_callback (OnFrameSendCallback callback); + public void set_on_frame_not_send_callback (OnFrameNotSendCallback callback); + public void set_on_stream_close_callback (OnStreamCloseCallback callback); + public void set_on_begin_frame_callback (OnBeginFrameCallback callback); + [CCode (cname = "nghttp2_session_callbacks_set_error_callback2")] + public void set_error_callback (ErrorCallback callback); + } + + [CCode (cname = "nghttp2_send_callback", has_target = false)] + public delegate ssize_t SendCallback (Session session, [CCode (array_length_type = "size_t")] uint8[] data, int flags, + void * user_data); + + [CCode (cname = "nghttp2_on_frame_recv_callback", has_target = false)] + public delegate int OnFrameRecvCallback (Session session, Frame frame, void * user_data); + + [CCode (cname = "nghttp2_on_data_chunk_recv_callback", has_target = false)] + public delegate int OnDataChunkRecvCallback (Session session, uint8 flags, int32 stream_id, + [CCode (array_length_type = "size_t")] uint8[] data, void * user_data); + + [CCode (cname = "nghttp2_before_frame_send_callback", has_target = false)] + public delegate int BeforeFrameSendCallback (Session session, Frame frame, void * user_data); + + [CCode (cname = "nghttp2_on_frame_send_callback", has_target = false)] + public delegate int OnFrameSendCallback (Session session, Frame frame, void * user_data); + + [CCode (cname = "nghttp2_on_frame_not_send_callback", has_target = false)] + public delegate int OnFrameNotSendCallback (Session session, Frame frame, ErrorCode lib_error_code, void * user_data); + + [CCode (cname = "nghttp2_on_stream_close_callback", has_target = false)] + public delegate int OnStreamCloseCallback (Session session, int32 stream_id, uint32 error_code, void * user_data); + + [CCode (cname = "nghttp2_on_begin_frame_callback", has_target = false)] + public delegate int OnBeginFrameCallback (Session session, FrameHd hd, void * user_data); + + [CCode (cname = "nghttp2_error_callback2", has_target = false)] + public delegate int ErrorCallback (Session session, ErrorCode code, [CCode (array_length_type = "size_t")] char[] msg, + void * user_data); + + [CCode (cname = "nghttp2_frame")] + public struct Frame { + public FrameHd hd; + public DataFrame data; + public HeadersFrame headers; + public PriorityFrame priority; + public RstStreamFrame rst_stream; + public SettingsFrame settings; + public PushPromiseFrame push_promise; + public PingFrame ping; + public GoawayFrame goaway; + public WindowUpdateFrame window_update; + public ExtensionFrame ext; + } + + [CCode (cname = "nghttp2_frame_type", cprefix = "NGHTTP2_", has_type_id = false)] + public enum FrameType { + DATA, + HEADERS, + PRIORITY, + RST_STREAM, + SETTINGS, + PUSH_PROMISE, + PING, + GOAWAY, + WINDOW_UPDATE, + CONTINUATION, + ALTSVC, + ORIGIN, + PRIORITY_UPDATE, + } + + [CCode (cname = "nghttp2_frame_hd")] + public struct FrameHd { + public size_t length; + public int32 stream_id; + public FrameType type; + public uint8 flags; + public uint8 reserved; + } + + [CCode (cname = "nghttp2_data")] + public struct DataFrame { + public FrameHd hd; + public size_t padlen; + } + + [CCode (cname = "nghttp2_headers")] + public struct HeadersFrame { + public FrameHd hd; + public size_t padlen; + public PrioritySpec pri_spec; + public NV * nva; + public size_t nvlen; + public HeadersCategory cat; + } + + [CCode (cname = "nghttp2_headers_category", cprefix = "NGHTTP2_HCAT_", has_type_id = false)] + public enum HeadersCategory { + REQUEST, + RESPONSE, + PUSH_RESPONSE, + HEADERS, + } + + [CCode (cname = "nghttp2_priority")] + public struct PriorityFrame { + public FrameHd hd; + public PrioritySpec pri_spec; + } + + [CCode (cname = "nghttp2_rst_stream")] + public struct RstStreamFrame { + public FrameHd hd; + public uint32 error_code; + } + + [CCode (cname = "nghttp2_settings")] + public struct SettingsFrame { + public FrameHd hd; + public size_t niv; + public SettingsEntry * iv; + } + + [CCode (cname = "nghttp2_push_promise")] + public struct PushPromiseFrame { + public FrameHd hd; + public size_t padlen; + public NV * nva; + public size_t nvlen; + public int32 promised_stream_id; + public uint8 reserved; + } + + [CCode (cname = "nghttp2_ping")] + public struct PingFrame { + public FrameHd hd; + public uint8[] opaque_data; + } + + [CCode (cname = "nghttp2_goaway")] + public struct GoawayFrame { + public FrameHd hd; + public int32 last_stream_id; + public uint32 error_code; + public uint8 * opaque_data; + public size_t opaque_data_len; + public uint8 reserved; + } + + [CCode (cname = "nghttp2_window_update")] + public struct WindowUpdateFrame { + public FrameHd hd; + public int32 window_size_increment; + public uint8 reserved; + } + + [CCode (cname = "nghttp2_extension")] + public struct ExtensionFrame { + public FrameHd hd; + public void * payload; + } + + [Compact] + [CCode (cname = "nghttp2_option", cprefix = "nghttp2_option_", free_function = "nghttp2_option_del")] + public class Option { + [CCode (cname = "nghttp2_option_new")] + public static int make (out Option option); + + public void set_no_auto_window_update (bool val); + public void set_peer_max_concurrent_streams (uint32 val); + public void set_no_recv_client_magic (bool val); + public void set_no_http_messaging (bool val); + public void set_no_http_semantics (bool val); + public void set_max_reserved_remote_streams (uint32 val); + public void set_user_recv_extension_type (uint8 type); + public void set_builtin_recv_extension_type (uint8 type); + public void set_no_auto_ping_ack (bool val); + public void set_max_send_header_block_length (size_t val); + public void set_max_deflate_dynamic_table_size (size_t val); + public void set_no_closed_streams (bool val); + public void set_max_outbound_ack (size_t val); + public void set_max_settings (size_t val); + public void set_server_fallback_rfc7540_priorities (bool val); + public void set_no_rfc9113_leading_and_trailing_ws_validation (bool val); + } + + [CCode (cname = "nghttp2_settings_entry")] + public struct SettingsEntry { + public SettingsId settings_id; + public uint32 value; + } + + [CCode (cname = "nghttp2_settings_id", cprefix = "NGHTTP2_SETTINGS_", has_type_id = false)] + public enum SettingsId { + HEADER_TABLE_SIZE, + ENABLE_PUSH, + MAX_CONCURRENT_STREAMS, + INITIAL_WINDOW_SIZE, + MAX_FRAME_SIZE, + MAX_HEADER_LIST_SIZE, + ENABLE_CONNECT_PROTOCOL, + NO_RFC7540_PRIORITIES, + } + + [CCode (cname = "nghttp2_priority_spec")] + public struct PrioritySpec { + public int32 stream_id; + public int32 weight; + public uint8 exclusive; + } + + [CCode (cname = "nghttp2_nv")] + public struct NV { + public uint8 * name; + public uint8 * value; + public size_t namelen; + public size_t valuelen; + public NVFlag flags; + } + + [CCode (cname = "nghttp2_nv_flag", cprefix = "NGHTTP2_NV_FLAG_", has_type_id = false)] + [Flags] + public enum NVFlag { + NONE, + NO_INDEX, + NO_COPY_NAME, + NO_COPY_VALUE, + } + + [CCode (cname = "nghttp2_data_provider")] + public struct DataProvider { + public DataSource source; + public DataSourceReadCallback read_callback; + } + + [CCode (cname = "nghttp2_data_source")] + public struct DataSource { + public void * ptr; + } + + [CCode (cname = "nghttp2_data_source_read_callback", has_target = false)] + public delegate ssize_t DataSourceReadCallback (Session session, int32 stream_id, + [CCode (array_length_type = "size_t")] uint8[] buf, ref uint32 data_flags, DataSource source, void * user_data); + + [CCode (cname = "nghttp2_strerror")] + public unowned string strerror (ssize_t result); + + [CCode (cname = "nghttp2_error", cprefix = "NGHTTP2_ERR_", has_type_id = false)] + public enum ErrorCode { + INVALID_ARGUMENT, + BUFFER_ERROR, + UNSUPPORTED_VERSION, + WOULDBLOCK, + PROTO, + INVALID_FRAME, + EOF, + DEFERRED, + STREAM_ID_NOT_AVAILABLE, + STREAM_CLOSED, + STREAM_CLOSING, + STREAM_SHUT_WR, + INVALID_STREAM_ID, + INVALID_STREAM_STATE, + DEFERRED_DATA_EXIST, + START_STREAM_NOT_ALLOWED, + GOAWAY_ALREADY_SENT, + INVALID_HEADER_BLOCK, + INVALID_STATE, + TEMPORAL_CALLBACK_FAILURE, + FRAME_SIZE_ERROR, + HEADER_COMP, + FLOW_CONTROL, + INSUFF_BUFSIZE, + PAUSE, + TOO_MANY_INFLIGHT_SETTINGS, + PUSH_DISABLED, + DATA_EXIST, + SESSION_CLOSING, + HTTP_HEADER, + HTTP_MESSAGING, + REFUSED_STREAM, + INTERNAL, + CANCEL, + SETTINGS_EXPECTED, + TOO_MANY_SETTINGS, + FATAL, + NOMEM, + CALLBACK_FAILURE, + BAD_CLIENT_MAGIC, + FLOODED, + } + + [CCode (cname = "nghttp2_flag", cprefix = "NGHTTP2_FLAG_", has_type_id = false)] + [Flags] + public enum Flag { + NONE, + END_STREAM, + END_HEADERS, + ACK, + PADDED, + PRIORITY, + } + + [CCode (cname = "nghttp2_data_flag", cprefix = "NGHTTP2_DATA_FLAG_", has_type_id = false)] + [Flags] + public enum DataFlag { + NONE, + EOF, + NO_END_STREAM, + NO_COPY, + } +} diff --git a/vapi/libngtcp2.vapi b/vapi/libngtcp2.vapi new file mode 100644 index 000000000..8e1349dfc --- /dev/null +++ b/vapi/libngtcp2.vapi @@ -0,0 +1,535 @@ +[CCode (cheader_filename = "ngtcp2/ngtcp2.h", lower_case_cprefix = "ngtcp2_", gir_namespace = "NGTcp2", gir_version = "1.0")] +namespace NGTcp2 { + [Compact] + [CCode (cname = "ngtcp2_conn", cprefix = "ngtcp2_conn_", free_function = "ngtcp2_conn_del")] + public class Connection { + [CCode (cname = "ngtcp2_conn_client_new")] + public static int make_client (out Connection conn, ConnectionID dcid, ConnectionID scid, Path path, + ProtocolVersion client_chosen_version, Callbacks callbacks, Settings settings, TransportParams params, + MemoryAllocator? mem, void * user_data); + + public void set_tls_native_handle (void * tls_native_handle); + + public void set_keep_alive_timeout (Duration timeout); + + public Timestamp get_expiry (); + public int handle_expiry (Timestamp ts); + + [CCode (cname = "ngtcp2_conn_read_pkt")] + public int read_packet (Path path, PacketInfo * pi, uint8[] pkt, Timestamp ts); + + public int open_bidi_stream (out int64 stream_id, void * stream_user_data); + + public ssize_t write_stream (Path * path, PacketInfo * pi, uint8[] dest, ssize_t * pdatalen, WriteStreamFlags flags, + int64 stream_id, uint8[]? data, Timestamp ts); + public ssize_t writev_stream (Path * path, PacketInfo * pi, uint8[] dest, ssize_t * pdatalen, WriteStreamFlags flags, + int64 stream_id, IOVector[] datav, Timestamp ts); + + public ssize_t write_datagram (Path * path, PacketInfo * pi, uint8[] dest, int * paccepted, uint32 flags, uint64 dgram_id, + uint8[] data, Timestamp ts); + public ssize_t writev_datagram (Path * path, PacketInfo * pi, uint8[] dest, int * paccepted, uint32 flags, uint64 dgram_id, + IOVector[] datav, Timestamp ts); + + public uint64 get_max_data_left (); + public uint64 get_max_stream_data_left (int64 stream_id); + } + + public unowned string strerror (int liberr); + + [CCode (cname = "int", cprefix = "NGTCP2_ERR_", has_type_id = false)] + public enum ErrorCode { + INVALID_ARGUMENT, + NOBUF, + PROTO, + INVALID_STATE, + ACK_FRAME, + STREAM_ID_BLOCKED, + STREAM_IN_USE, + STREAM_DATA_BLOCKED, + FLOW_CONTROL, + CONNECTION_ID_LIMIT, + STREAM_LIMIT, + FINAL_SIZE, + CRYPTO, + PKT_NUM_EXHAUSTED, + REQUIRED_TRANSPORT_PARAM, + MALFORMED_TRANSPORT_PARAM, + FRAME_ENCODING, + DECRYPT, + STREAM_SHUT_WR, + STREAM_NOT_FOUND, + STREAM_STATE, + RECV_VERSION_NEGOTIATION, + CLOSING, + DRAINING, + TRANSPORT_PARAM, + DISCARD_PKT, + CONN_ID_BLOCKED, + INTERNAL, + CRYPTO_BUFFER_EXCEEDED, + WRITE_MORE, + RETRY, + DROP_CONN, + AEAD_LIMIT_REACHED, + NO_VIABLE_PATH, + VERSION_NEGOTIATION, + HANDSHAKE_TIMEOUT, + VERSION_NEGOTIATION_FAILURE, + IDLE_CLOSE, + FATAL, + NOMEM, + CALLBACK_FAILURE, + } + + [CCode (cname = "ngtcp2_cid", destroy_function = "")] + public struct ConnectionID { + public size_t datalen; + public uint8 data[MAX_CIDLEN]; + } + + [CCode (cname = "ngtcp2_connection_id_status_type", cprefix = "NGTCP2_CONNECTION_ID_STATUS_TYPE_", has_type_id = false)] + public enum ConnectionIdStatusType { + ACTIVATE, + DEACTIVATE, + } + + [CCode (cname = "ngtcp2_path", destroy_function = "")] + public struct Path { + public Address local; + public Address remote; + public void * user_data; + } + + [CCode (cname = "ngtcp2_path_validation_result", cprefix = "NGTCP2_PATH_VALIDATION_RESULT_", has_type_id = false)] + public enum PathValidationResult { + SUCCESS, + FAILURE, + ABORTED, + } + + [CCode (cname = "uint32_t", cprefix = "NGTCP2_PROTO_VER_", has_type_id = false)] + public enum ProtocolVersion { + V1, + V2, + MIN, + MAX, + } + + [CCode (cname = "ngtcp2_pkt_info", destroy_function = "")] + public struct PacketInfo { + public uint8 ecn; + } + + [CCode (cname = "ngtcp2_pkt_hd", destroy_function = "")] + public struct PacketHeader { + public ConnectionID dcid; + public ConnectionID scid; + public int64 pkt_num; + [CCode (array_length_cname = "tokenlen")] + public uint8[]? token; + public size_t pkt_numlen; + public size_t len; + public uint32 version; + public uint8 type; + public uint8 flags; + } + + [CCode (cname = "ngtcp2_pkt_stateless_reset", destroy_function = "")] + public struct PacketStatelessReset { + public uint8 stateless_reset_token[STATELESS_RESET_TOKENLEN]; + [CCode (array_length_cname = "randlen")] + public uint8[] rand; + } + + [Flags] + [CCode (cname = "uint32_t", cprefix = "NGTCP2_WRITE_STREAM_FLAG_", has_type_id = false)] + public enum WriteStreamFlags { + NONE, + MORE, + FIN, + } + + [CCode (cname = "NGTCP2_MAX_CIDLEN")] + public const size_t MAX_CIDLEN; + [CCode (cname = "NGTCP2_MIN_INITIAL_DCIDLEN")] + public const size_t MIN_INITIAL_DCIDLEN; + + [CCode (cname = "NGTCP2_STATELESS_RESET_TOKENLEN")] + public const size_t STATELESS_RESET_TOKENLEN; + + [CCode (cname = "ngtcp2_vec", destroy_function = "")] + public struct IOVector { + [CCode (array_length_cname = "len")] + public unowned uint8[] base; + } + + [CCode (cname = "ngtcp2_sa_family", cprefix = "NGTCP2_AF_", has_type_id = false)] + public enum SocketAddressFamily { + INET, + INET6, + } + + [SimpleType] + [CCode (cname = "ngtcp2_in_port")] + public struct InternetPort : uint16 { + } + + [CCode (cname = "ngtcp2_sockaddr", destroy_function = "")] + public struct SocketAddress { + public SocketAddressFamily sa_family; + public uint8 sa_data[14]; + } + + [CCode (cname = "ngtcp2_in_addr", destroy_function = "")] + public struct InternetAddress { + public uint32 s_addr; + } + + [CCode (cname = "ngtcp2_sockaddr_in", destroy_function = "")] + public struct SocketAddressInternet { + public SocketAddressFamily sin_family; + public InternetPort sin_port; + public InternetAddress sin_addr; + public uint8 sin_zero[8]; + } + + [CCode (cname = "ngtcp2_in6_addr", destroy_function = "")] + public struct Internet6Address { + public uint8 in6_addr[16]; + } + + [CCode (cname = "ngtcp2_sockaddr_in6", destroy_function = "")] + public struct SocketAddressInternet6 { + public SocketAddressFamily sin6_family; + public InternetPort sin6_port; + public uint32 sin6_flowinfo; + public Internet6Address sin6_addr; + public uint32 sin6_scope_id; + } + + [SimpleType] + [CCode (cname = "ngtcp2_socklen")] + public struct SocketLength : uint32 { + } + + [CCode (cname = "ngtcp2_addr", destroy_function = "")] + public struct Address { + [CCode (array_length_cname = "addrlen")] + public uint8[] addr; + } + + [CCode (cname = "ngtcp2_preferred_addr", destroy_function = "")] + public struct PreferredAddress { + public ConnectionID cid; + public SocketAddressInternet ipv4; + public SocketAddressInternet6 ipv6; + public uint8 ipv4_present; + public uint8 ipv6_present; + public uint8 stateless_reset_token[STATELESS_RESET_TOKENLEN]; + } + + [CCode (cname = "ngtcp2_version_info", destroy_function = "")] + public struct VersionInfo { + public uint32 chosen_version; + [CCode (array_length_cname = "available_versionslen")] + public uint8[] available_versions; + } + + [CCode (cname = "ngtcp2_encryption_level", cprefix = "NGTCP2_ENCRYPTION_LEVEL_", has_type_id = false)] + public enum EncryptionLevel { + INITIAL, + HANDSHAKE, + 1RTT, + 0RTT, + } + + [CCode (cname = "ngtcp2_token_type", cprefix = "NGTCP2_TOKEN_TYPE_", has_type_id = false)] + public enum TokenType { + UNKNOWN, + RETRY, + NEW_TOKEN, + } + + [CCode (cname = "ngtcp2_rand_ctx", destroy_function = "")] + public struct RNGContext { + public void * native_handle; + } + + [CCode (cname = "ngtcp2_crypto_aead", destroy_function = "")] + public struct CryptoAead { + public void * native_handle; + public size_t max_overhead; + } + + [CCode (cname = "ngtcp2_crypto_cipher", destroy_function = "")] + public struct CryptoCipher { + public void * native_handle; + } + + [CCode (cname = "ngtcp2_crypto_aead_ctx", destroy_function = "")] + public struct CryptoAeadCtx { + public void * native_handle; + } + + [CCode (cname = "ngtcp2_crypto_cipher_ctx", destroy_function = "")] + public struct CryptoCipherCtx { + public void * native_handle; + } + + [CCode (cname = "ngtcp2_cc_algo", cprefix = "NGTCP2_CC_ALGO_", has_type_id = false)] + public enum CongestionControlAlgorithm { + RENO, + CUBIC, + BBR, + } + + [SimpleType] + [CCode (cname = "ngtcp2_tstamp")] + public struct Timestamp : uint64 { + } + + [CCode (cname = "NGTCP2_SECONDS")] + public const uint64 SECONDS; + + [CCode (cname = "NGTCP2_MILLISECONDS")] + public const uint64 MILLISECONDS; + + [CCode (cname = "NGTCP2_MICROSECONDS")] + public const uint64 MICROSECONDS; + + [CCode (cname = "NGTCP2_NANOSECONDS")] + public const uint64 NANOSECONDS; + + [SimpleType] + [CCode (cname = "ngtcp2_duration")] + public struct Duration : uint64 { + } + + [CCode (cname = "ngtcp2_callbacks", destroy_function = "")] + public struct Callbacks { + public ClientInitial? client_initial; + public RecvClientInitial? recv_client_initial; + public RecvCryptoData recv_crypto_data; + public HandshakeCompleted? handshake_completed; + public RecvVersionNegotiation? recv_version_negotiation; + public Encrypt encrypt; + public Decrypt decrypt; + public HpMask hp_mask; + public RecvStreamData? recv_stream_data; + public AckedStreamDataOffset? acked_stream_data_offset; + public StreamOpen? stream_open; + public StreamClose? stream_close; + public RecvStatelessReset? recv_stateless_reset; + public RecvRetry? recv_retry; + public ExtendMaxStreams? extend_max_local_streams_bidi; + public ExtendMaxStreams? extend_max_local_streams_uni; + public Rand? rand; + public GetNewConnectionId get_new_connection_id; + public RemoveConnectionId? remove_connection_id; + public UpdateKey update_key; + public PathValidation? path_validation; + public SelectPreferredAddr? select_preferred_addr; + public StreamReset? stream_reset; + public ExtendMaxStreams? extend_max_remote_streams_bidi; + public ExtendMaxStreams? extend_max_remote_streams_uni; + public ExtendMaxStreamData? extend_max_stream_data; + public ConnectionIdStatus? dcid_status; + public HandshakeConfirmed? handshake_confirmed; + public RecvNewToken? recv_new_token; + public DeleteCryptoAeadCtx delete_crypto_aead_ctx; + public DeleteCryptoCipherCtx delete_crypto_cipher_ctx; + public RecvDatagram? recv_datagram; + public AckDatagram? ack_datagram; + public LostDatagram? lost_datagram; + public GetPathChallengeData get_path_challenge_data; + public StreamStopSending? stream_stop_sending; + public VersionNegotiation version_negotiation; + public RecvKey recv_rx_key; + public RecvKey recv_tx_key; + public TlsEarlyDataRejected? tls_early_data_rejected; + } + + [CCode (cname = "ngtcp2_client_initial", has_target = false)] + public delegate int ClientInitial (Connection conn, void * user_data); + [CCode (cname = "ngtcp2_recv_client_initial", has_target = false)] + public delegate int RecvClientInitial (Connection conn, ConnectionID dcid, void * user_data); + [CCode (cname = "ngtcp2_recv_crypto_data", has_target = false)] + public delegate int RecvCryptoData (Connection conn, EncryptionLevel encryption_level, uint64 offset, + [CCode (array_length_type = "size_t")] uint8[] data, void * user_data); + [CCode (cname = "ngtcp2_handshake_completed", has_target = false)] + public delegate int HandshakeCompleted (Connection conn, void * user_data); + [CCode (cname = "ngtcp2_recv_version_negotiation", has_target = false)] + public delegate int RecvVersionNegotiation (Connection conn, PacketHeader hd, [CCode (array_length_type = "size_t")] uint32[] sv, + void * user_data); + [CCode (cname = "ngtcp2_encrypt", has_target = false)] + public delegate int Encrypt ([CCode (array_length = false)] uint8[] dest, CryptoAead aead, CryptoAeadCtx aead_ctx, + [CCode (array_length_type = "size_t")] uint8[] plaintext, + [CCode (array_length_type = "size_t")] uint8[] nonce, + [CCode (array_length_type = "size_t")] uint8[] aad); + [CCode (cname = "ngtcp2_decrypt", has_target = false)] + public delegate int Decrypt ([CCode (array_length = false)] uint8[] dest, CryptoAead aead, CryptoAeadCtx aead_ctx, + [CCode (array_length_type = "size_t")] uint8[] ciphertext, + [CCode (array_length_type = "size_t")] uint8[] nonce, + [CCode (array_length_type = "size_t")] uint8[] aad); + [CCode (cname = "ngtcp2_hp_mask", has_target = false)] + public delegate int HpMask ([CCode (array_length = false)] uint8[] dest, CryptoCipher hp, CryptoCipherCtx hp_ctx, + [CCode (array_length = false)] uint8[] sample); + [CCode (cname = "ngtcp2_recv_stream_data", has_target = false)] + public delegate int RecvStreamData (Connection conn, uint32 flags, int64 stream_id, uint64 offset, + [CCode (array_length_type = "size_t")] uint8[] data, void * user_data, void * stream_user_data); + [CCode (cname = "ngtcp2_acked_stream_data_offset", has_target = false)] + public delegate int AckedStreamDataOffset (Connection conn, int64 stream_id, uint64 offset, uint64 datalen, void * user_data, + void * stream_user_data); + [CCode (cname = "ngtcp2_stream_open", has_target = false)] + public delegate int StreamOpen (Connection conn, int64 stream_id, void * user_data); + [CCode (cname = "ngtcp2_stream_close", has_target = false)] + public delegate int StreamClose (Connection conn, uint32 flags, int64 stream_id, uint64 app_error_code, void * user_data, + void * stream_user_data); + [CCode (cname = "ngtcp2_recv_stateless_reset", has_target = false)] + public delegate int RecvStatelessReset (Connection conn, PacketStatelessReset sr, void * user_data); + [CCode (cname = "ngtcp2_recv_retry", has_target = false)] + public delegate int RecvRetry (Connection conn, PacketHeader hd, void * user_data); + [CCode (cname = "ngtcp2_extend_max_streams", has_target = false)] + public delegate int ExtendMaxStreams (Connection conn, uint64 max_streams, void * user_data); + [CCode (cname = "ngtcp2_extend_max_stream_data", has_target = false)] + public delegate int ExtendMaxStreamData (Connection conn, int64 stream_id, uint64 max_data, void * user_data, + void * stream_user_data); + [CCode (cname = "ngtcp2_rand", has_target = false)] + public delegate void Rand ([CCode (array_length_type = "size_t")] uint8[] dest, RNGContext rand_ctx); + [CCode (cname = "ngtcp2_get_new_connection_id", has_target = false)] + public delegate int GetNewConnectionId (Connection conn, out ConnectionID cid, [CCode (array_length = false)] uint8[] token, + size_t cidlen, void * user_data); + [CCode (cname = "ngtcp2_remove_connection_id", has_target = false)] + public delegate int RemoveConnectionId (Connection conn, ConnectionID cid, void * user_data); + [CCode (cname = "ngtcp2_update_key", has_target = false)] + public delegate int UpdateKey (Connection conn, + [CCode (array_length = false)] uint8[] rx_secret, + [CCode (array_length = false)] uint8[] tx_secret, + CryptoAeadCtx rx_aead_ctx, [CCode (array_length = false)] uint8[] rx_iv, + CryptoAeadCtx tx_aead_ctx, [CCode (array_length = false)] uint8[] tx_iv, + [CCode (array_length_pos = 9.1)] uint8[] current_rx_secret, + [CCode (array_length_pos = 9.1)] uint8[] current_tx_secret, + void * user_data); + [CCode (cname = "ngtcp2_path_validation", has_target = false)] + public delegate int PathValidation (Connection conn, uint32 flags, Path path, Path old_path, PathValidationResult res, + void * user_data); + [CCode (cname = "ngtcp2_select_preferred_addr", has_target = false)] + public delegate int SelectPreferredAddr (Connection conn, Path dest, PreferredAddress paddr, void * user_data); + [CCode (cname = "ngtcp2_stream_reset", has_target = false)] + public delegate int StreamReset (Connection conn, int64 stream_id, uint64 final_size, uint64 app_error_code, void * user_data, + void * stream_user_data); + [CCode (cname = "ngtcp2_connection_id_status", has_target = false)] + public delegate int ConnectionIdStatus (Connection conn, ConnectionIdStatusType type, uint64 seq, ConnectionID cid, + [CCode (array_length = false)] uint8[] token, void * user_data); + [CCode (cname = "ngtcp2_handshake_confirmed", has_target = false)] + public delegate int HandshakeConfirmed (Connection conn, void * user_data); + [CCode (cname = "ngtcp2_recv_new_token", has_target = false)] + public delegate int RecvNewToken (Connection conn, [CCode (array_length_type = "size_t")] uint8[] token, void * user_data); + [CCode (cname = "ngtcp2_delete_crypto_aead_ctx", has_target = false)] + public delegate void DeleteCryptoAeadCtx (Connection conn, CryptoAeadCtx aead_ctx, void * user_data); + [CCode (cname = "ngtcp2_delete_crypto_cipher_ctx", has_target = false)] + public delegate void DeleteCryptoCipherCtx (Connection conn, CryptoCipherCtx cipher_ctx, void * user_data); + [CCode (cname = "ngtcp2_recv_datagram", has_target = false)] + public delegate int RecvDatagram (Connection conn, uint32 flags, [CCode (array_length_type = "size_t")] uint8[] data, + void * user_data); + [CCode (cname = "ngtcp2_ack_datagram", has_target = false)] + public delegate int AckDatagram (Connection conn, uint64 dgram_id, void * user_data); + [CCode (cname = "ngtcp2_lost_datagram", has_target = false)] + public delegate int LostDatagram (Connection conn, uint64 dgram_id, void * user_data); + [CCode (cname = "ngtcp2_get_path_challenge_data", has_target = false)] + public delegate int GetPathChallengeData (Connection conn, [CCode (array_length = false)] uint8[] data, void * user_data); + [CCode (cname = "ngtcp2_stream_stop_sending", has_target = false)] + public delegate int StreamStopSending (Connection conn, int64 stream_id, uint64 app_error_code, void * user_data, + void * stream_user_data); + [CCode (cname = "ngtcp2_version_negotiation", has_target = false)] + public delegate int VersionNegotiation (Connection conn, uint32 version, ConnectionID client_dcid, void * user_data); + [CCode (cname = "ngtcp2_recv_key", has_target = false)] + public delegate int RecvKey (Connection conn, EncryptionLevel level, void * user_data); + [CCode (cname = "ngtcp2_tls_early_data_rejected", has_target = false)] + public delegate int TlsEarlyDataRejected (Connection conn, void * user_data); + + [CCode (cname = "ngtcp2_settings", destroy_function = "")] + public struct Settings { + [CCode (cname = "ngtcp2_settings_default")] + public Settings.make_default (); + + public QlogWrite? qlog_write; + public CongestionControlAlgorithm cc_algo; + public Timestamp initial_ts; + public Duration initial_rtt; + public Printf? log_printf; + public size_t max_tx_udp_payload_size; + [CCode (array_length_cname = "tokenlen")] + public uint8[]? token; + public TokenType token_type; + public RNGContext rand_ctx; + public uint64 max_window; + public uint64 max_stream_window; + public size_t ack_thresh; + public bool no_tx_udp_payload_size_shaping; + public Duration handshake_timeout; + [CCode (array_length_cname = "preferred_versionslen")] + public ProtocolVersion[]? preferred_versions; + [CCode (array_length_cname = "available_versionslen")] + public ProtocolVersion[]? available_versions; + public uint32 original_version; + public bool no_pmtud; + public uint32 initial_pkt_num; + } + + [CCode (cname = "ngtcp2_qlog_write", has_target = false)] + public delegate void QlogWrite (void * user_data, uint32 flags, [CCode (array_length_type = "size_t")] uint8[] data); + [CCode (cname = "ngtcp2_printf", has_target = false)] + public delegate void Printf (void * user_data, string format, ...); + + [CCode (cname = "ngtcp2_transport_params", destroy_function = "")] + public struct TransportParams { + [CCode (cname = "ngtcp2_transport_params_default")] + public TransportParams.make_default (); + + public PreferredAddress preferred_addr; + public ConnectionID original_dcid; + public ConnectionID initial_scid; + public ConnectionID retry_scid; + public uint64 initial_max_stream_data_bidi_local; + public uint64 initial_max_stream_data_bidi_remote; + public uint64 initial_max_stream_data_uni; + public uint64 initial_max_data; + public uint64 initial_max_streams_bidi; + public uint64 initial_max_streams_uni; + public Duration max_idle_timeout; + public uint64 max_udp_payload_size; + public uint64 active_connection_id_limit; + public uint64 ack_delay_exponent; + public Duration max_ack_delay; + public uint64 max_datagram_frame_size; + public bool stateless_reset_token_present; + public bool disable_active_migration; + public bool original_dcid_present; + public bool initial_scid_present; + public bool retry_scid_present; + public bool preferred_addr_present; + public uint8 stateless_reset_token[STATELESS_RESET_TOKENLEN]; + public bool grease_quic_bit; + public VersionInfo version_info; + public bool version_info_present; + } + + [CCode (cname = "ngtcp2_mem", destroy_function = "")] + public struct MemoryAllocator { + public void * user_data; + public Malloc malloc; + public Free free; + public Calloc calloc; + public Realloc realloc; + } + + [CCode (cname = "ngtcp2_malloc", has_target = false)] + public delegate void * Malloc (size_t size, void * user_data); + [CCode (cname = "ngtcp2_free", has_target = false)] + public delegate void Free (void * ptr, void * user_data); + [CCode (cname = "ngtcp2_calloc", has_target = false)] + public delegate void * Calloc (size_t nmemb, size_t size, void * user_data); + [CCode (cname = "ngtcp2_realloc", has_target = false)] + public delegate void * Realloc (void * ptr, size_t size, void * user_data); +} diff --git a/vapi/libngtcp2_crypto_quictls.vapi b/vapi/libngtcp2_crypto_quictls.vapi new file mode 100644 index 000000000..4641f16be --- /dev/null +++ b/vapi/libngtcp2_crypto_quictls.vapi @@ -0,0 +1,28 @@ +[CCode (cheader_filename = "ngtcp2/ngtcp2_crypto.h", lower_case_cprefix = "ngtcp2_crypto_", gir_namespace = "NGTcp2Crypto", gir_version = "1.0")] +namespace NGTcp2.Crypto { + [CCode (cheader_filename = "ngtcp2/ngtcp2_crypto_quictls.h")] + namespace Quictls { + public int configure_client_context (OpenSSL.SSLContext ssl_ctx); + } + + [CCode (cname = "ngtcp2_crypto_conn_ref", destroy_function = "")] + public struct ConnectionRef { + public GetConnection get_conn; + public void * user_data; + } + + [CCode (cname = "ngtcp2_crypto_get_conn", has_target = false)] + public delegate unowned Connection GetConnection (ConnectionRef conn_ref); + + public ClientInitial client_initial_cb; + public RecvCryptoData recv_crypto_data_cb; + public Encrypt encrypt_cb; + public Decrypt decrypt_cb; + public HpMask hp_mask_cb; + public RecvRetry recv_retry_cb; + public UpdateKey update_key_cb; + public DeleteCryptoAeadCtx delete_crypto_aead_ctx_cb; + public DeleteCryptoCipherCtx delete_crypto_cipher_ctx_cb; + public GetPathChallengeData get_path_challenge_data_cb; + public VersionNegotiation version_negotiation_cb; +} diff --git a/vapi/lwip.vapi b/vapi/lwip.vapi new file mode 100644 index 000000000..22006a2fa --- /dev/null +++ b/vapi/lwip.vapi @@ -0,0 +1,195 @@ +[CCode (gir_namespace = "LWIP", gir_version = "1.0")] +namespace LWIP { + [CCode (cheader_filename = "lwip/tcpip.h", lower_case_cprefix = "tcpip_")] + namespace Runtime { + public void init (InitDoneFunc init_done); + + [CCode (cname = "tcpip_callback")] + public ErrorCode schedule (WorkFunc work); + + [CCode (cname = "tcpip_init_done_fn")] + public delegate void InitDoneFunc (); + + [CCode (cname = "tcpip_callback_fn")] + public delegate void WorkFunc (); + } + + [CCode (cheader_filename = "lwip/netif.h", cname = "struct netif", cprefix = "netif_")] + public struct NetworkInterface { + public static void add_noaddr (ref NetworkInterface netif, void * state, NetworkInterfaceInitFunc init, + NetworkInterfaceInputFunc input = NetworkInterface.default_input_handler); + + public void remove (); + + public void set_up (); + public void set_down (); + + public void ip6_addr_set (int8 addr_idx, IP6Address address); + public ErrorCode add_ip6_address (IP6Address address, int8 * chosen_index = null); + public void ip6_addr_set_state (int8 addr_index, IP6AddressState state); + + [CCode (cname = "netif_input")] + public static ErrorCode default_input_handler (PacketBuffer pbuf, NetworkInterface netif); + + public NetworkInterfaceInputFunc input; + public NetworkInterfaceOutputIP6Func output_ip6; + + public void * state; + + public uint16 mtu; + } + + [CCode (cname = "netif_init_fn", has_target = false)] + public delegate ErrorCode NetworkInterfaceInitFunc (NetworkInterface netif); + + [CCode (cname = "netif_input_fn", has_target = false)] + public delegate ErrorCode NetworkInterfaceInputFunc (PacketBuffer pbuf, NetworkInterface netif); + + [CCode (cname = "netif_output_ip6_fn", has_target = false)] + public delegate ErrorCode NetworkInterfaceOutputIP6Func (NetworkInterface netif, PacketBuffer pbuf, IP6Address address); + + [CCode (cheader_filename = "lwip/ip6_addr.h", cname = "ip6_addr_t", cprefix = "ip6_addr_")] + public struct IP6Address { + [CCode (cname = "ip6addr_aton")] + public static IP6Address parse (string str); + } + + [Flags] + [CCode (cheader_filename = "lwip/ip6_addr.h", cname = "u8_t", cprefix = "IP6_ADDR_", has_type_id = false)] + public enum IP6AddressState { + INVALID, + TENTATIVE, + TENTATIVE_1, + TENTATIVE_2, + TENTATIVE_3, + TENTATIVE_4, + TENTATIVE_5, + TENTATIVE_6, + TENTATIVE_7, + VALID, + PREFERRED, + DEPRECATED, + DUPLICATED, + } + + [CCode (cheader_filename = "lwip/ip_addr.h", cname = "u8_t", cprefix = "IPADDR_TYPE_", has_type_id = false)] + public enum IPAddressType { + V4, + V6, + ANY, + } + + [Compact] + [CCode (cheader_filename = "lwip/pbuf.h", cname = "struct pbuf", cprefix = "pbuf_", free_function = "")] + public class PacketBuffer { + public static PacketBuffer alloc (Layer layer, uint16 length, Type type); + + public uint8 free (); + + public PacketBuffer? next; + [CCode (array_length_cname = "len")] + public uint8[] payload; + public uint16 tot_len; + + [CCode (array_length = false)] + public unowned uint8[] get_contiguous (uint8[] buffer, uint16 len, uint16 offset = 0); + + public ErrorCode take (uint8[] data); + + [CCode (cname = "pbuf_layer", cprefix = "PBUF_", has_type_id = false)] + public enum Layer { + TRANSPORT, + IP, + LINK, + RAW_TX, + RAW, + } + + [CCode (cname = "pbuf_type", cprefix = "PBUF_", has_type_id = false)] + public enum Type { + RAM, + ROM, + REF, + POOL, + } + } + + [Compact] + [CCode (cheader_filename = "lwip/tcp.h", cname = "struct tcp_pcb", cprefix = "tcp_", free_function = "")] + public class TcpPcb { + [CCode (cname = "tcp_new_ip_type")] + public TcpPcb (IPAddressType type); + + [CCode (cname = "tcp_arg")] + public void set_user_data (void * user_data); + + [CCode (cname = "tcp_recv")] + public void set_recv_callback (RecvFunc f); + [CCode (cname = "tcp_sent")] + public void set_sent_callback (SentFunc f); + [CCode (cname = "tcp_err")] + public void set_error_callback (ErrorFunc f); + + public void nagle_disable (); + public void nagle_enable (); + + public void abort (); + public ErrorCode close (); + public ErrorCode shutdown (bool shut_rx, bool shut_tx); + + public void bind_netif (NetworkInterface? netif); + + public ErrorCode connect (IP6Address address, uint16 port, ConnectedFunc connected); + + [CCode (cname = "tcp_recved")] + public void notify_received (uint16 len); + + [CCode (cname = "tcp_sndbuf")] + public uint16 query_available_send_buffer_space (); + + public ErrorCode write (uint8[] data, WriteFlags flags = 0); + public ErrorCode output (); + + [CCode (cname = "tcp_recv_fn", has_target = false)] + public delegate ErrorCode RecvFunc (void * user_data, TcpPcb pcb, PacketBuffer? pbuf, ErrorCode err); + + [CCode (cname = "tcp_sent_fn", has_target = false)] + public delegate ErrorCode SentFunc (void * user_data, TcpPcb pcb, uint16 len); + + [CCode (cname = "tcp_err_fn", has_target = false)] + public delegate void ErrorFunc (void * user_data, ErrorCode err); + + [CCode (cname = "tcp_connected_fn", has_target = false)] + public delegate ErrorCode ConnectedFunc (void * user_data, TcpPcb pcb, ErrorCode err); + + [Flags] + [CCode (cname = "u8_t", cprefix = "TCP_WRITE_FLAG_", has_type_id = false)] + public enum WriteFlags { + COPY, + MORE, + } + } + + [CCode (cheader_filename = "lwip/err.h", cname = "err_t", cprefix = "ERR_", lower_case_cprefix = "err_", has_type_id = false)] + public enum ErrorCode { + OK, + MEM, + BUF, + TIMEOUT, + RTE, + INPROGRESS, + VAL, + WOULDBLOCK, + USE, + ALREADY, + ISCONN, + CONN, + IF, + ABRT, + RST, + CLSD, + ARG; + + public int to_errno (); + } +} diff --git a/vapi/openssl.vapi b/vapi/openssl.vapi index a424a00bd..4b0d12519 100644 --- a/vapi/openssl.vapi +++ b/vapi/openssl.vapi @@ -1 +1,500 @@ -// Placeholder to satisfy Meson assumptions. +[CCode (lower_case_cprefix = "OPENSSL_", gir_namespace = "OpenSSL", gir_version = "1.0")] +namespace OpenSSL { + [Compact] + [CCode (cname = "SSL_CTX", cprefix = "SSL_CTX_")] + public class SSLContext { + public SSLContext (SSLMethod meth); + + [CCode (cname = "SSL_CTX_use_certificate")] + public int use_certificate (X509 cert); + [CCode (cname = "SSL_CTX_use_PrivateKey")] + public int use_private_key (Envelope.Key key); + } + + [Compact] + [CCode (cname = "SSL", cprefix = "SSL_")] + public class SSL { + public SSL (SSLContext ctx); + + public void set_app_data (void * data); + + public void set_connect_state (); + public void set_accept_state (); + + public int set_alpn_protos (uint8[] protos); + + public void set_quic_transport_version (int version); + } + + [Compact] + [CCode (cname = "SSL_METHOD", cprefix = "SSL_METHOD_", free_function = "")] + public class SSLMethod { + [CCode (cname = "TLS_method")] + public static unowned SSLMethod tls (); + [CCode (cname = "TLS_server_method")] + public static unowned SSLMethod tls_server (); + [CCode (cname = "TLS_client_method")] + public static unowned SSLMethod tls_client (); + + [CCode (cname = "DTLS_method")] + public static unowned SSLMethod dtls (); + [CCode (cname = "DTLS_server_method")] + public static unowned SSLMethod dtls_server (); + [CCode (cname = "DTLS_client_method")] + public static unowned SSLMethod dtls_client (); + } + + [CCode (lower_case_cprefix = "TLSEXT_TYPE_")] + namespace TLSExtensionType { + public const int quic_transport_parameters; + } + + [Compact] + [CCode (cname = "X509", cprefix = "X509_")] + public class X509 { + public X509 (); + + [CCode (cname = "X509_get_serialNumber")] + public unowned ASN1.Integer get_serial_number (); + + [CCode (cname = "X509_getm_notBefore")] + public unowned ASN1.Time get_not_before (); + [CCode (cname = "X509_getm_notAfter")] + public unowned ASN1.Time get_not_after (); + + public unowned Name get_subject_name (); + public int set_issuer_name (Name name); + + public int set_pubkey (Envelope.Key key); + + public int sign_ctx (Envelope.MessageDigestContext ctx); + + [CCode (cname = "X509_get_X509_PUBKEY")] + public unowned X509PublicKey get_public_key (); + + [CCode (cname = "i2d_X509_bio", instance_pos = 2)] + public int to_der (BasicIO sink); + + [CCode (cname = "PEM_write_bio_X509", instance_pos = 2)] + public int to_pem (BasicIO sink); + + [Compact] + [CCode (cname = "X509_NAME", cprefix = "X509_NAME_")] + public class Name { + public int add_entry_by_txt (string field, ASN1.MultiByteStringType type, uint8[] bytes, int loc = -1, int set = 0); + } + } + + [Compact] + [CCode (cname = "X509_PUBKEY", cprefix = "X509_PUBKEY_", free_function = "")] + public class X509PublicKey { + [CCode (cname = "i2d_X509_PUBKEY_bio", instance_pos = 2)] + public int to_der (BasicIO sink); + + [CCode (cname = "PEM_write_bio_X509_PUBKEY", instance_pos = 2)] + public int to_pem (BasicIO sink); + } + + [Compact] + [CCode (cname = "BIO", cprefix = "BIO_")] + public class BasicIO { + public BasicIO (BasicIOMethod method); + [CCode (cname = "BIO_new_mem_buf")] + public BasicIO.from_static_memory_buffer (uint8[] buf); + + public long get_mem_data ([CCode (array_length = false)] out unowned uint8[] data); + } + + [Compact] + [CCode (cname = "BIO_METHOD", cprefix = "BIO_METHOD_", free_function = "")] + public class BasicIOMethod { + [CCode (cname = "BIO_s_mem")] + public static unowned BasicIOMethod memory (); + } + + [CCode (cheader_filename = "openssl/asn1.h")] + namespace ASN1 { + [Compact] + [CCode (cname = "ASN1_INTEGER", cprefix = "ASN1_INTEGER_")] + public class Integer { + public int set_int64 (int64 v); + public int set_uint64 (uint64 v); + } + + [Compact] + [CCode (cname = "ASN1_TIME", cprefix = "ASN1_TIME_")] + public class Time { + [CCode (cname = "X509_gmtime_adj")] + public unowned Time adjust (long delta); + } + + [CCode (cname = "int", cprefix = "MBSTRING_", has_type_id = false)] + public enum MultiByteStringType { + UTF8, + [CCode (cname = "MBSTRING_ASC")] + ASCII, + } + } + + [CCode (cheader_filename = "openssl/evp.h")] + namespace Envelope { + [Compact] + [CCode (cname = "EVP_PKEY_CTX", cprefix = "EVP_PKEY_", free_function = "EVP_PKEY_CTX_free")] + public class KeyContext { + [CCode (cname = "EVP_PKEY_CTX_new")] + public KeyContext.for_key (Key key, Engine? engine = null); + [CCode (cname = "EVP_PKEY_CTX_new_id")] + public KeyContext.for_key_type (KeyType type, Engine? engine = null); + + public int keygen_init (); + public int keygen (ref Key pkey); + + public int derive_init (); + public int derive_set_peer (Key peer); + public int derive ([CCode (array_length = false)] uint8[]? key, ref size_t keylen); + } + + [Compact] + [CCode (cname = "EVP_PKEY", cprefix = "EVP_PKEY_", copy_function = "EVP_PKEY_dup")] + public class Key { + [CCode (cname = "d2i_PUBKEY_bio")] + public Key.from_der (BasicIO source, Key ** a = null); + [CCode (cname = "EVP_PKEY_new_raw_public_key")] + public Key.from_raw_public_key (KeyType type, Engine? engine, uint8[] pub); + [CCode (cname = "EVP_PKEY_new_raw_private_key")] + public Key.from_raw_private_key (KeyType type, Engine? engine, uint8[] priv); + + public int get_raw_public_key ([CCode (array_length = false)] uint8[]? pub, ref size_t len); + public int get_raw_private_key ([CCode (array_length = false)] uint8[]? priv, ref size_t len); + + [CCode (cname = "i2d_PUBKEY_bio", instance_pos = 2)] + public int to_der (BasicIO sink); + + [CCode (cname = "PEM_write_bio_PUBKEY", instance_pos = 2)] + public int to_pem (BasicIO sink); + } + + [Compact] + [CCode (cname = "EVP_PKEY_CTX", cprefix = "EVP_PKEY_CTX_")] + public class PublicKeyContext { + } + + [CCode (cname = "int", cprefix = "EVP_PKEY_", has_type_id = false)] + public enum KeyType { + NONE, + RSA, + RSA2, + RSA_PSS, + DSA, + DSA1, + DSA2, + DSA3, + DSA4, + DH, + DHX, + EC, + SM2, + HMAC, + CMAC, + SCRYPT, + TLS1_PRF, + HKDF, + POLY1305, + SIPHASH, + X25519, + ED25519, + X448, + ED448, + KEYMGMT, + } + + [Compact] + [CCode (cheader_filename = "openssl/kdf.h", cname = "EVP_KDF", cprefix = "EVP_KDF_")] + public class KeyDerivationFunction { + public static KeyDerivationFunction? fetch (LibraryContext? ctx, string algorithm, string? properties = null); + } + + [Compact] + [CCode (cheader_filename = "openssl/kdf.h", cname = "EVP_KDF_CTX", cprefix = "EVP_KDF_", + free_function = "EVP_KDF_CTX_free")] + public class KeyDerivationContext { + [CCode (cname = "EVP_KDF_CTX_new")] + public KeyDerivationContext (KeyDerivationFunction kdf); + + public int derive (uint8[] key, [CCode (array_length = false)] Param[] params); + } + + [CCode (cheader_filename = "openssl/core_names.h", lower_case_cprefix = "OSSL_KDF_NAME_")] + namespace KeyDerivationAlgorithm { + public const string HKDF; + public const string TLS1_3_KDF; + public const string PBKDF1; + public const string PBKDF2; + public const string SCRYPT; + public const string SSHKDF; + public const string SSKDF; + public const string TLS1_PRF; + public const string X942KDF_ASN1; + public const string X942KDF_CONCAT; + public const string X963KDF; + public const string KBKDF; + public const string KRB5KDF; + } + + [CCode (cheader_filename = "openssl/core_names.h", lower_case_cprefix = "OSSL_KDF_PARAM_")] + namespace KeyDerivationParameter { + public const string SECRET; + public const string KEY; + public const string SALT; + public const string PASSWORD; + public const string PREFIX; + public const string LABEL; + public const string DATA; + public const string DIGEST; + public const string CIPHER; + public const string MAC; + public const string MAC_SIZE; + public const string PROPERTIES; + public const string ITER; + public const string MODE; + public const string PKCS5; + public const string UKM; + public const string CEK_ALG; + public const string SCRYPT_N; + public const string SCRYPT_R; + public const string SCRYPT_P; + public const string SCRYPT_MAXMEM; + public const string INFO; + public const string SEED; + public const string SSHKDF_XCGHASH; + public const string SSHKDF_SESSION_ID; + public const string SSHKDF_TYPE; + public const string SIZE; + public const string CONSTANT; + public const string PKCS12_ID; + public const string KBKDF_USE_L; + public const string KBKDF_USE_SEPARATOR; + public const string X942_ACVPINFO; + public const string X942_PARTYUINFO; + public const string X942_PARTYVINFO; + public const string X942_SUPP_PUBINFO; + public const string X942_SUPP_PRIVINFO; + public const string X942_USE_KEYBITS; + } + + [Compact] + [CCode (cname = "EVP_CIPHER_CTX", cprefix = "EVP_CIPHER_CTX_")] + public class CipherContext { + public CipherContext (); + + public int reset (); + + public int ctrl (CipherCtrlType type, int arg, void * ptr); + + [CCode (cname = "EVP_EncryptInit")] + public int encrypt_init (Cipher cipher, + [CCode (array_length = false)] uint8[] key, + [CCode (array_length = false)] uint8[] iv); + [CCode (cname = "EVP_EncryptUpdate")] + public int encrypt_update ([CCode (array_length = false)] uint8[] output, ref int outlen, uint8[] input); + [CCode (cname = "EVP_EncryptFinal")] + public int encrypt_final ([CCode (array_length = false)] uint8[] output, ref int outlen); + + [CCode (cname = "EVP_DecryptInit")] + public int decrypt_init (Cipher cipher, + [CCode (array_length = false)] uint8[] key, + [CCode (array_length = false)] uint8[] iv); + [CCode (cname = "EVP_DecryptUpdate")] + public int decrypt_update ([CCode (array_length = false)] uint8[] output, ref int outlen, uint8[] input); + [CCode (cname = "EVP_DecryptFinal")] + public int decrypt_final ([CCode (array_length = false)] uint8[] output, ref int outlen); + } + + [Compact] + [CCode (cname = "EVP_CIPHER", cprefix = "EVP_CIPHER_")] + public class Cipher { + public static Cipher? fetch (LibraryContext? ctx, string algorithm, string? properties = null); + } + + [CCode (cheader_filename = "openssl/evp.h", cname = "int", cprefix = "EVP_CTRL_", has_type_id = false)] + public enum CipherCtrlType { + INIT, + SET_KEY_LENGTH, + GET_RC2_KEY_BITS, + SET_RC2_KEY_BITS, + GET_RC5_ROUNDS, + SET_RC5_ROUNDS, + RAND_KEY, + PBE_PRF_NID, + COPY, + AEAD_SET_IVLEN, + AEAD_GET_TAG, + AEAD_SET_TAG, + AEAD_SET_IV_FIXED, + GCM_SET_IVLEN, + GCM_GET_TAG, + GCM_SET_TAG, + GCM_SET_IV_FIXED, + GCM_IV_GEN, + CCM_SET_IVLEN, + CCM_GET_TAG, + CCM_SET_TAG, + CCM_SET_IV_FIXED, + CCM_SET_L, + CCM_SET_MSGLEN, + AEAD_TLS1_AAD, + AEAD_SET_MAC_KEY, + GCM_SET_IV_INV, + TLS1_1_MULTIBLOCK_AAD, + TLS1_1_MULTIBLOCK_ENCRYPT, + TLS1_1_MULTIBLOCK_DECRYPT, + TLS1_1_MULTIBLOCK_MAX_BUFSIZE, + SSL3_MASTER_SECRET, + SET_SBOX, + SBOX_USED, + KEY_MESH, + BLOCK_PADDING_MODE, + SET_PIPELINE_OUTPUT_BUFS, + SET_PIPELINE_INPUT_BUFS, + SET_PIPELINE_INPUT_LENS, + GET_IVLEN, + SET_SPEED, + PROCESS_UNPROTECTED, + GET_WRAP_CIPHER, + TLSTREE, + } + + [Compact] + [CCode (cname = "EVP_MD_CTX", cprefix = "EVP_MD_CTX_")] + public class MessageDigestContext { + public MessageDigestContext (); + + [CCode (cname = "EVP_DigestSignInit")] + public int digest_sign_init (PublicKeyContext ** pctx, MessageDigest? type, Engine? engine = null, Key? key = null); + [CCode (cname = "EVP_DigestSignUpdate")] + public int digest_sign_update (uint8[] data); + [CCode (cname = "EVP_DigestSignFinal")] + public int digest_sign_final ([CCode (array_length = false)] uint8[]? sigret, ref size_t siglen); + [CCode (cname = "EVP_DigestSign")] + public int digest_sign ([CCode (array_length = false)] uint8[]? sigret, ref size_t siglen, uint8[] tbs); + } + + [Compact] + [CCode (cname = "EVP_MD", cprefix = "EVP_MD_")] + public class MessageDigest { + public static MessageDigest fetch (LibraryContext? ctx, string algorithm, string? properties = null); + } + } + + [Compact] + [CCode (cheader_filename = "openssl/engine.h", cname = "ENGINE")] + public class Engine { + } + + [Compact] + [CCode (cheader_filename = "openssl/types.h", cname = "OSSL_LIB_CTX")] + public class LibraryContext { + } + + [CCode (cheader_filename = "openssl/core.h", cname = "OSSL_PARAM", copy_function = "", destroy_function = "")] + public struct Param { + public unowned string? key; + public ParamDataType data_type; + [CCode (array_length_cname = "data_size")] + public unowned uint8[]? data; + public size_t return_size; + } + + [CCode (cheader_filename = "openssl/core.h", cname = "guint", cprefix = "OSSL_PARAM_", has_type_id = false)] + public enum ParamDataType { + INTEGER, + UNSIGNED_INTEGER, + REAL, + UTF8_STRING, + OCTET_STRING, + UTF8_PTR, + OCTET_PTR, + } + + [CCode (cheader_filename = "openssl/core.h", cname = "size_t", cprefix = "OSSL_PARAM_", has_type_id = false)] + public enum ParamReturnSize { + UNMODIFIED, + } + + [CCode (cheader_filename = "openssl/objects.h", lower_case_cprefix = "SN_")] + namespace ShortName { + public const string sha256; + public const string sha384; + public const string sha512; + public const string chacha20_poly1305; + } + + [CCode (cheader_filename = "openssl/rand.h")] + namespace Rng { + [CCode (cname = "RAND_bytes")] + public int generate (uint8[] buf); + } + + [Compact] + [CCode (cname = "BN_CTX", cprefix = "BN_CTX_")] + public class BigNumberContext { + [CCode (cname = "BN_CTX_secure_new")] + public BigNumberContext.secure (); + } + + [Compact] + [CCode (cname = "BIGNUM", cprefix = "BN_")] + public class BigNumber { + public BigNumber (); + [CCode (cname = "BN_native2bn")] + public BigNumber.from_native (uint8[] data, BigNumber * ret = null); + [CCode (cname = "BN_bin2bn")] + public BigNumber.from_big_endian (uint8[] data, BigNumber * ret = null); + + public static BigNumber get_rfc2409_prime_768 (BigNumber * ret = null); + public static BigNumber get_rfc2409_prime_1024 (BigNumber * ret = null); + + public static BigNumber get_rfc3526_prime_1536 (BigNumber * ret = null); + public static BigNumber get_rfc3526_prime_2048 (BigNumber * ret = null); + public static BigNumber get_rfc3526_prime_3072 (BigNumber * ret = null); + public static BigNumber get_rfc3526_prime_4096 (BigNumber * ret = null); + public static BigNumber get_rfc3526_prime_6144 (BigNumber * ret = null); + public static BigNumber get_rfc3526_prime_8192 (BigNumber * ret = null); + + public bool is_zero (); + + public static int add (BigNumber r, BigNumber a, BigNumber b); + public static int sub (BigNumber r, BigNumber a, BigNumber b); + public static int mul (BigNumber r, BigNumber a, BigNumber b, BigNumberContext ctx); + public static int mod (BigNumber rem, BigNumber m, BigNumber d, BigNumberContext ctx); + public static int mod_exp (BigNumber r, BigNumber a, BigNumber p, BigNumber m, BigNumberContext ctx); + + public int num_bits (); + public int num_bytes (); + + [CCode (cname = "BN_bn2bin")] + public int to_big_endian ([CCode (array_length = false)] uint8[] to); + + [CCode (cname = "BN_bn2binpad")] + public int to_big_endian_padded (uint8[] to); + + [CCode (cname = "BN_bn2dec")] + public char * to_string (); + + [CCode (cname = "BN_bn2hex")] + public char * to_hex_string (); + } + + [CCode (cheader_filename = "openssl/crypto.h", lower_case_cprefix = "CRYPTO_")] + namespace Crypto { + public int memcmp (void * a, void * b, size_t len); + } + + [CCode (lower_case_cprefix = "ERR_")] + namespace Error { + public ulong get_error (); + public void error_string_n (ulong e, char[] buf); + } + + public void free (void * addr); +}