diff --git a/docs/docs.go b/docs/docs.go new file mode 100644 index 00000000..a56cee77 --- /dev/null +++ b/docs/docs.go @@ -0,0 +1,1048 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": {}, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/device/{udid}/accessibility/disable": { + "get": { + "description": "Turns off the accessibility session on the device", + "produces": [ + "application/json" + ], + "tags": [ + "accessibility" + ], + "summary": "Disable accessibility service", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/device/{udid}/accessibility/enable": { + "get": { + "description": "Starts an accessibility session on the device and enables selection mode", + "produces": [ + "application/json" + ], + "tags": [ + "accessibility" + ], + "summary": "Enable accessibility service", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + } + } + } + }, + "/device/{udid}/accessibility/next": { + "post": { + "description": "Moves the selection to the next element using accessibility service", + "produces": [ + "application/json" + ], + "tags": [ + "accessibility" + ], + "summary": "Navigate to next accessible element", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + } + } + } + }, + "/device/{udid}/accessibility/perform-action": { + "post": { + "description": "Performs the default action on the current focused element using DTX", + "produces": [ + "application/json" + ], + "tags": [ + "accessibility" + ], + "summary": "Perform accessibility action via DTX", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + } + } + } + }, + "/device/{udid}/accessibility/previous": { + "post": { + "description": "Moves the selection to the previous element using accessibility service", + "produces": [ + "application/json" + ], + "tags": [ + "accessibility" + ], + "summary": "Navigate to previous accessible element", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + } + } + } + }, + "/device/{udid}/accessibility/wda/perform-action": { + "post": { + "description": "Performs an action using WebDriverAgent and returns action UUID", + "produces": [ + "application/json" + ], + "tags": [ + "accessibility" + ], + "summary": "Perform accessibility action via WDA", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + } + } + } + }, + "/device/{udid}/activate": { + "post": { + "description": "Returns and error if activation fails. Otherwise {\"message\":\"Activation successful\"}", + "produces": [ + "application/json" + ], + "tags": [ + "general_device_specific", + "activation" + ], + "summary": "Activate the device by udid", + "parameters": [ + { + "type": "string", + "description": "Device UDID", + "name": "udid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/device/{udid}/apps": { + "post": { + "description": "List the installed apps on a device", + "produces": [ + "application/json" + ], + "tags": [ + "apps" + ], + "summary": "List apps on a device", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/installationproxy.AppInfo" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + } + } + } + }, + "/device/{udid}/apps/install": { + "post": { + "description": "Install app on a device by uploading an ipa file", + "produces": [ + "application/json" + ], + "tags": [ + "apps" + ], + "summary": "Install app on a device", + "parameters": [ + { + "type": "file", + "description": "ipa file to install", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + } + } + } + }, + "/device/{udid}/apps/kill": { + "post": { + "description": "Kill running app on a device by provided bundleID", + "produces": [ + "application/json" + ], + "tags": [ + "apps" + ], + "summary": "Kill running app on a device", + "parameters": [ + { + "type": "string", + "description": "bundle identifier of the targeted app", + "name": "bundleID", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + } + } + } + }, + "/device/{udid}/apps/launch": { + "post": { + "description": "Launch app on a device by provided bundleID", + "produces": [ + "application/json" + ], + "tags": [ + "apps" + ], + "summary": "Launch app on a device", + "parameters": [ + { + "type": "string", + "description": "bundle identifier of the targeted app", + "name": "bundleID", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + } + } + } + }, + "/device/{udid}/conditions": { + "get": { + "description": "Get a list of the available conditions that can be applied on the device", + "produces": [ + "application/json" + ], + "tags": [ + "general_device_specific" + ], + "summary": "Get a list of available device conditions", + "parameters": [ + { + "type": "string", + "description": "Device UDID", + "name": "udid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/instruments.ProfileType" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + } + } + } + }, + "/device/{udid}/disable-condition": { + "post": { + "description": "Disable the currently active condition on a device", + "produces": [ + "application/json" + ], + "tags": [ + "general_device_specific" + ], + "summary": "Disable the currently active condition on a device", + "parameters": [ + { + "type": "string", + "description": "Device UDID", + "name": "udid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + } + } + } + }, + "/device/{udid}/enable-condition": { + "put": { + "description": "Enable condition on a device by provided profileTypeID and profileID", + "produces": [ + "application/json" + ], + "tags": [ + "general_device_specific" + ], + "summary": "Enable condition on a device", + "parameters": [ + { + "type": "string", + "description": "Device UDID", + "name": "udid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Identifier of the profile type, eg. SlowNetworkCondition", + "name": "profileTypeID", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Identifier of the sub-profile, eg. SlowNetwork100PctLoss", + "name": "profileID", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + } + } + } + }, + "/device/{udid}/info": { + "get": { + "description": "Returns all lockdown values and additional instruments properties for development enabled devices.", + "produces": [ + "application/json" + ], + "tags": [ + "general_device_specific" + ], + "summary": "Get lockdown info for a device by udid", + "parameters": [ + { + "type": "string", + "description": "device udid", + "name": "udid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Device UDID", + "name": "udid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/device/{udid}/pair": { + "post": { + "description": "Pair a device with/without supervision", + "produces": [ + "application/json" + ], + "tags": [ + "general_device_specific" + ], + "summary": "Pair a device with/without supervision", + "parameters": [ + { + "type": "string", + "description": "Device UDID", + "name": "udid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Set if device is supervised - true/false", + "name": "supervised", + "in": "query", + "required": true + }, + { + "type": "file", + "description": "Supervision *.p12 file", + "name": "p12file", + "in": "formData" + }, + { + "type": "string", + "description": "Supervision password", + "name": "supervision_password", + "in": "formData" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + } + } + } + }, + "/device/{udid}/profiles": { + "get": { + "description": "get the list of installed profiles from the ios device", + "produces": [ + "application/json" + ], + "tags": [ + "general_device_specific" + ], + "summary": "get the list of profiles", + "parameters": [ + { + "type": "string", + "description": "Device UDID", + "name": "udid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + } + } + } + }, + "/device/{udid}/resetlocation": { + "post": { + "description": "Reset the changed device location to the actual one", + "produces": [ + "application/json" + ], + "tags": [ + "general_device_specific" + ], + "summary": "Reset the changed device location", + "parameters": [ + { + "type": "string", + "description": "Device UDID", + "name": "udid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK" + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + } + } + } + }, + "/device/{udid}/screenshot": { + "get": { + "description": "Takes a png screenshot and returns it.", + "produces": [ + "image/png" + ], + "tags": [ + "general_device_specific" + ], + "summary": "Get screenshot for device", + "parameters": [ + { + "type": "string", + "description": "device udid", + "name": "udid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Device UDID", + "name": "udid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + } + } + } + } + }, + "/device/{udid}/setlocation": { + "post": { + "description": "Change the current device location to provided latitude and longtitude", + "produces": [ + "application/json" + ], + "tags": [ + "general_device_specific" + ], + "summary": "Change the current device location", + "parameters": [ + { + "type": "string", + "description": "Location latitude", + "name": "latitude", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Location longtitude", + "name": "longtitude", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Device UDID", + "name": "udid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + } + } + } + }, + "/list": { + "get": { + "description": "get device list of currently connected devices.", + "produces": [ + "application/json" + ], + "tags": [ + "general" + ], + "summary": "Get device list", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/listen": { + "get": { + "description": "Uses SSE to connect to the LISTEN command", + "produces": [ + "application/json" + ], + "tags": [ + "general" + ], + "summary": "Uses SSE to connect to the LISTEN command", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/notifications": { + "get": { + "description": "uses instruments to get application state change events", + "produces": [ + "application/json" + ], + "tags": [ + "general" + ], + "summary": "uses instruments to get application state change events", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/wda/session": { + "post": { + "description": "Create a new WebDriverAgent session for the specified device", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "WebDriverAgent" + ], + "summary": "Create a new WDA session", + "parameters": [ + { + "description": "WebDriverAgent Configuration", + "name": "config", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api.WdaConfig" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.WdaSession" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + } + } + } + }, + "/wda/session/{sessionId}": { + "get": { + "description": "Get a WebDriverAgent session by sessionId", + "produces": [ + "application/json" + ], + "tags": [ + "WebDriverAgent" + ], + "summary": "Get a WebDriverAgent session", + "parameters": [ + { + "type": "string", + "description": "Session ID", + "name": "sessionId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.WdaSession" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + } + } + }, + "delete": { + "description": "Delete a WebDriverAgent session by sessionId", + "produces": [ + "application/json" + ], + "tags": [ + "WebDriverAgent" + ], + "summary": "Delete a WebDriverAgent session", + "parameters": [ + { + "type": "string", + "description": "Session ID", + "name": "sessionId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.WdaSession" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + } + } + } + } + }, + "definitions": { + "api.GenericResponse": { + "type": "object", + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "api.WdaConfig": { + "type": "object", + "required": [ + "bundleId", + "testBundleId", + "xcTestConfig" + ], + "properties": { + "args": { + "type": "array", + "items": { + "type": "string" + } + }, + "bundleId": { + "type": "string" + }, + "env": { + "type": "object", + "additionalProperties": true + }, + "testBundleId": { + "type": "string" + }, + "xcTestConfig": { + "type": "string" + } + } + }, + "api.WdaSession": { + "type": "object", + "required": [ + "config", + "sessionId", + "udid" + ], + "properties": { + "config": { + "$ref": "#/definitions/api.WdaConfig" + }, + "sessionId": { + "type": "string" + }, + "udid": { + "type": "string" + } + } + }, + "installationproxy.AppInfo": { + "type": "object", + "additionalProperties": {} + }, + "instruments.Profile": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "identifier": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "instruments.ProfileType": { + "type": "object", + "properties": { + "activeProfile": { + "type": "string" + }, + "identifier": { + "type": "string" + }, + "isActive": { + "type": "boolean" + }, + "isDestructive": { + "type": "boolean" + }, + "isInternal": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "profiles": { + "type": "array", + "items": { + "$ref": "#/definitions/instruments.Profile" + } + }, + "profilesSorted": { + "type": "boolean" + } + } + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "", + Host: "", + BasePath: "", + Schemes: []string{}, + Title: "", + Description: "", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/docs/swagger.json b/docs/swagger.json new file mode 100644 index 00000000..5e7142ea --- /dev/null +++ b/docs/swagger.json @@ -0,0 +1,1019 @@ +{ + "swagger": "2.0", + "info": { + "contact": {} + }, + "paths": { + "/device/{udid}/accessibility/disable": { + "get": { + "description": "Turns off the accessibility session on the device", + "produces": [ + "application/json" + ], + "tags": [ + "accessibility" + ], + "summary": "Disable accessibility service", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/device/{udid}/accessibility/enable": { + "get": { + "description": "Starts an accessibility session on the device and enables selection mode", + "produces": [ + "application/json" + ], + "tags": [ + "accessibility" + ], + "summary": "Enable accessibility service", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + } + } + } + }, + "/device/{udid}/accessibility/next": { + "post": { + "description": "Moves the selection to the next element using accessibility service", + "produces": [ + "application/json" + ], + "tags": [ + "accessibility" + ], + "summary": "Navigate to next accessible element", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + } + } + } + }, + "/device/{udid}/accessibility/perform-action": { + "post": { + "description": "Performs the default action on the current focused element using DTX", + "produces": [ + "application/json" + ], + "tags": [ + "accessibility" + ], + "summary": "Perform accessibility action via DTX", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + } + } + } + }, + "/device/{udid}/accessibility/previous": { + "post": { + "description": "Moves the selection to the previous element using accessibility service", + "produces": [ + "application/json" + ], + "tags": [ + "accessibility" + ], + "summary": "Navigate to previous accessible element", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + } + } + } + }, + "/device/{udid}/accessibility/wda/perform-action": { + "post": { + "description": "Performs an action using WebDriverAgent and returns action UUID", + "produces": [ + "application/json" + ], + "tags": [ + "accessibility" + ], + "summary": "Perform accessibility action via WDA", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + } + } + } + }, + "/device/{udid}/activate": { + "post": { + "description": "Returns and error if activation fails. Otherwise {\"message\":\"Activation successful\"}", + "produces": [ + "application/json" + ], + "tags": [ + "general_device_specific", + "activation" + ], + "summary": "Activate the device by udid", + "parameters": [ + { + "type": "string", + "description": "Device UDID", + "name": "udid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/device/{udid}/apps": { + "post": { + "description": "List the installed apps on a device", + "produces": [ + "application/json" + ], + "tags": [ + "apps" + ], + "summary": "List apps on a device", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/installationproxy.AppInfo" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + } + } + } + }, + "/device/{udid}/apps/install": { + "post": { + "description": "Install app on a device by uploading an ipa file", + "produces": [ + "application/json" + ], + "tags": [ + "apps" + ], + "summary": "Install app on a device", + "parameters": [ + { + "type": "file", + "description": "ipa file to install", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + } + } + } + }, + "/device/{udid}/apps/kill": { + "post": { + "description": "Kill running app on a device by provided bundleID", + "produces": [ + "application/json" + ], + "tags": [ + "apps" + ], + "summary": "Kill running app on a device", + "parameters": [ + { + "type": "string", + "description": "bundle identifier of the targeted app", + "name": "bundleID", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + } + } + } + }, + "/device/{udid}/apps/launch": { + "post": { + "description": "Launch app on a device by provided bundleID", + "produces": [ + "application/json" + ], + "tags": [ + "apps" + ], + "summary": "Launch app on a device", + "parameters": [ + { + "type": "string", + "description": "bundle identifier of the targeted app", + "name": "bundleID", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + } + } + } + }, + "/device/{udid}/conditions": { + "get": { + "description": "Get a list of the available conditions that can be applied on the device", + "produces": [ + "application/json" + ], + "tags": [ + "general_device_specific" + ], + "summary": "Get a list of available device conditions", + "parameters": [ + { + "type": "string", + "description": "Device UDID", + "name": "udid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/instruments.ProfileType" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + } + } + } + }, + "/device/{udid}/disable-condition": { + "post": { + "description": "Disable the currently active condition on a device", + "produces": [ + "application/json" + ], + "tags": [ + "general_device_specific" + ], + "summary": "Disable the currently active condition on a device", + "parameters": [ + { + "type": "string", + "description": "Device UDID", + "name": "udid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + } + } + } + }, + "/device/{udid}/enable-condition": { + "put": { + "description": "Enable condition on a device by provided profileTypeID and profileID", + "produces": [ + "application/json" + ], + "tags": [ + "general_device_specific" + ], + "summary": "Enable condition on a device", + "parameters": [ + { + "type": "string", + "description": "Device UDID", + "name": "udid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Identifier of the profile type, eg. SlowNetworkCondition", + "name": "profileTypeID", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Identifier of the sub-profile, eg. SlowNetwork100PctLoss", + "name": "profileID", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + } + } + } + }, + "/device/{udid}/info": { + "get": { + "description": "Returns all lockdown values and additional instruments properties for development enabled devices.", + "produces": [ + "application/json" + ], + "tags": [ + "general_device_specific" + ], + "summary": "Get lockdown info for a device by udid", + "parameters": [ + { + "type": "string", + "description": "device udid", + "name": "udid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Device UDID", + "name": "udid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/device/{udid}/pair": { + "post": { + "description": "Pair a device with/without supervision", + "produces": [ + "application/json" + ], + "tags": [ + "general_device_specific" + ], + "summary": "Pair a device with/without supervision", + "parameters": [ + { + "type": "string", + "description": "Device UDID", + "name": "udid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Set if device is supervised - true/false", + "name": "supervised", + "in": "query", + "required": true + }, + { + "type": "file", + "description": "Supervision *.p12 file", + "name": "p12file", + "in": "formData" + }, + { + "type": "string", + "description": "Supervision password", + "name": "supervision_password", + "in": "formData" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + } + } + } + }, + "/device/{udid}/profiles": { + "get": { + "description": "get the list of installed profiles from the ios device", + "produces": [ + "application/json" + ], + "tags": [ + "general_device_specific" + ], + "summary": "get the list of profiles", + "parameters": [ + { + "type": "string", + "description": "Device UDID", + "name": "udid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + } + } + } + }, + "/device/{udid}/resetlocation": { + "post": { + "description": "Reset the changed device location to the actual one", + "produces": [ + "application/json" + ], + "tags": [ + "general_device_specific" + ], + "summary": "Reset the changed device location", + "parameters": [ + { + "type": "string", + "description": "Device UDID", + "name": "udid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK" + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + } + } + } + }, + "/device/{udid}/screenshot": { + "get": { + "description": "Takes a png screenshot and returns it.", + "produces": [ + "image/png" + ], + "tags": [ + "general_device_specific" + ], + "summary": "Get screenshot for device", + "parameters": [ + { + "type": "string", + "description": "device udid", + "name": "udid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Device UDID", + "name": "udid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + } + } + } + } + }, + "/device/{udid}/setlocation": { + "post": { + "description": "Change the current device location to provided latitude and longtitude", + "produces": [ + "application/json" + ], + "tags": [ + "general_device_specific" + ], + "summary": "Change the current device location", + "parameters": [ + { + "type": "string", + "description": "Location latitude", + "name": "latitude", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Location longtitude", + "name": "longtitude", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Device UDID", + "name": "udid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + } + } + } + }, + "/list": { + "get": { + "description": "get device list of currently connected devices.", + "produces": [ + "application/json" + ], + "tags": [ + "general" + ], + "summary": "Get device list", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/listen": { + "get": { + "description": "Uses SSE to connect to the LISTEN command", + "produces": [ + "application/json" + ], + "tags": [ + "general" + ], + "summary": "Uses SSE to connect to the LISTEN command", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/notifications": { + "get": { + "description": "uses instruments to get application state change events", + "produces": [ + "application/json" + ], + "tags": [ + "general" + ], + "summary": "uses instruments to get application state change events", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/wda/session": { + "post": { + "description": "Create a new WebDriverAgent session for the specified device", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "WebDriverAgent" + ], + "summary": "Create a new WDA session", + "parameters": [ + { + "description": "WebDriverAgent Configuration", + "name": "config", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api.WdaConfig" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.WdaSession" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + } + } + } + }, + "/wda/session/{sessionId}": { + "get": { + "description": "Get a WebDriverAgent session by sessionId", + "produces": [ + "application/json" + ], + "tags": [ + "WebDriverAgent" + ], + "summary": "Get a WebDriverAgent session", + "parameters": [ + { + "type": "string", + "description": "Session ID", + "name": "sessionId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.WdaSession" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + } + } + }, + "delete": { + "description": "Delete a WebDriverAgent session by sessionId", + "produces": [ + "application/json" + ], + "tags": [ + "WebDriverAgent" + ], + "summary": "Delete a WebDriverAgent session", + "parameters": [ + { + "type": "string", + "description": "Session ID", + "name": "sessionId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.WdaSession" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.GenericResponse" + } + } + } + } + } + }, + "definitions": { + "api.GenericResponse": { + "type": "object", + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "api.WdaConfig": { + "type": "object", + "required": [ + "bundleId", + "testBundleId", + "xcTestConfig" + ], + "properties": { + "args": { + "type": "array", + "items": { + "type": "string" + } + }, + "bundleId": { + "type": "string" + }, + "env": { + "type": "object", + "additionalProperties": true + }, + "testBundleId": { + "type": "string" + }, + "xcTestConfig": { + "type": "string" + } + } + }, + "api.WdaSession": { + "type": "object", + "required": [ + "config", + "sessionId", + "udid" + ], + "properties": { + "config": { + "$ref": "#/definitions/api.WdaConfig" + }, + "sessionId": { + "type": "string" + }, + "udid": { + "type": "string" + } + } + }, + "installationproxy.AppInfo": { + "type": "object", + "additionalProperties": {} + }, + "instruments.Profile": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "identifier": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "instruments.ProfileType": { + "type": "object", + "properties": { + "activeProfile": { + "type": "string" + }, + "identifier": { + "type": "string" + }, + "isActive": { + "type": "boolean" + }, + "isDestructive": { + "type": "boolean" + }, + "isInternal": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "profiles": { + "type": "array", + "items": { + "$ref": "#/definitions/instruments.Profile" + } + }, + "profilesSorted": { + "type": "boolean" + } + } + } + } +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml new file mode 100644 index 00000000..f004ffed --- /dev/null +++ b/docs/swagger.yaml @@ -0,0 +1,681 @@ +definitions: + api.GenericResponse: + properties: + error: + type: string + message: + type: string + type: object + api.WdaConfig: + properties: + args: + items: + type: string + type: array + bundleId: + type: string + env: + additionalProperties: true + type: object + testBundleId: + type: string + xcTestConfig: + type: string + required: + - bundleId + - testBundleId + - xcTestConfig + type: object + api.WdaSession: + properties: + config: + $ref: '#/definitions/api.WdaConfig' + sessionId: + type: string + udid: + type: string + required: + - config + - sessionId + - udid + type: object + installationproxy.AppInfo: + additionalProperties: {} + type: object + instruments.Profile: + properties: + description: + type: string + identifier: + type: string + name: + type: string + type: object + instruments.ProfileType: + properties: + activeProfile: + type: string + identifier: + type: string + isActive: + type: boolean + isDestructive: + type: boolean + isInternal: + type: boolean + name: + type: string + profiles: + items: + $ref: '#/definitions/instruments.Profile' + type: array + profilesSorted: + type: boolean + type: object +info: + contact: {} +paths: + /device/{udid}/accessibility/disable: + get: + description: Turns off the accessibility session on the device + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: + type: string + type: object + summary: Disable accessibility service + tags: + - accessibility + /device/{udid}/accessibility/enable: + get: + description: Starts an accessibility session on the device and enables selection + mode + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/api.GenericResponse' + summary: Enable accessibility service + tags: + - accessibility + /device/{udid}/accessibility/next: + post: + description: Moves the selection to the next element using accessibility service + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/api.GenericResponse' + summary: Navigate to next accessible element + tags: + - accessibility + /device/{udid}/accessibility/perform-action: + post: + description: Performs the default action on the current focused element using + DTX + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: + type: string + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/api.GenericResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/api.GenericResponse' + summary: Perform accessibility action via DTX + tags: + - accessibility + /device/{udid}/accessibility/previous: + post: + description: Moves the selection to the previous element using accessibility + service + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/api.GenericResponse' + summary: Navigate to previous accessible element + tags: + - accessibility + /device/{udid}/accessibility/wda/perform-action: + post: + description: Performs an action using WebDriverAgent and returns action UUID + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: + type: string + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/api.GenericResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/api.GenericResponse' + summary: Perform accessibility action via WDA + tags: + - accessibility + /device/{udid}/activate: + post: + description: Returns and error if activation fails. Otherwise {"message":"Activation + successful"} + parameters: + - description: Device UDID + in: path + name: udid + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + summary: Activate the device by udid + tags: + - general_device_specific + - activation + /device/{udid}/apps: + post: + description: List the installed apps on a device + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/installationproxy.AppInfo' + type: array + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/api.GenericResponse' + summary: List apps on a device + tags: + - apps + /device/{udid}/apps/install: + post: + description: Install app on a device by uploading an ipa file + parameters: + - description: ipa file to install + in: formData + name: file + required: true + type: file + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/api.GenericResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/api.GenericResponse' + summary: Install app on a device + tags: + - apps + /device/{udid}/apps/kill: + post: + description: Kill running app on a device by provided bundleID + parameters: + - description: bundle identifier of the targeted app + in: query + name: bundleID + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/api.GenericResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/api.GenericResponse' + summary: Kill running app on a device + tags: + - apps + /device/{udid}/apps/launch: + post: + description: Launch app on a device by provided bundleID + parameters: + - description: bundle identifier of the targeted app + in: query + name: bundleID + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/api.GenericResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/api.GenericResponse' + summary: Launch app on a device + tags: + - apps + /device/{udid}/conditions: + get: + description: Get a list of the available conditions that can be applied on the + device + parameters: + - description: Device UDID + in: path + name: udid + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/instruments.ProfileType' + type: array + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/api.GenericResponse' + summary: Get a list of available device conditions + tags: + - general_device_specific + /device/{udid}/disable-condition: + post: + description: Disable the currently active condition on a device + parameters: + - description: Device UDID + in: path + name: udid + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/api.GenericResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/api.GenericResponse' + summary: Disable the currently active condition on a device + tags: + - general_device_specific + /device/{udid}/enable-condition: + put: + description: Enable condition on a device by provided profileTypeID and profileID + parameters: + - description: Device UDID + in: path + name: udid + required: true + type: string + - description: Identifier of the profile type, eg. SlowNetworkCondition + in: query + name: profileTypeID + required: true + type: string + - description: Identifier of the sub-profile, eg. SlowNetwork100PctLoss + in: query + name: profileID + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/api.GenericResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/api.GenericResponse' + summary: Enable condition on a device + tags: + - general_device_specific + /device/{udid}/info: + get: + description: Returns all lockdown values and additional instruments properties + for development enabled devices. + parameters: + - description: device udid + in: path + name: udid + required: true + type: string + - description: Device UDID + in: path + name: udid + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + summary: Get lockdown info for a device by udid + tags: + - general_device_specific + /device/{udid}/pair: + post: + description: Pair a device with/without supervision + parameters: + - description: Device UDID + in: path + name: udid + required: true + type: string + - description: Set if device is supervised - true/false + in: query + name: supervised + required: true + type: string + - description: Supervision *.p12 file + in: formData + name: p12file + type: file + - description: Supervision password + in: formData + name: supervision_password + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/api.GenericResponse' + "422": + description: Unprocessable Entity + schema: + $ref: '#/definitions/api.GenericResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/api.GenericResponse' + summary: Pair a device with/without supervision + tags: + - general_device_specific + /device/{udid}/profiles: + get: + description: get the list of installed profiles from the ios device + parameters: + - description: Device UDID + in: path + name: udid + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "404": + description: Not Found + schema: + $ref: '#/definitions/api.GenericResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/api.GenericResponse' + summary: get the list of profiles + tags: + - general_device_specific + /device/{udid}/resetlocation: + post: + description: Reset the changed device location to the actual one + parameters: + - description: Device UDID + in: path + name: udid + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/api.GenericResponse' + summary: Reset the changed device location + tags: + - general_device_specific + /device/{udid}/screenshot: + get: + description: Takes a png screenshot and returns it. + parameters: + - description: device udid + in: path + name: udid + required: true + type: string + - description: Device UDID + in: path + name: udid + required: true + type: string + produces: + - image/png + responses: + "200": + description: OK + schema: + items: + format: int32 + type: integer + type: array + summary: Get screenshot for device + tags: + - general_device_specific + /device/{udid}/setlocation: + post: + description: Change the current device location to provided latitude and longtitude + parameters: + - description: Location latitude + in: query + name: latitude + required: true + type: string + - description: Location longtitude + in: query + name: longtitude + required: true + type: string + - description: Device UDID + in: path + name: udid + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/api.GenericResponse' + "422": + description: Unprocessable Entity + schema: + $ref: '#/definitions/api.GenericResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/api.GenericResponse' + summary: Change the current device location + tags: + - general_device_specific + /list: + get: + description: get device list of currently connected devices. + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + summary: Get device list + tags: + - general + /listen: + get: + description: Uses SSE to connect to the LISTEN command + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + summary: Uses SSE to connect to the LISTEN command + tags: + - general + /notifications: + get: + description: uses instruments to get application state change events + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + summary: uses instruments to get application state change events + tags: + - general + /wda/session: + post: + consumes: + - application/json + description: Create a new WebDriverAgent session for the specified device + parameters: + - description: WebDriverAgent Configuration + in: body + name: config + required: true + schema: + $ref: '#/definitions/api.WdaConfig' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/api.WdaSession' + "400": + description: Bad Request + schema: + $ref: '#/definitions/api.GenericResponse' + summary: Create a new WDA session + tags: + - WebDriverAgent + /wda/session/{sessionId}: + delete: + description: Delete a WebDriverAgent session by sessionId + parameters: + - description: Session ID + in: path + name: sessionId + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/api.WdaSession' + "400": + description: Bad Request + schema: + $ref: '#/definitions/api.GenericResponse' + summary: Delete a WebDriverAgent session + tags: + - WebDriverAgent + get: + description: Get a WebDriverAgent session by sessionId + parameters: + - description: Session ID + in: path + name: sessionId + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/api.WdaSession' + "400": + description: Bad Request + schema: + $ref: '#/definitions/api.GenericResponse' + summary: Get a WebDriverAgent session + tags: + - WebDriverAgent +swagger: "2.0" diff --git a/go.sum b/go.sum index 8e56603d..922c9020 100644 --- a/go.sum +++ b/go.sum @@ -13,6 +13,8 @@ github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/elazarl/goproxy v0.0.0-20240726154733-8b0c20506380 h1:1NyRx2f4W4WBRyg0Kys0ZbaNmDDzZ2R/C7DTi+bbsJ0= github.com/elazarl/goproxy v0.0.0-20240726154733-8b0c20506380/go.mod h1:thX175TtLTzLj3p7N/Q9IiKZ7NF+p72cvL91emV0hzo= +github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 h1:dWB6v3RcOy03t/bUadywsbyrQwCqZeNIEX6M1OtSZOM= +github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= diff --git a/ios/accessibility/accessibility.go b/ios/accessibility/accessibility.go index cb28f908..33e46f57 100644 --- a/ios/accessibility/accessibility.go +++ b/ios/accessibility/accessibility.go @@ -9,22 +9,33 @@ const serviceName string = "com.apple.accessibility.axAuditDaemon.remoteserver" // NewWithoutEventChangeListeners creates and connects to the given device, a new ControlInterface instance // without setting accessibility event change listeners to avoid keeping constant connection. -func NewWithoutEventChangeListeners(device ios.DeviceEntry) (ControlInterface, error) { +func NewWithoutEventChangeListeners(device ios.DeviceEntry) (*ControlInterface, error) { conn, err := dtx.NewUsbmuxdConnection(device, serviceName) if err != nil { - return ControlInterface{}, err + return nil, err } - control := ControlInterface{conn.GlobalChannel()} + control := NewControlInterface(conn.GlobalChannel(), "") return control, nil } // New creates and connects to the given device, a new ControlInterface instance -func New(device ios.DeviceEntry) (ControlInterface, error) { +func New(device ios.DeviceEntry) (*ControlInterface, error) { conn, err := dtx.NewUsbmuxdConnection(device, serviceName) if err != nil { - return ControlInterface{}, err + return nil, err } - control := ControlInterface{conn.GlobalChannel()} + control := NewControlInterface(conn.GlobalChannel(), "") + err = control.init() + return control, err +} + +// NewWithWDA creates and connects to the given device, a new ControlInterface instance with WDA host configured +func NewWithWDA(device ios.DeviceEntry, wdaHost string) (*ControlInterface, error) { + conn, err := dtx.NewUsbmuxdConnection(device, serviceName) + if err != nil { + return nil, err + } + control := NewControlInterface(conn.GlobalChannel(), wdaHost) err = control.init() return control, err } diff --git a/ios/accessibility/accessibility_control.go b/ios/accessibility/accessibility_control.go index 1defae48..67613b4e 100644 --- a/ios/accessibility/accessibility_control.go +++ b/ios/accessibility/accessibility_control.go @@ -1,7 +1,16 @@ package accessibility import ( + "bytes" + "encoding/base64" + "encoding/json" "fmt" + "io" + "net/http" + "regexp" + "strconv" + "strings" + "time" dtx "github.com/danielpaulus/go-ios/ios/dtx_codec" "github.com/danielpaulus/go-ios/ios/nskeyedarchiver" @@ -11,7 +20,61 @@ import ( // ControlInterface provides a simple interface to controlling the AX service on the device // It only needs the global dtx channel as all AX methods are invoked on it. type ControlInterface struct { - channel *dtx.Channel + channel *dtx.Channel + currentPlatformElementValue string + currentFocusedLabel string + currentCaptionText string + lastFetchAt time.Time + labelTraverseCount map[string]int + wdaHost string // WDA host for alert detection and actions + wdaActionWaitTime time.Duration // Wait time after WDA action before checking alerts again + elementChangeTimeout time.Duration // Timeout for waiting for element changes +} + +// Direction represents navigation direction values used by AX service +const ( + DirectionPrevious int32 = 3 + DirectionNext int32 = 4 + DirectionFirst int32 = 5 + DirectionLast int32 = 6 +) + +// NewControlInterface creates a new ControlInterface with the given channel and optional WDA host +func NewControlInterface(channel *dtx.Channel, wdaHost string) *ControlInterface { + return &ControlInterface{ + channel: channel, + wdaHost: wdaHost, + labelTraverseCount: make(map[string]int), + wdaActionWaitTime: 100 * time.Millisecond, // Default wait time + elementChangeTimeout: 5 * time.Second, // Default timeout for element changes + } +} + +// SetWDAHost sets the WDA host for alert detection and actions +func (a *ControlInterface) SetWDAHost(wdaHost string) { + log.Infof("Setting WDA host to: %q", wdaHost) + a.wdaHost = wdaHost +} + +// ClearWDAHost clears the WDA host, disabling alert detection +func (a *ControlInterface) ClearWDAHost() { + log.Infof("Clearing WDA host (was: %q)", a.wdaHost) + a.wdaHost = "" +} + +// SetWDAActionWaitTime sets the wait time after WDA action before checking alerts again +func (a *ControlInterface) SetWDAActionWaitTime(waitTime time.Duration) { + a.wdaActionWaitTime = waitTime +} + +// SetElementChangeTimeout sets the timeout for waiting for element changes +func (a *ControlInterface) SetElementChangeTimeout(timeout time.Duration) { + a.elementChangeTimeout = timeout +} + +// GetWDAHost returns the current WDA host configuration +func (a *ControlInterface) GetWDAHost() string { + return a.wdaHost } func (a ControlInterface) readhostAppStateChanged() { @@ -38,14 +101,40 @@ func (a ControlInterface) readhostInspectorNotificationReceived() { } } +func (a ControlInterface) readhostFoundAuditIssue() { + for { + msg := a.channel.ReceiveMethodCall("hostFoundAuditIssue:") + // optionally, parse or log; for now, just drain to avoid blocking + _ = msg + } +} + +func (a ControlInterface) readhostAppendAuditLog() { + for { + msg := a.channel.ReceiveMethodCall("hostAppendAuditLog:") + _ = msg + } +} + // init wires up event receivers and gets Info from the device func (a ControlInterface) init() error { a.channel.RegisterMethodForRemote("hostInspectorCurrentElementChanged:") a.channel.RegisterMethodForRemote("hostInspectorMonitoredEventTypeChanged:") a.channel.RegisterMethodForRemote("hostAppStateChanged:") a.channel.RegisterMethodForRemote("hostInspectorNotificationReceived:") + // audit results callback + a.channel.RegisterMethodForRemote("hostDeviceDidCompleteAuditCategoriesWithAuditIssues:") + // some older versions might emit a slightly different selector + a.channel.RegisterMethodForRemote("hostDeviceDidCompleteAuditCaseIDsWithAuditIssues:") + // issue-stream callback during audit + a.channel.RegisterMethodForRemote("hostFoundAuditIssue:") + // audit log lines (can contain rects as text) + a.channel.RegisterMethodForRemote("hostAppendAuditLog:") go a.readhostAppStateChanged() go a.readhostInspectorNotificationReceived() + // drain audit issue stream and logs so they don't block dispatch + go a.readhostFoundAuditIssue() + go a.readhostAppendAuditLog() err := a.notifyPublishedCapabilities() if err != nil { @@ -64,11 +153,14 @@ func (a ControlInterface) init() error { } log.Info("Api version:", apiVersion) - auditCaseIds, err := a.deviceAllAuditCaseIDs() - if err != nil { - return err + if a.labelTraverseCount == nil { + a.labelTraverseCount = make(map[string]int) } - log.Info("AuditCaseIDs", auditCaseIds) + // auditCaseIds, err := a.deviceAllAuditCaseIDs() + // if err != nil { + // return err + // } + // log.Info("AuditCaseIDs", auditCaseIds) deviceInspectorSupportedEventTypes, err := a.deviceInspectorSupportedEventTypes() if err != nil { @@ -87,13 +179,13 @@ func (a ControlInterface) init() error { return err } - for _, v := range auditCaseIds { - name, err := a.deviceHumanReadableDescriptionForAuditCaseID(v) - if err != nil { - return err - } - log.Infof("%s -- %s", v, name) - } + // for _, v := range auditCaseIds { + // name, err := a.deviceHumanReadableDescriptionForAuditCaseID(v) + // if err != nil { + // return err + // } + // log.Infof("%s -- %s", v, name) + // } return nil } @@ -131,13 +223,278 @@ func (a ControlInterface) TurnOff() { } // GetElement moves the green selection rectangle one element further -func (a ControlInterface) GetElement() { +func (a *ControlInterface) GetElement() { log.Info("changing") - a.deviceInspectorMoveWithOptions() + a.deviceInspectorMoveWithOptions(DirectionNext) // a.deviceInspectorMoveWithOptions() + log.Info("before changed") resp := a.awaitHostInspectorCurrentElementChanged() - log.Info("item changed", resp) + + // Assume 'resp' is your top-level map[string]interface{} object + + value, ok := resp["Value"].(map[string]interface{}) + if !ok { + log.Warn("resp[\"Value\"] is not a map") + return + } + + innerValue, ok := value["Value"].(map[string]interface{}) + if !ok { + log.Warn("Value[\"Value\"] is not a map") + return + } + + // Capture caption text if present + if capRaw, ok := innerValue["CaptionTextValue_v1"]; ok { + capVal := a.deserializeObject(capRaw) + if s, ok := capVal.(string); ok && s != "" { + a.currentCaptionText = s + log.Infof("caption: %q", s) + } else if capVal != nil { + a.currentCaptionText = fmt.Sprintf("%v", capVal) + } + } + + elementValue, ok := innerValue["ElementValue_v1"].(map[string]interface{}) + if !ok { + log.Warn("ElementValue_v1 is not a map") + return + } + + axElement, ok := elementValue["Value"].(map[string]interface{}) + if !ok { + log.Warn("ElementValue_v1[\"Value\"] is not a map") + return + } + + platformElement, ok := axElement["Value"].(map[string]interface{})["PlatformElementValue_v1"].(map[string]interface{}) + if !ok { + log.Warn("PlatformElementValue_v1 is not a map") + return + } + + byteArray, ok := platformElement["Value"].([]byte) + if !ok { + log.Warn("PlatformElementValue_v1[\"Value\"] is not a []byte") + return + } + encoded := base64.StdEncoding.EncodeToString(byteArray) + // a.GetRectForPlatformElement(encoded) + // // Now decode the byte array + // log.Infof("Attempting to decode PlatformElementValue_v1 raw bytes: %x", byteArray) + // decoded, err := nskeyedarchiver.Unarchive(byteArray) + // if err != nil { + // log.Warnf("Failed to decode PlatformElementValue_v1: %v", err) + // } else { + // log.Infof("Decoded PlatformElementValue_v1: %#v", decoded) + // } + // if err != nil { + // log.Warnf("Failed to decode PlatformElementValue_v1: %v", err) + // return + // } + a.currentPlatformElementValue = encoded + + if label, err := a.GetElementLabel(); err == nil && label != "" { + if a.labelTraverseCount == nil { + a.labelTraverseCount = make(map[string]int) + } + a.labelTraverseCount[label] = a.labelTraverseCount[label] + 1 + a.currentFocusedLabel = label + log.Infof("label '%s' traverse count=%d", label, a.labelTraverseCount[label]) + } + + // _, _ = a.SupportedAuditTypes() + + // // Convert []interface{} -> []int32 + // var ids []int32 + // for _, v := range types { + // switch t := v.(type) { + // case int32: + // ids = append(ids, t) + // case int, int64, uint64, float64: + // ids = append(ids, int32(int64(fmt.Sprintf("%v", t)[0]))) // replace with real numeric conversion in your code + // } + // } + + // removed automatic RunAudit() to avoid delaying navigation + + // log.Infof("Label: %q (err=%v)", label, err) + + // rect, err := a.GetFocusedElementRectViaAudit() + // if err != nil { + // log.Error(err) + // return + // } + // log.Infof("rect: %#v", rect) + + // xNorm := 195.0 / 390.0 + // yNorm := 245.0 / 844.0 + + // rect, _, _ = a.FetchElementAtNormalizedDeviceCoordinate(xNorm, yNorm) + // if err != nil { + // log.Error(err) + // return + // } + // log.Infof("rect: %#v", rect) + + // _, _, _ = a.FetchElementAtNormalizedDeviceCoordinateViaAttribute(22.0, 468.0) + + // pos, err := a.GetElementFrame() + // log.Infof("PlatformElementValue_v1 raw bytes: %v", encoded) + // if err != nil { + // log.Error(err) + // return + // } + // log.Infof("cursor position: %#v", pos) +} + +// checkForAlerts checks if there are any XCUIElementTypeAlert elements present using WDA +func (a *ControlInterface) checkForAlerts() (bool, error) { + if a.wdaHost == "" { + return false, fmt.Errorf("WDA host not configured") + } + + findURL := fmt.Sprintf("%s/wda/elementsWithCoords", a.wdaHost) + payload := map[string]string{ + "using": "predicate string", + "value": "type == \"XCUIElementTypeAlert\"", + } + body, err := json.Marshal(payload) + if err != nil { + return false, err + } + + resp, err := http.Post(findURL, "application/json", bytes.NewReader(body)) + if err != nil { + return false, err + } + defer resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + b, _ := io.ReadAll(resp.Body) + return false, fmt.Errorf("WDA /elementsWithCoords %d: %s", resp.StatusCode, string(b)) + } + + var result struct { + Value []struct { + Element string `json:"ELEMENT"` + ElementUUID string `json:"element-6066-11e4-a52e-4f735466cecf"` + Rect map[string]float64 `json:"rect"` + } `json:"value"` + } + + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return false, err + } + + // Return true if any alert elements are found + return len(result.Value) > 0, nil +} + +func (a *ControlInterface) PerformAction(actionName string) error { + if a.currentPlatformElementValue == "" { + return fmt.Errorf("no selected element to perform action on") + } + + log.Infof("PerformAction called with WDA host: %q", a.wdaHost) + + // Check for alerts first - if alerts are present, use WDA action instead + if a.wdaHost != "" { + hasAlerts, err := a.checkForAlerts() + if err != nil { + log.Warnf("Failed to check for alerts, falling back to default action: %v", err) + } else if hasAlerts { + log.Info("Alert detected, switching to WDA action") + _, err := a.PerformWDAAction() + if err != nil { + return err + } + + // After WDA action, check again for alerts to determine next action + // Wait for UI to update after the action + // time.Sleep(a.wdaActionWaitTime) + + // hasAlertsAfter, err := a.checkForAlerts() + // if err != nil { + // log.Warnf("Failed to check for alerts after WDA action: %v", err) + // return nil // WDA action succeeded, just log the warning + // } + + // if hasAlertsAfter { + // log.Info("Alert still present after WDA action") + // return nil // Alert still there, WDA action completed + // } else { + // log.Info("Alert dismissed, switching back to default action") + // // Alert was dismissed, now perform the original action + // return a.performDefaultAction(actionName) + // } + } + } + + // If no alerts detected or WDA not configured, perform default action + return a.performDefaultAction(actionName) +} + +// performDefaultAction performs the standard accessibility action without alert checking +func (a *ControlInterface) performDefaultAction(actionName string) error { + platformBytes, err := base64.StdEncoding.DecodeString(a.currentPlatformElementValue) + if err != nil { + return fmt.Errorf("invalid PlatformElementValue_v1 base64: %w", err) + } + + elementArg := nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "AXAuditElement_v1", + "Value": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", + "Value": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "PlatformElementValue_v1": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", + "Value": platformBytes, + }), + }), + }), + }) + + attributeArg := nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "AXAuditElementAttribute_v1", + "Value": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", + "Value": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "AttributeNameValue_v1": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", "Value": actionName, + }), + "HumanReadableNameValue_v1": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", "Value": "Activate", + }), + "PerformsActionValue_v1": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", "Value": true, + }), + "SettableValue_v1": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", "Value": false, + }), + "DisplayAsTree_v1": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", "Value": false, + }), + "DisplayInlineValue_v1": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", "Value": false, + }), + "IsInternal_v1": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", "Value": false, + }), + "ValueTypeValue_v1": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", "Value": int32(1), + }), + }), + }), + }) + + valueArg := nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{}) + + if _, err := a.channel.MethodCall("deviceElement:performAction:withValue:", elementArg, attributeArg, valueArg); err != nil { + return fmt.Errorf("failed to send performAction DTX message: %w", err) + } + return nil } func (a ControlInterface) UpdateAccessibilitySetting(name string, val interface{}) { @@ -159,7 +516,12 @@ func (a ControlInterface) ResetToDefaultAccessibilitySettings() error { } func (a ControlInterface) awaitHostInspectorCurrentElementChanged() map[string]interface{} { - msg := a.channel.ReceiveMethodCall("hostInspectorCurrentElementChanged:") + // Use configurable timeout to prevent indefinite blocking + msg, err := a.channel.ReceiveMethodCallWithTimeout("hostInspectorCurrentElementChanged:", a.elementChangeTimeout) + if err != nil { + log.Errorf("Timeout waiting for hostInspectorCurrentElementChanged (timeout: %v): %v", a.elementChangeTimeout, err) + panic(fmt.Sprintf("Timeout waiting for hostInspectorCurrentElementChanged: %s", err)) + } log.Info("received hostInspectorCurrentElementChanged") result, err := nskeyedarchiver.Unarchive(msg.Auxiliary.GetArguments()[0].([]byte)) if err != nil { @@ -174,13 +536,35 @@ func (a ControlInterface) awaitHostInspectorMonitoredEventTypeChanged() { log.Infof("hostInspectorMonitoredEventTypeChanged: was set to %d by the device", n[0]) } -func (a ControlInterface) deviceInspectorMoveWithOptions() { +func (a ControlInterface) GetCurrentCursorPosition() (interface{}, error) { + resp, err := a.channel.MethodCall("deviceInspectorInformCurrentCursorPosition:", nskeyedarchiver.NewNSNull()) + if err != nil { + return nil, err + } + if len(resp.Payload) > 0 { + return resp.Payload[0], nil + } + // Some replies may return data in Auxiliary as an archived object + auxArgs := resp.Auxiliary.GetArguments() + if len(auxArgs) > 0 { + if b, ok := auxArgs[0].([]byte); ok { + if decoded, err := nskeyedarchiver.Unarchive(b); err == nil && len(decoded) > 0 { + return decoded[0], nil + } + return b, nil + } + return auxArgs[0], nil + } + return nil, fmt.Errorf("no cursor position in reply") +} + +func (a ControlInterface) deviceInspectorMoveWithOptions(direction int32) { method := "deviceInspectorMoveWithOptions:" options := nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ "ObjectType": "passthrough", "Value": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ "allowNonAX": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{"ObjectType": "passthrough", "Value": false}), - "direction": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{"ObjectType": "passthrough", "Value": int32(4)}), + "direction": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{"ObjectType": "passthrough", "Value": direction}), "includeContainers": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{"ObjectType": "passthrough", "Value": true}), }), }) @@ -311,11 +695,16 @@ func (a ControlInterface) deviceSetAuditTargetPid(pid uint64) error { } func (a ControlInterface) deviceInspectorFocusOnElement() error { - return a.channel.MethodCallAsync("deviceInspectorFocusOnElement:", nskeyedarchiver.NewNSNull()) + return a.channel.MethodCallAsync("deviceInspectorFocusOnElement:", map[string]interface{}{}) } func (a ControlInterface) deviceInspectorPreviewOnElement() error { - return a.channel.MethodCallAsync("deviceInspectorPreviewOnElement:", nskeyedarchiver.NewNSNull()) + resp, err := a.channel.MethodCall("deviceInspectorPreviewOnElement:", nskeyedarchiver.NewNSNull()) + if err != nil { + return err + } + log.Infof("deviceInspectorPreviewOnElement reply: header=%s payload=%#v", resp.PayloadHeader.String(), resp.Payload) + return nil } func (a ControlInterface) deviceHighlightIssue() error { @@ -326,6 +715,1447 @@ func (a ControlInterface) deviceInspectorSetMonitoredEventType(eventtype uint64) return a.channel.MethodCallAsync("deviceInspectorSetMonitoredEventType:", eventtype) } -func (a ControlInterface) deviceInspectorShowVisuals(val bool) error { - return a.channel.MethodCallAsync("deviceInspectorShowVisuals:", val) +func (a *ControlInterface) GetElementAttribute(attributeName string) (interface{}, error) { + if a.currentPlatformElementValue == "" { + return nil, fmt.Errorf("no selected element to query attribute for") + } + + platformBytes, err := base64.StdEncoding.DecodeString(a.currentPlatformElementValue) + if err != nil { + return nil, fmt.Errorf("invalid PlatformElementValue_v1 base64: %w", err) + } + + elementArg := nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "AXAuditElement_v1", + "Value": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", + "Value": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "PlatformElementValue_v1": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", + "Value": platformBytes, + }), + }), + }), + }) + + attributeArg := nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "AXAuditElementAttribute_v1", + "Value": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", + "Value": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "AttributeNameValue_v1": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", + "Value": attributeName, + }), + "DisplayAsTree_v1": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", "Value": false, + }), + "DisplayInlineValue_v1": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", "Value": false, + }), + "HumanReadableNameValue_v1": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", "Value": attributeName, + }), + "IsInternal_v1": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", "Value": false, + }), + "PerformsActionValue_v1": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", "Value": false, + }), + "SettableValue_v1": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", "Value": false, + }), + "ValueTypeValue_v1": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", "Value": int32(2), + }), + }), + }), + }) + + resp, err := a.channel.MethodCall("deviceElement:valueForAttribute:", elementArg, attributeArg) + if err != nil { + return nil, err + } + if len(resp.Payload) > 0 { + return resp.Payload[0], nil + } + auxArgs := resp.Auxiliary.GetArguments() + if len(auxArgs) > 0 { + if b, ok := auxArgs[0].([]byte); ok { + if decoded, err := nskeyedarchiver.Unarchive(b); err == nil && len(decoded) > 0 { + return decoded[0], nil + } + return b, nil + } + return auxArgs[0], nil + } + return nil, fmt.Errorf("attribute %s not found in reply", attributeName) +} + +// getLabelForPlatformElement queries the Label attribute for a raw PlatformElement bytes (base64-encoded) +func (a *ControlInterface) getLabelForPlatformElement(platformBase64 string) (string, error) { + platformBytes, err := base64.StdEncoding.DecodeString(platformBase64) + if err != nil { + return "", fmt.Errorf("invalid platform element base64: %w", err) + } + + elementArg := nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "AXAuditElement_v1", + "Value": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", + "Value": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "PlatformElementValue_v1": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", + "Value": platformBytes, + }), + }), + }), + }) + + attributeArg := nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "AXAuditElementAttribute_v1", + "Value": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", + "Value": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "AttributeNameValue_v1": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", + "Value": "Label", + }), + "DisplayAsTree_v1": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", "Value": false, + }), + "DisplayInlineValue_v1": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", "Value": false, + }), + "HumanReadableNameValue_v1": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", "Value": "Label", + }), + "IsInternal_v1": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", "Value": false, + }), + "PerformsActionValue_v1": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", "Value": false, + }), + "SettableValue_v1": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", "Value": false, + }), + "ValueTypeValue_v1": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", "Value": int32(2), + }), + }), + }), + }) + + resp, err := a.channel.MethodCall("deviceElement:valueForAttribute:", elementArg, attributeArg) + if err != nil { + return "", err + } + if len(resp.Payload) > 0 { + if s, ok := resp.Payload[0].(string); ok { + return s, nil + } + return fmt.Sprintf("%v", resp.Payload[0]), nil + } + auxArgs := resp.Auxiliary.GetArguments() + if len(auxArgs) > 0 { + if b, ok := auxArgs[0].([]byte); ok { + if decoded, err := nskeyedarchiver.Unarchive(b); err == nil && len(decoded) > 0 { + if s, ok := decoded[0].(string); ok { + return s, nil + } + return fmt.Sprintf("%v", decoded[0]), nil + } + } + return fmt.Sprintf("%v", auxArgs[0]), nil + } + return "", fmt.Errorf("label not found in reply") +} + +func (a *ControlInterface) GetElementLabel() (string, error) { + + val, err := a.GetElementAttribute("Label") + if err != nil { + return "", err + } + s, ok := val.(string) + if ok { + return s, nil + } + return fmt.Sprintf("%v", val), nil +} + +// GetRectForPlatformElement returns a rect for the provided PlatformElementValue_v1 (base64). +// Tries AXFrame, then fallbacks used by GetElementFrame. +func (a *ControlInterface) GetRectForPlatformElement(platformBase64 string) (map[string]float64, error) { + platformBytes, err := base64.StdEncoding.DecodeString(platformBase64) + if err != nil { + return nil, fmt.Errorf("invalid platform element base64: %w", err) + } + + elementArg := nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "AXAuditElement_v1", + "Value": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", + "Value": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "PlatformElementValue_v1": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", + "Value": platformBytes, + }), + }), + }), + }) + + attr := func(name string) (interface{}, error) { + attributeArg := nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "AXAuditElementAttribute_v1", + "Value": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", + "Value": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "AttributeNameValue_v1": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", + "Value": name, + }), + "DisplayAsTree_v1": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", "Value": false, + }), + "DisplayInlineValue_v1": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", "Value": false, + }), + "HumanReadableNameValue_v1": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", "Value": name, + }), + "IsInternal_v1": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", "Value": false, + }), + "PerformsActionValue_v1": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", "Value": false, + }), + "SettableValue_v1": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", "Value": false, + }), + "ValueTypeValue_v1": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", "Value": int32(256), + }), + }), + }), + }) + resp, err := a.channel.MethodCall("deviceElement:valueForAttribute:", elementArg, attributeArg) + if err != nil { + return nil, err + } + if len(resp.Payload) > 0 { + return resp.Payload[0], nil + } + aux := resp.Auxiliary.GetArguments() + if len(aux) > 0 { + if b, ok := aux[0].([]byte); ok { + if decoded, err := nskeyedarchiver.Unarchive(b); err == nil && len(decoded) > 0 { + return decoded[0], nil + } + return b, nil + } + return aux[0], nil + } + return nil, fmt.Errorf("attribute %s not found", name) + } + + // Primary: AXFrame + if v, err := attr("AXFrame"); err == nil { + if rect, ok := tryExtractRect(v); ok { + return rect, nil + } + } + // Fallbacks + for _, name := range []string{"AXBounds", "AXFrameInContainerSpace"} { + if v, err := attr(name); err == nil { + if rect, ok := tryExtractRect(v); ok { + return rect, nil + } + } + } + // Combine pos+size + vpos, _ := attr("AXPosition") + vsz, _ := attr("AXSize") + if pos, ok := tryExtractPoint(vpos); ok { + if size, ok := tryExtractSize(vsz); ok { + return map[string]float64{"x": pos[0], "y": pos[1], "width": size[0], "height": size[1]}, nil + } + } + return nil, fmt.Errorf("rect not available for element") +} + +func (a *ControlInterface) GetElementFrame() (map[string]float64, error) { + if a.currentPlatformElementValue == "" { + return nil, fmt.Errorf("no selected element to query frame for") + } + + platformBytes, err := base64.StdEncoding.DecodeString(a.currentPlatformElementValue) + if err != nil { + return nil, fmt.Errorf("invalid PlatformElementValue_v1 base64: %w", err) + } + + elementArg := nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "AXAuditElement_v1", + "Value": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", + "Value": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "PlatformElementValue_v1": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", + "Value": platformBytes, + }), + }), + }), + }) + + // Try simplest form first: attribute as plain string (NSString) + attributeArg := nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "AXAuditElementAttribute_v1", + "Value": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", + "Value": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "AttributeNameValue_v1": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", + "Value": "AXFrame", + }), + "DisplayAsTree_v1": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", + "Value": false, + }), + "DisplayInlineValue_v1": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", + "Value": false, + }), + "HumanReadableNameValue_v1": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", + "Value": "Frame", + }), + "IsInternal_v1": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", + "Value": false, + }), + "PerformsActionValue_v1": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", + "Value": false, + }), + "SettableValue_v1": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", + "Value": false, + }), + "ValueTypeValue_v1": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", + "Value": int32(256), + }), + }), + }), + }) + + resp, err := a.channel.MethodCall("deviceElement:valueForAttribute:", elementArg, attributeArg) + if err != nil { + return nil, err + } + if rect, ok := a.getPreviewRect(); ok { + return rect, nil + } + + // Attempt to extract rect from payload + if len(resp.Payload) > 0 { + if rect, ok := tryExtractRect(resp.Payload[0]); ok { + return rect, nil + } + } + // Try auxiliary (archived value) + auxArgs := resp.Auxiliary.GetArguments() + if len(auxArgs) > 0 { + if b, ok := auxArgs[0].([]byte); ok { + if decoded, err := nskeyedarchiver.Unarchive(b); err == nil && len(decoded) > 0 { + if rect, ok := tryExtractRect(decoded[0]); ok { + return rect, nil + } + } + } + if rect, ok := tryExtractRect(auxArgs[0]); ok { + return rect, nil + } + } + + // Fallback 1: try additional common attribute names + candidates := []string{"AXBounds", "AXFrameInContainerSpace"} + for _, name := range candidates { + val, err := a.GetElementAttribute(name) + if err == nil { + if rect, ok := tryExtractRect(val); ok { + return rect, nil + } + } + } + + // Fallback 2: combine position + size + if pos, ok := tryExtractPoint(mustGetAttr(a, "AXPosition")); ok { + if size, ok := tryExtractSize(mustGetAttr(a, "AXSize")); ok { + return map[string]float64{"x": pos[0], "y": pos[1], "width": size[0], "height": size[1]}, nil + } + } + + // Fallback 3: use preview reply (often includes a rect-like structure) + if rect, ok := a.getPreviewRect(); ok { + return rect, nil + } + + // Fallback 4: use current cursor position + size + if cur, err := a.GetCurrentCursorPosition(); err == nil { + if pos, ok := tryExtractPoint(cur); ok { + if size, ok := tryExtractSize(mustGetAttr(a, "AXSize")); ok { + return map[string]float64{"x": pos[0], "y": pos[1], "width": size[0], "height": size[1]}, nil + } + } + } + + return nil, fmt.Errorf("frame not found in reply") +} + +func tryExtractRect(v interface{}) (map[string]float64, bool) { + // handle NSValue decoded rects + if nv, ok := v.(nskeyedarchiver.NSValue); ok { + if rect, ok := parseNSRectString(nv.NSRectval); ok { + return rect, true + } + } + if s, ok := v.(string); ok { + if rect, ok := parseNSRectString(s); ok { + return rect, true + } + } + + m, ok := v.(map[string]interface{}) + if !ok { + return nil, false + } + // Common key variants + keys := [][]string{ + {"X", "Y", "Width", "Height"}, + {"x", "y", "width", "height"}, + {"XValue", "YValue", "WidthValue", "HeightValue"}, + } + for _, ks := range keys { + x, xok := toFloat(m[ks[0]]) + y, yok := toFloat(m[ks[1]]) + w, wok := toFloat(m[ks[2]]) + h, hok := toFloat(m[ks[3]]) + if xok && yok && wok && hok { + return map[string]float64{"x": x, "y": y, "width": w, "height": h}, true + } + } + return nil, false +} + +var rectNumRe = regexp.MustCompile(`-?\d+(?:\.\d+)?`) + +func parseNSRectString(s string) (map[string]float64, bool) { + nums := rectNumRe.FindAllString(s, 4) + if len(nums) < 4 { + return nil, false + } + parse := func(ss string) (float64, bool) { + f, err := strconv.ParseFloat(ss, 64) + return f, err == nil + } + x, ok1 := parse(nums[0]) + y, ok2 := parse(nums[1]) + w, ok3 := parse(nums[2]) + h, ok4 := parse(nums[3]) + if ok1 && ok2 && ok3 && ok4 { + return map[string]float64{"x": x, "y": y, "width": w, "height": h}, true + } + return nil, false +} + +func toFloat(v interface{}) (float64, bool) { + switch t := v.(type) { + case float64: + return t, true + case float32: + return float64(t), true + case int: + return float64(t), true + case int32: + return float64(t), true + case int64: + return float64(t), true + case uint: + return float64(t), true + case uint32: + return float64(t), true + case uint64: + return float64(t), true + default: + return 0, false + } +} + +// tryExtractPoint attempts to parse a point-like value and returns [x,y] +func tryExtractPoint(v interface{}) ([2]float64, bool) { + // NSValue string, like "{x, y}" or "x= y=" + if s, ok := v.(string); ok { + nums := rectNumRe.FindAllString(s, 2) + if len(nums) >= 2 { + xOk, yOk := false, false + var x, y float64 + if xv, ok := strconv.ParseFloat(nums[0], 64); ok == nil { + x = xv + xOk = true + } + if yv, ok := strconv.ParseFloat(nums[1], 64); ok == nil { + y = yv + yOk = true + } + if xOk && yOk { + return [2]float64{x, y}, true + } + } + } + // map-like + if m, ok := v.(map[string]interface{}); ok { + if x, xok := toFloat(m["x"]); xok { + if y, yok := toFloat(m["y"]); yok { + return [2]float64{x, y}, true + } + } + if x, xok := toFloat(m["X"]); xok { + if y, yok := toFloat(m["Y"]); yok { + return [2]float64{x, y}, true + } + } + } + return [2]float64{}, false +} + +// tryExtractSize attempts to parse a size-like value and returns [w,h] +func tryExtractSize(v interface{}) ([2]float64, bool) { + if s, ok := v.(string); ok { + nums := rectNumRe.FindAllString(s, 2) + if len(nums) >= 2 { + if wv, err := strconv.ParseFloat(nums[0], 64); err == nil { + if hv, err := strconv.ParseFloat(nums[1], 64); err == nil { + return [2]float64{wv, hv}, true + } + } + } + } + if m, ok := v.(map[string]interface{}); ok { + if w, wok := toFloat(m["width"]); wok { + if h, hok := toFloat(m["height"]); hok { + return [2]float64{w, h}, true + } + } + if w, wok := toFloat(m["Width"]); wok { + if h, hok := toFloat(m["Height"]); hok { + return [2]float64{w, h}, true + } + } + } + return [2]float64{}, false +} + +// mustGetAttr fetches an attribute; returns nil if missing +func mustGetAttr(a *ControlInterface, name string) interface{} { + v, err := a.GetElementAttribute(name) + if err != nil { + return nil + } + return v +} + +// getPreviewRect calls deviceInspectorPreviewOnElement and tries to extract a rect from reply +func (a ControlInterface) getPreviewRect() (map[string]float64, bool) { + resp, err := a.channel.MethodCall("deviceInspectorPreviewOnElement:", nskeyedarchiver.NewNSNull()) + if err == nil { + // payload first + for _, p := range resp.Payload { + if rect, ok := tryExtractRect(p); ok { + return rect, true + } + } + // aux archived + for _, arg := range resp.Auxiliary.GetArguments() { + if b, ok := arg.([]byte); ok { + if decoded, err := nskeyedarchiver.Unarchive(b); err == nil { + for _, dv := range decoded { + if rect, ok := tryExtractRect(dv); ok { + return rect, true + } + } + } + } else if rect, ok := tryExtractRect(arg); ok { + return rect, true + } + } + } + return nil, false +} + +func (a *ControlInterface) ProbeAttributes(attributeNames []string) { + for _, name := range attributeNames { + val, err := a.GetElementAttribute(name) + if err != nil { + log.Infof("attr %s: err=%v", name, err) + continue + } + if rect, ok := tryExtractRect(val); ok { + log.Infof("attr %s: RECT=%#v", name, rect) + continue + } + log.Infof("attr %s: val=%#v", name, val) + } +} + +func (a *ControlInterface) ProbeDefaultGeometryAttributes() { + defaultAttrs := []string{ + // "AXFrame", + // "AXActivationPoint", + // "AXPosition", + // "AXSize", + // "AXBounds", + // "AXFrameInContainerSpace", + // sanity checks + "axElement", + "elementRef", + "Label", + "Value", + "Title", + "Name", + "Header", + "Identifier", + } + a.ProbeAttributes(defaultAttrs) +} + +func (a *ControlInterface) ProbeRectLikeAttributes() { + candidates := []string{ + "Header", + "ElementMemoryAddress", + "Label", + // "Label", + // "_elementRect", + // "elementRect", + // "V_elementRect", + // "ElementRect", + // "elementRect", + // "ElementRectValue_v1", + // "elementRectValue_v1", + // "ElementRectValue_v1", + + // "Value", + // "Type", + // "Identifier", + + // "AXFrame", + // "AXBounds", + // "axposition", + // "AXSize", + // "AXActivationPoint", + // "AXFrameInContainerSpace", + // "AXAuditRect_v1", + // "RectValue_v1", + // "AXFrameValue_v1", + // "AXBoundsValue_v1", + // "AXPositionValue_v1", + // // mac-style variants that may be bridged + // "accessibilityFrame", + // "accessibilityActivationPoint", + // // generic fallbacks observed in symbol dumps + // "Frame", + // "frame", + // "bounds", + // "position", + // "size", + // "rect", + // "rect", + // "ElementRect", + // "elementRect", + // "displayBounds", + // "borderFrame", + // "imageFrame", + } + a.ProbeAttributes(candidates) +} + +// ===== Accessibility Audit Support ===== + +// AuditType mirrors known audit categories observed in third-party clients +// and symbol dumps. Values are best-effort and may vary by iOS version. +const ( + auditTypeDynamicText = 3001 + auditTypeDynamicTextAlt = 3002 + auditTypeTextClipped = 3003 + auditTypeElementDetection = 1000 + auditTypeSufficientDescription = 5000 + auditTypeHitRegion = 100 + auditTypeContrast = 12 + auditTypeContrastAlt = 13 +) + +var auditTypeDescriptions = map[int]string{ + auditTypeDynamicText: "testTypeDynamicText", + auditTypeDynamicTextAlt: "testTypeDynamicText", + auditTypeTextClipped: "testTypeTextClipped", + auditTypeElementDetection: "testTypeElementDetection", + auditTypeSufficientDescription: "testTypeSufficientElementDescription", + auditTypeHitRegion: "testTypeHitRegion", + auditTypeContrast: "testTypeContrast", + auditTypeContrastAlt: "testTypeContrast", +} + +// FetchElementAtNormalizedDeviceCoordinate queries the AX service for the element +// located at the normalized device coordinate (x,y) where both are in [0.0, 1.0]. +// It returns the PlatformElementValue_v1 as base64 (if found) and the decoded object tree. +func (a *ControlInterface) FetchElementAtNormalizedDeviceCoordinate(x, y float64) (string, interface{}, error) { + // Build CGPoint-like structure via passthrough dictionary + // coord := nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + // "ObjectType": "passthrough", + // "Value": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + // "x": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + // "ObjectType": "passthrough", "Value": x, + // }), + // "y": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + // "ObjectType": "passthrough", "Value": y, + // }), + // }), + // }) + // if needed: yNorm = 1.0 - (yPx / h) + + coord := nskeyedarchiver.NewNSValuePoint(x, y) + // if xml, err := nskeyedarchiver.ArchiveXML(coord); err == nil { + // log.Infof("AX arg (coord) plist:\n%s", xml) + // } + resp, err := a.channel.MethodCall("deviceFetchElementAtNormalizedDeviceCoordinate:", coord) + if err != nil { + return "", nil, err + } + + // Try payload first + if len(resp.Payload) > 0 { + if b, ok := resp.Payload[0].([]byte); ok { + if decoded, err := nskeyedarchiver.Unarchive(b); err == nil && len(decoded) > 0 { + obj := a.deserializeObject(decoded) + if be, ok := findPlatformElementBase64(obj); ok { + // Remember as current for follow-up calls + a.currentPlatformElementValue = be + return be, obj, nil + } + return "", obj, nil + } + } else { + obj := a.deserializeObject(resp.Payload[0]) + if be, ok := findPlatformElementBase64(obj); ok { + a.currentPlatformElementValue = be + return be, obj, nil + } + return "", obj, nil + } + } + + // Try auxiliary archived arguments + for _, arg := range resp.Auxiliary.GetArguments() { + if b, ok := arg.([]byte); ok { + if decoded, err := nskeyedarchiver.Unarchive(b); err == nil && len(decoded) > 0 { + obj := a.deserializeObject(decoded) + if be, ok := findPlatformElementBase64(obj); ok { + a.currentPlatformElementValue = be + return be, obj, nil + } + return "", obj, nil + } + } else { + obj := a.deserializeObject(arg) + if be, ok := findPlatformElementBase64(obj); ok { + a.currentPlatformElementValue = be + return be, obj, nil + } + return "", obj, nil + } + } + return "", nil, fmt.Errorf("no element returned for normalized coordinate") +} + +// FetchElementAtNormalizedDeviceCoordinateViaAttribute uses the system-wide element +// and attribute 91701 with a NSValue(CGPoint) parameter to fetch an element. +// Coordinates must be normalized [0..1]. This respects a 0.1s throttle like the +// ObjC reference implementation. +func (a *ControlInterface) FetchElementAtNormalizedDeviceCoordinateViaAttribute(x, y float64) (string, interface{}, error) { + if dt := time.Since(a.lastFetchAt); dt < 100*time.Millisecond { + // throttle: return last known if available + if a.currentPlatformElementValue != "" { + return a.currentPlatformElementValue, nil, nil + } + } + coord := nskeyedarchiver.NewNSValuePoint(x, y) + + // system wide element as an AXAuditElement_v1 wrapper + sysWide := nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "AXAuditElement_v1", + "Value": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", + "Value": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + // Convention: system-wide element may be encoded with a sentinel value or empty dict + }), + }), + }) + + // Attribute 91701 with parameter = NSValue(CGPoint) + attr := nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "AXAuditElementAttribute_v1", + "Value": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", + "Value": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "AttributeNameValue_v1": nskeyedarchiver.NewNSMutableDictionary(map[string]interface{}{ + "ObjectType": "passthrough", + "Value": int32(91701), + }), + }), + }), + }) + + resp, err := a.channel.MethodCall("deviceElement:valueForAttribute:", sysWide, attr, coord) + if err != nil { + return "", nil, err + } + // parse like other helpers + // payload first + if len(resp.Payload) > 0 { + if b, ok := resp.Payload[0].([]byte); ok { + if decoded, err := nskeyedarchiver.Unarchive(b); err == nil && len(decoded) > 0 { + obj := a.deserializeObject(decoded) + if be, ok := findPlatformElementBase64(obj); ok { + a.currentPlatformElementValue = be + a.lastFetchAt = time.Now() + return be, obj, nil + } + return "", obj, nil + } + } else { + obj := a.deserializeObject(resp.Payload[0]) + if be, ok := findPlatformElementBase64(obj); ok { + a.currentPlatformElementValue = be + a.lastFetchAt = time.Now() + return be, obj, nil + } + return "", obj, nil + } + } + for _, arg := range resp.Auxiliary.GetArguments() { + if b, ok := arg.([]byte); ok { + if decoded, err := nskeyedarchiver.Unarchive(b); err == nil && len(decoded) > 0 { + obj := a.deserializeObject(decoded) + if be, ok := findPlatformElementBase64(obj); ok { + a.currentPlatformElementValue = be + a.lastFetchAt = time.Now() + return be, obj, nil + } + return "", obj, nil + } + } else { + obj := a.deserializeObject(arg) + if be, ok := findPlatformElementBase64(obj); ok { + a.currentPlatformElementValue = be + a.lastFetchAt = time.Now() + return be, obj, nil + } + return "", obj, nil + } + } + return "", nil, fmt.Errorf("no element for normalized coordinate via attribute") +} + +// findPlatformElementBase64 walks an arbitrary decoded structure and returns the +// PlatformElementValue_v1 bytes as base64, if present. +func findPlatformElementBase64(v interface{}) (string, bool) { + switch t := v.(type) { + case []interface{}: + for _, it := range t { + if be, ok := findPlatformElementBase64(it); ok { + return be, true + } + } + case map[string]interface{}: + if pe, ok := t["PlatformElementValue_v1"]; ok { + if pm, ok := pe.(map[string]interface{}); ok { + if b, ok := pm["Value"].([]byte); ok { + return base64.StdEncoding.EncodeToString(b), true + } + } + } + // Dive deeper + for _, vv := range t { + if be, ok := findPlatformElementBase64(vv); ok { + return be, true + } + } + } + return "", false +} + +// AXAuditIssueV1 is a simplified Go representation of AXAuditIssue_v1 +// Only fields useful for consumers are exposed. +type AXAuditIssueV1 struct { + ElementRect map[string]float64 `json:"element_rect_value,omitempty"` + IssueClassificationRaw interface{} `json:"-"` + IssueClassificationLabel string `json:"issue_classification"` + FontSize interface{} `json:"font_size,omitempty"` + MLGeneratedDescription interface{} `json:"ml_generated_description,omitempty"` + LongDescriptionExtraInfo interface{} `json:"long_description_extra_info,omitempty"` + ForegroundColor interface{} `json:"foreground_color,omitempty"` + BackgroundColor interface{} `json:"background_color,omitempty"` + PlatformElementBase64 string `json:"platform_element_value,omitempty"` + Label string `json:"label,omitempty"` +} + +// SupportedAuditTypes returns the list of supported audit category identifiers. +// On iOS 15+ this maps to deviceAllSupportedAuditTypes, otherwise deviceAllAuditCaseIDs. +func (a *ControlInterface) SupportedAuditTypes() ([]interface{}, error) { + api, err := a.deviceAPIVersion() + if err != nil { + return nil, err + } + var resp dtx.Message + if api >= 15 { + resp, err = a.channel.MethodCall("deviceAllSupportedAuditTypes") + } else { + resp, err = a.channel.MethodCall("deviceAllAuditCaseIDs") + } + if err != nil { + return nil, err + } + // Flatten common return shapes + if len(resp.Payload) > 0 { + if list, ok := resp.Payload[0].([]interface{}); ok { + return list, nil + } + } + aux := resp.Auxiliary.GetArguments() + if len(aux) > 0 { + if b, ok := aux[0].([]byte); ok { + if decoded, err := nskeyedarchiver.Unarchive(b); err == nil && len(decoded) > 0 { + if list, ok := decoded[0].([]interface{}); ok { + return list, nil + } + } + } + } + return nil, fmt.Errorf("unsupported supported-audits reply format") +} + +// RunAudit triggers the accessibility audit for all supported audit types and waits +// for the completion callback with the resulting issues. +// For iOS >= 15, auditTypes are category integers. For older iOS, case IDs are used. +func (a *ControlInterface) RunAudit() ([]AXAuditIssueV1, error) { + // Fetch the supported types from the device first + supported, err := a.SupportedAuditTypes() + if err != nil { + return nil, err + } + // Convert the []interface{} returned by device to []int32 + auditTypes := make([]int32, 0, len(supported)) + for _, v := range supported { + switch t := v.(type) { + case int32: + auditTypes = append(auditTypes, t) + case int64: + auditTypes = append(auditTypes, int32(t)) + case int: + auditTypes = append(auditTypes, int32(t)) + case float64: + auditTypes = append(auditTypes, int32(t)) + case uint64: + auditTypes = append(auditTypes, int32(t)) + } + } + api, err := a.deviceAPIVersion() + if err != nil { + return nil, err + } + + // NSKeyedArchiver requires []interface{} (NSArray), convert the ints + values := make([]interface{}, len(auditTypes)) + for i, v := range auditTypes { + values[i] = int32(v) + } + + // Start audit. The result is delivered asynchronously as a host* callback. + if api >= 15 { + if err := a.channel.MethodCallAsync("deviceBeginAuditTypes:", values); err != nil { + return nil, err + } + } else { + if err := a.channel.MethodCallAsync("deviceBeginAuditCaseIDs:", values); err != nil { + return nil, err + } + } + + // Wait for completion event and parse issues; support selector variants + msg := a.channel.ReceiveMethodCall("hostDeviceDidCompleteAuditCategoriesWithAuditIssues:") + if len(msg.Auxiliary.GetArguments()) == 0 && len(msg.Payload) == 0 { + msg = a.channel.ReceiveMethodCall("hostDeviceDidCompleteAuditCaseIDsWithAuditIssues:") + } + + var allIssues []AXAuditIssueV1 + + // Look into auxiliary arguments first + args := msg.Auxiliary.GetArguments() + for _, aarg := range args { + if b, ok := aarg.([]byte); ok { + if decoded, err := nskeyedarchiver.Unarchive(b); err == nil { + issues := a.extractIssuesFromInterface(a.deserializeObject(decoded)) + allIssues = append(allIssues, issues...) + } + continue + } + issues := a.extractIssuesFromInterface(aarg) + allIssues = append(allIssues, issues...) + } + + // Also scan payload in case values are embedded there + for _, p := range msg.Payload { + issues := a.extractIssuesFromInterface(p) + allIssues = append(allIssues, issues...) + } + + if len(allIssues) == 0 { + return nil, fmt.Errorf("no audit issues found in reply") + } + // Enrich with labels when element bytes are available + for i := range allIssues { + if allIssues[i].PlatformElementBase64 == "" { + continue + } + label, err := a.getLabelForPlatformElement(allIssues[i].PlatformElementBase64) + if err == nil && label != "" { + allIssues[i].Label = label + } + } + return allIssues, nil +} + +// extractIssuesFromInterface walks an arbitrary decoded structure to find a list +// of AXAuditIssue_v1-like maps and converts them to AXAuditIssueV1. +func (a ControlInterface) extractIssuesFromInterface(root interface{}) []AXAuditIssueV1 { + // Attempt to locate a list of maps that contain known AXAuditIssue_v1 keys + var out []AXAuditIssueV1 + + var walk func(v interface{}) (found []map[string]interface{}) + walk = func(v interface{}) (found []map[string]interface{}) { + switch t := v.(type) { + case []interface{}: + // If this slice looks like issues, return it directly + if len(t) > 0 { + if _, ok := t[0].(map[string]interface{}); ok { + // Check sentinel keys on first element + m := t[0].(map[string]interface{}) + if hasAnyKey(m, "AXAuditIssue_v1", "IssueClassificationValue_v1", "ElementRectValue_v1") { + for _, it := range t { + if mm, ok := it.(map[string]interface{}); ok { + found = append(found, mm) + } + } + return + } + } + } + // Otherwise recurse + for _, it := range t { + found = append(found, walk(it)...) + } + case map[string]interface{}: + // Direct value under common keys + if val, ok := t["value"]; ok { + found = append(found, walk(val)...) + } + if val, ok := t["Value"]; ok { + found = append(found, walk(val)...) + } + for _, v2 := range t { + found = append(found, walk(v2)...) + } + } + return + } + + candidates := walk(root) + for _, m := range candidates { + // Some decoders keep the typed object name; accept both flat and nested forms + if val, ok := m["Value"]; ok { + if mv, ok := val.(map[string]interface{}); ok { + m = mv + } + } + issue := AXAuditIssueV1{} + + // IssueClassificationValue_v1 + if ic, ok := m["IssueClassificationValue_v1"]; ok { + issue.IssueClassificationRaw = ic + if iv, ok := toFloat(ic); ok { + // map to human label when possible + lbl, ok := auditTypeDescriptions[int(iv)] + if ok { + issue.IssueClassificationLabel = lbl + } else { + issue.IssueClassificationLabel = fmt.Sprintf("%v", ic) + } + } else { + issue.IssueClassificationLabel = fmt.Sprintf("%v", ic) + } + } + + // Rect extraction + if r, ok := m["ElementRectValue_v1"]; ok { + if rect, ok := tryExtractRect(a.deserializeObject(r)); ok { + issue.ElementRect = rect + } + } + + // Optional: extract element bytes for later attribute lookups + if ev, ok := m["ElementValue_v1"]; ok { + if mv, ok := a.deserializeObject(ev).(map[string]interface{}); ok { + if pe, ok := mv["PlatformElementValue_v1"]; ok { + if pm, ok := a.deserializeObject(pe).(map[string]interface{}); ok { + if b, ok := pm["Value"].([]byte); ok { + issue.PlatformElementBase64 = base64.StdEncoding.EncodeToString(b) + } + } + } + } + } + + // Optional fields + if v, ok := m["FontSizeValue_v1"]; ok { + issue.FontSize = a.deserializeObject(v) + } + if v, ok := m["MLGeneratedDescriptionValue_v1"]; ok { + issue.MLGeneratedDescription = a.deserializeObject(v) + } + if v, ok := m["ElementLongDescExtraInfo_v1"]; ok { + issue.LongDescriptionExtraInfo = a.deserializeObject(v) + } + if v, ok := m["ForegroundColorValue_v1"]; ok { + issue.ForegroundColor = a.deserializeObject(v) + } + if v, ok := m["BackgroundColorValue_v1"]; ok { + issue.BackgroundColor = a.deserializeObject(v) + } + + // Only append if we recognized it as an issue + if issue.IssueClassificationLabel != "" || issue.ElementRect != nil { + out = append(out, issue) + } + } + return out +} + +// deserializeObject mirrors the Python helper: unwraps {ObjectType: 'passthrough', Value: ...} +// and recursively processes containers. For other typed objects, returns their Value recursively. +func (a ControlInterface) deserializeObject(d interface{}) interface{} { + switch t := d.(type) { + case []interface{}: + out := make([]interface{}, 0, len(t)) + for _, v := range t { + out = append(out, a.deserializeObject(v)) + } + return out + case map[string]interface{}: + if ot, ok := t["ObjectType"]; ok { + if ot == "passthrough" { + return a.deserializeObject(t["Value"]) + } + // For other typed objects, we generally care about their 'Value' + if v, ok := t["Value"]; ok { + return a.deserializeObject(v) + } + return t + } + // Plain dictionary: recursively process values + out := make(map[string]interface{}, len(t)) + for k, v := range t { + out[k] = a.deserializeObject(v) + } + return out + default: + return d + } +} + +func hasAnyKey(m map[string]interface{}, keys ...string) bool { + for _, k := range keys { + if _, ok := m[k]; ok { + return true + } + } + return false +} + +// Removed WDAElementInfo usage; PerformWDAAction returns uuid after tapping the element +func (a *ControlInterface) PerformWDAAction() (string, error) { + // Use the currently focused label captured during GetElement() + lastLabel := a.currentFocusedLabel + if lastLabel == "" { + return "", fmt.Errorf("no currently focused label to act on") + } + // Fetch the traverse count for this focused label; default to 1 when missing + lastCount := 1 + if a.labelTraverseCount != nil { + if c, ok := a.labelTraverseCount[lastLabel]; ok && c > 0 { + lastCount = c + } + } + // normalize cases where label was stored like: map[ObjectType:passthrough Value:Photos] + if strings.HasPrefix(lastLabel, "map[") { + re := regexp.MustCompile(`Value:([^\]]+)`) + m := re.FindStringSubmatch(lastLabel) + if len(m) > 1 { + lastLabel = strings.TrimSpace(m[1]) + } + } + + // Check if we're dealing with a switch based on currentCaptionText + isSwitch := false + if a.currentCaptionText != "" { + // Look for patterns like "Airplane Mode, 0, Button, Toggle" or "Airplane Mode, 0, Button," + caption := strings.ToLower(a.currentCaptionText) + if strings.Contains(caption, "toggle") || + (strings.Contains(caption, "button") && strings.Contains(caption, ",")) { + isSwitch = true + log.Infof("Detected switch element based on caption: %q", a.currentCaptionText) + } + } + + // 1) find elements by label predicate via WDA + log.Infof("PerformWDAAction: WDA host is: %q", a.wdaHost) + if a.wdaHost == "" { + return "", fmt.Errorf("WDA host is not set - please enable accessibility service with wda_host parameter") + } + findURL := fmt.Sprintf("%s/wda/elementsWithCoords", a.wdaHost) + payload := map[string]string{"using": "predicate string", "value": fmt.Sprintf("label == \"%s\"", lastLabel)} + body, _ := json.Marshal(payload) + resp, err := http.Post(findURL, "application/json", bytes.NewReader(body)) + if err != nil { + return "", err + } + defer resp.Body.Close() + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + b, _ := io.ReadAll(resp.Body) + return "", fmt.Errorf("WDA /elements %d: %s", resp.StatusCode, string(b)) + } + var result struct { + Value []struct { + Element string `json:"ELEMENT"` + ElementUUID string `json:"element-6066-11e4-a52e-4f735466cecf"` + Rect map[string]float64 `json:"rect"` + Type string `json:"type"` + } `json:"value"` + } + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return "", err + } + if len(result.Value) == 0 { + return "", fmt.Errorf("no elements found for label %q", lastLabel) + } + + // 2) choose element by index = count-1 (0-based). clamp to range + idx := lastCount - 1 + if idx < 0 { + idx = 0 + } + if idx >= len(result.Value) { + idx = len(result.Value) - 1 + } + + // 3) If we detected a switch, look for XCUIElementTypeSwitch in the results + if isSwitch { + // First try to find a switch element at the same index + if idx < len(result.Value) && result.Value[idx].Type == "XCUIElementTypeSwitch" { + log.Infof("Found switch element at index %d", idx) + } else { + // Look for any switch element in the results + switchFound := false + for i, elem := range result.Value { + if elem.Type == "XCUIElementTypeSwitch" { + idx = i + switchFound = true + log.Infof("Found switch element at index %d (was %d)", i, lastCount-1) + break + } + } + if !switchFound { + log.Warnf("Switch detected but no XCUIElementTypeSwitch found in %d elements", len(result.Value)) + } + } + } + + uuid := result.Value[idx].ElementUUID + if uuid == "" { + uuid = result.Value[idx].Element + } + if uuid == "" { + return "", fmt.Errorf("element uuid not present in WDA reply") + } + + // 4) If it's a switch, use WDA click method; otherwise use testobject tap + if isSwitch { + log.Infof("Clicking switch element with UUID: %s", uuid) + err := a.clickElementByUUID(uuid) + if err != nil { + return "", fmt.Errorf("failed to click switch element: %w", err) + } + } else { + // use rect from elementsWithCoords to get x,y and call testobject tap with arguments {x,y,duration} + rect := result.Value[idx].Rect + x := rect["x"] + y := rect["y"] + if w, ok := rect["width"]; ok { + x += w / 2.0 + } + if h, ok := rect["height"]; ok { + y += h / 2.0 + } + testObjectURL := fmt.Sprintf("%s/testobject/tap", a.wdaHost) + tapArgs := map[string]float64{ + "x": x, + "y": y, + "duration": 0, + } + tapBody, _ := json.Marshal(tapArgs) + tapResp, err := http.Post(testObjectURL, "application/json", bytes.NewReader(tapBody)) + if err != nil { + return "", err + } + defer tapResp.Body.Close() + if tapResp.StatusCode < 200 || tapResp.StatusCode >= 300 { + b, _ := io.ReadAll(tapResp.Body) + return "", fmt.Errorf("testobject/tap %d: %s", tapResp.StatusCode, string(b)) + } + } + return uuid, nil +} + +// clickElementByUUID clicks on an element using WDA's element click method +func (a *ControlInterface) clickElementByUUID(uuid string) error { + if a.wdaHost == "" { + return fmt.Errorf("WDA host is not set") + } + + // Use WDA's element click method + clickURL := fmt.Sprintf("%s/element/%s/click", a.wdaHost, uuid) + log.Infof("Clicking element via WDA: %s", clickURL) + + resp, err := http.Post(clickURL, "application/json", nil) + if err != nil { + return fmt.Errorf("failed to send click request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + b, _ := io.ReadAll(resp.Body) + return fmt.Errorf("WDA element click failed %d: %s", resp.StatusCode, string(b)) + } + + log.Infof("Element %s clicked successfully", uuid) + return nil +} + +func (a ControlInterface) deviceInspectorShowVisuals(val bool) error { + return a.channel.MethodCallAsync("deviceInspectorShowVisuals:", val) +} + +// Move navigates focus using the given direction and updates current element and label state +func (a *ControlInterface) Move(direction int32) { + log.Info("changing") + a.deviceInspectorMoveWithOptions(direction) + // a.deviceInspectorMoveWithOptions() + log.Info("before changed") + + resp := a.awaitHostInspectorCurrentElementChanged() + + // Assume 'resp' is your top-level map[string]interface{} object + + value, ok := resp["Value"].(map[string]interface{}) + if !ok { + log.Warn("resp[\"Value\"] is not a map") + return + } + + innerValue, ok := value["Value"].(map[string]interface{}) + if !ok { + log.Warn("Value[\"Value\"] is not a map") + return + } + + // Capture caption text if present + if capRaw, ok := innerValue["CaptionTextValue_v1"]; ok { + capVal := a.deserializeObject(capRaw) + if s, ok := capVal.(string); ok && s != "" { + a.currentCaptionText = s + log.Infof("caption: %q", s) + } else if capVal != nil { + a.currentCaptionText = fmt.Sprintf("%v", capVal) + } + } + + elementValue, ok := innerValue["ElementValue_v1"].(map[string]interface{}) + if !ok { + log.Warn("ElementValue_v1 is not a map") + return + } + + axElement, ok := elementValue["Value"].(map[string]interface{}) + if !ok { + log.Warn("ElementValue_v1[\"Value\"] is not a map") + return + } + + platformElement, ok := axElement["Value"].(map[string]interface{})["PlatformElementValue_v1"].(map[string]interface{}) + if !ok { + log.Warn("PlatformElementValue_v1 is not a map") + return + } + + byteArray, ok := platformElement["Value"].([]byte) + if !ok { + log.Warn("PlatformElementValue_v1[\"Value\"] is not a []byte") + return + } + encoded := base64.StdEncoding.EncodeToString(byteArray) + a.currentPlatformElementValue = encoded + + if label, err := a.GetElementLabel(); err == nil && label != "" { + if a.labelTraverseCount == nil { + a.labelTraverseCount = make(map[string]int) + } + current := a.labelTraverseCount[label] + if direction == DirectionPrevious { + if current > 1 { + current = current - 1 + } else { + current = 1 + } + } else { + current = current + 1 + } + a.labelTraverseCount[label] = current + a.currentFocusedLabel = label + log.Infof("label '%s' traverse count=%d", label, current) + } + + a.ProbeDefaultGeometryAttributes() + + // issues, err := a.RunAudit() + // if err != nil { + // panic(err) + // } + // for _, it := range issues { + // fmt.Printf("type=%s rect=%v font=%v ml=%v fg=%v bg=%v extra=%v\n", + // it.IssueClassificationLabel, it.ElementRect, it.FontSize, it.MLGeneratedDescription, + // it.ForegroundColor, it.BackgroundColor, it.LongDescriptionExtraInfo) + // } +} + +func (a *ControlInterface) Navigate(direction int32) { + a.Move(direction) } diff --git a/ios/accessibility/utils.go b/ios/accessibility/utils.go index 15c09f65..ad34985c 100644 --- a/ios/accessibility/utils.go +++ b/ios/accessibility/utils.go @@ -3,6 +3,9 @@ package accessibility import "fmt" func convertToStringList(payload []interface{}) ([]string, error) { + if payload == nil { + return nil, fmt.Errorf("payload is nil") + } if len(payload) != 1 { return nil, fmt.Errorf("invalid payload length %d", len(payload)) } diff --git a/ios/dtx_codec/channel.go b/ios/dtx_codec/channel.go index bba99765..4a20b235 100644 --- a/ios/dtx_codec/channel.go +++ b/ios/dtx_codec/channel.go @@ -46,6 +46,18 @@ func (d *Channel) ReceiveMethodCall(selector string) Message { return <-channel } +func (d *Channel) ReceiveMethodCallWithTimeout(selector string, timeout time.Duration) (Message, error) { + d.mutex.Lock() + channel := d.registeredMethods[selector] + d.mutex.Unlock() + select { + case msg := <-channel: + return msg, nil + case <-time.After(timeout): + return Message{}, fmt.Errorf("timeout waiting for selector %s", selector) + } +} + // MethodCall is the standard DTX style remote method invocation pattern. The ObjectiveC Selector goes as a NSKeyedArchiver.archived NSString into the // DTXMessage payload, and the arguments are separately NSKeyArchiver.archived and put into the Auxiliary DTXPrimitiveDictionary. It returns the response message and an error. func (d *Channel) MethodCall(selector string, args ...interface{}) (Message, error) { @@ -80,9 +92,14 @@ func (d *Channel) methodCallWithReply(selector string, auxiliary PrimitiveDictio func (d *Channel) MethodCallAsync(selector string, args ...interface{}) error { payload, _ := nskeyedarchiver.ArchiveBin(selector) auxiliary := NewPrimitiveDictionary() + log.Infof("Sending async method call: selector=%s args=%#v", selector, args) for _, arg := range args { auxiliary.AddNsKeyedArchivedObject(arg) } + log.WithFields(log.Fields{"channel_id": d.channelName, "methodselector": selector}).Trace("Sending async method call") + defer log.WithFields(log.Fields{"channel_id": d.channelName, "methodselector": selector}).Trace("Async method call sent") + + log.Infof("Sending async method call: payload=%v auxiliary=%v", payload, auxiliary) err := d.Send(false, Methodinvocation, payload, auxiliary) if err != nil { log.WithFields(log.Fields{"channel_id": d.channelName, "error": err, "methodselector": selector}).Info("failed starting invoking method") @@ -98,6 +115,8 @@ func (d *Channel) Send(expectsReply bool, messageType MessageType, payloadBytes d.messageIdentifier++ d.mutex.Unlock() + log.Infof("Sending message: identifier=%d expectsReply=%v messageType=%v payloadBytes=%v auxiliary=%v", identifier, expectsReply, messageType, payloadBytes, auxiliary) + bytes, err := Encode(identifier, 0, d.channelCode, expectsReply, messageType, payloadBytes, auxiliary) if err != nil { return err diff --git a/ios/nskeyedarchiver/objectivec_classes.go b/ios/nskeyedarchiver/objectivec_classes.go index d29b90b1..51f7216a 100644 --- a/ios/nskeyedarchiver/objectivec_classes.go +++ b/ios/nskeyedarchiver/objectivec_classes.go @@ -68,6 +68,7 @@ func SetupEncoders() { "NSSet": archiveNSSet, "XCTTestIdentifier": archiveXCTTestIdentifier, "XCTTestIdentifierSet": archiveXCTTestIdentifierSet, + "NSValuePoint": archiveNSValuePoint, } } } @@ -468,6 +469,38 @@ func NewNSValue(object map[string]interface{}, objects []interface{}) interface{ return NSValue{NSRectval: rectval, NSSpecial: special} } +// NSValuePoint encodes a CGPoint as NSValue for archiving. +type NSValuePoint struct { + X float64 + Y float64 +} + +func NewNSValuePoint(x, y float64) NSValuePoint { return NSValuePoint{X: x, Y: y} } + +func archiveNSValuePoint(object interface{}, objects []interface{}) ([]interface{}, plist.UID) { + p := object.(NSValuePoint) + + // Create the NSValue dictionary object + dict := map[string]interface{}{} + dictIdx := len(objects) + objects = append(objects, dict) + + // Create the point string object as a separate entry and reference it via UID + pointStrIdx := len(objects) + objects = append(objects, fmt.Sprintf("{%g, %g}", p.X, p.Y)) + + // Append the class dictionary for NSValue and reference it from the dict + classIdx := len(objects) + objects = append(objects, buildClassDict("NSValue", "NSObject")) + dict["$class"] = plist.UID(classIdx) + + // Encode as NS.pointval (string reference) and NS.special=1 + dict["NS.pointval"] = plist.UID(pointStrIdx) + dict["NS.special"] = uint64(1) + + return objects, plist.UID(dictIdx) +} + type NSArray struct { Values []interface{} } diff --git a/performActionDTX.json b/performActionDTX.json new file mode 100644 index 00000000..47f477c7 --- /dev/null +++ b/performActionDTX.json @@ -0,0 +1,33 @@ +{ + "DtxMessage": { + "AuxiliaryContents": "[{t:binary, v:{\"ObjectType\":\"AXAuditElement_v1\",\"Value\":{\"ObjectType\":\"passthrough\",\"Value\":{\"AccessibilityIdentifier_v1\":{\"ObjectType\":\"passthrough\",\"Value\":\"axpasswordField\"},\"PlatformElementValue_v1\":{\"ObjectType\":\"passthrough\",\"Value\":\"mw4AAODknoICAAAAMAAAAAAAAAA=\"}}}}},{t:binary, v:{\"ObjectType\":\"AXAuditElementAttribute_v1\",\"Value\":{\"ObjectType\":\"passthrough\",\"Value\":{\"AttributeNameValue_v1\":{\"ObjectType\":\"passthrough\",\"Value\":\"AXAction-2010\"},\"DisplayAsTree_v1\":{\"ObjectType\":\"passthrough\",\"Value\":false},\"DisplayInlineValue_v1\":{\"ObjectType\":\"passthrough\",\"Value\":false},\"HumanReadableNameValue_v1\":{\"ObjectType\":\"passthrough\",\"Value\":\"Activate\"},\"IsInternal_v1\":{\"ObjectType\":\"passthrough\",\"Value\":false},\"PerformsActionValue_v1\":{\"ObjectType\":\"passthrough\",\"Value\":true},\"SettableValue_v1\":{\"ObjectType\":\"passthrough\",\"Value\":false},\"ValueTypeValue_v1\":{\"ObjectType\":\"passthrough\",\"Value\":1}}}}},{t:binary, v:{}},]", + "Fragments": 1, + "FragmentIndex": 0, + "MessageLength": 2224, + "Identifier": 82, + "ConversationIndex": 0, + "ChannelCode": 0, + "ExpectsReply": true, + "PayloadHeader": { + "MessageType": 2, + "AuxiliaryLength": 2030, + "TotalPayloadLength": 2208, + "Flags": 0 + }, + "Payload": [ + "deviceElement:performAction:withValue:" + ], + "AuxiliaryHeader": { + "BufferSize": 2032, + "Unknown": 0, + "AuxiliarySize": 2014, + "Unknown2": 0 + }, + "Auxiliary": {}, + "RawBytes": null + }, + "MessageType": "dtx", + "TimeReceived": "2025-08-13T13:29:14.791596+02:00", + "OffsetInDump": 98254, + "Length": 0 +} diff --git a/restapi/api/app_endpoints.go b/restapi/api/app_endpoints.go index 536df08e..2fab9c3c 100644 --- a/restapi/api/app_endpoints.go +++ b/restapi/api/app_endpoints.go @@ -105,8 +105,8 @@ func KillApp(c *gin.Context) { } for _, app := range response { - if app.CFBundleIdentifier == bundleID { - processName = app.CFBundleExecutable + if app.CFBundleIdentifier() == bundleID { + processName = app.CFBundleExecutable() break } } diff --git a/restapi/api/ax_endpoints.go b/restapi/api/ax_endpoints.go new file mode 100644 index 00000000..ed275798 --- /dev/null +++ b/restapi/api/ax_endpoints.go @@ -0,0 +1,390 @@ +package api + +import ( + "fmt" + "net/http" + "strconv" + "sync" + "time" + + "github.com/danielpaulus/go-ios/ios" + "github.com/danielpaulus/go-ios/ios/accessibility" + "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" +) + +var ( + axConn *accessibility.ControlInterface + axConnMux sync.RWMutex + isAXEnabled bool +) + +// enableAXService enables the accessibility service session for the device +// @Summary Enable accessibility service +// @Description Starts an accessibility session on the device and enables selection mode. Optionally accepts WDA host for alert detection. +// @Tags accessibility +// @Produce json +// @Param udid path string true "Device UDID" +// @Param wda_host query string false "WDA host for alert detection (e.g., http://192.168.2.196:8100)" +// @Success 200 {object} map[string]string +// @Failure 500 {object} GenericResponse +// @Router /device/{udid}/accessibility/enable [get] +func enableAXService(c *gin.Context) { + axConnMux.Lock() + defer axConnMux.Unlock() + + device := c.MustGet(IOS_KEY).(ios.DeviceEntry) + + // Get WDA host from query parameter + wdaHost := c.Query("wda_host") + log.Infof("enableAXService called with wda_host: %q", wdaHost) + + // If service is already enabled, just update WDA host if provided + if isAXEnabled && axConn != nil { + log.Infof("Service already enabled, updating WDA host to: %q", wdaHost) + if wdaHost != "" { + axConn.SetWDAHost(wdaHost) + c.JSON(http.StatusOK, map[string]string{"message": "Accessibility service already enabled, WDA host updated"}) + } else { + axConn.ClearWDAHost() + c.JSON(http.StatusOK, map[string]string{"message": "Accessibility service already enabled, WDA host cleared"}) + } + return + } + + // Create new connection with or without WDA host + var conn *accessibility.ControlInterface + var err error + if wdaHost != "" { + log.Infof("Creating connection with WDA host: %q", wdaHost) + conn, err = accessibility.NewWithWDA(device, wdaHost) + } else { + log.Infof("Creating connection without WDA host") + conn, err = accessibility.New(device) + } + + if err != nil { + c.JSON(http.StatusInternalServerError, GenericResponse{Error: err.Error()}) + return + } + + // Verify WDA host was set correctly + log.Infof("Connection created, WDA host is: %q", conn.GetWDAHost()) + + conn.SwitchToDevice() + + conn.EnableSelectionMode() + + axConn = conn + isAXEnabled = true + log.Infof("Accessibility service enabled, final WDA host: %q", axConn.GetWDAHost()) + c.JSON(http.StatusOK, map[string]string{"message": "Accessibility service enabled"}) +} + +// disableAXService disables the accessibility service session for the device +// @Summary Disable accessibility service +// @Description Turns off the accessibility session on the device +// @Tags accessibility +// @Produce json +// @Param udid path string true "Device UDID" +// @Success 200 {object} map[string]string +// @Router /device/{udid}/accessibility/disable [get] +func disableAXService(c *gin.Context) { + + if !isAXEnabled || axConn == nil { + c.JSON(http.StatusOK, map[string]string{"message": "Accessibility service already disabled"}) + return + } + + axConn.TurnOff() + + // Close the connection // Assuming there's a Close method + axConn = nil + isAXEnabled = false + c.JSON(http.StatusOK, map[string]string{"message": "Accessibility service disabled"}) +} + +// navigateToNextElement moves VoiceOver focus to the next element +// @Summary Navigate to next accessible element +// @Description Moves the selection to the next element using accessibility service +// @Tags accessibility +// @Produce json +// @Param udid path string true "Device UDID" +// @Success 200 {object} map[string]interface{} +// @Failure 400 {object} GenericResponse +// @Router /device/{udid}/accessibility/next [post] +func navigateToNextElement(c *gin.Context) { + axConnMux.RLock() + defer axConnMux.RUnlock() + + if !isAXEnabled || axConn == nil { + c.JSON(http.StatusBadRequest, GenericResponse{Error: "Accessibility service not enabled. Call /enable first"}) + return + } + + axConn.Navigate(accessibility.DirectionNext) + + c.JSON(http.StatusOK, map[string]interface{}{ + "message": "Navigated to next element", + }) +} + +// navigateToPrevElement moves VoiceOver focus to the previous element +// @Summary Navigate to previous accessible element +// @Description Moves the selection to the previous element using accessibility service +// @Tags accessibility +// @Produce json +// @Param udid path string true "Device UDID" +// @Success 200 {object} map[string]interface{} +// @Failure 400 {object} GenericResponse +// @Router /device/{udid}/accessibility/previous [post] +func navigateToPrevElement(c *gin.Context) { + axConnMux.RLock() + defer axConnMux.RUnlock() + + if !isAXEnabled || axConn == nil { + c.JSON(http.StatusBadRequest, GenericResponse{Error: "Accessibility service not enabled. Call /enable first"}) + return + } + + axConn.Navigate(accessibility.DirectionPrevious) + + c.JSON(http.StatusOK, map[string]interface{}{ + "message": "Navigated to previous element", + }) +} + +// navigateToFirstElement moves VoiceOver focus to the first element +// @Summary Navigate to first accessible element +// @Description Moves the selection to the first element using accessibility service +// @Tags accessibility +// @Produce json +// @Param udid path string true "Device UDID" +// @Success 200 {object} map[string]interface{} +// @Failure 400 {object} GenericResponse +// @Router /device/{udid}/accessibility/first [post] +func navigateToFirstElement(c *gin.Context) { + axConnMux.RLock() + defer axConnMux.RUnlock() + + if !isAXEnabled || axConn == nil { + c.JSON(http.StatusBadRequest, GenericResponse{Error: "Accessibility service not enabled. Call /enable first"}) + return + } + + axConn.Navigate(accessibility.DirectionFirst) + + c.JSON(http.StatusOK, map[string]interface{}{ + "message": "Navigated to first element", + }) +} + +// navigateToLastElement moves VoiceOver focus to the last element +// @Summary Navigate to last accessible element +// @Description Moves the selection to the last element using accessibility service +// @Tags accessibility +// @Produce json +// @Param udid path string true "Device UDID" +// @Success 200 {object} map[string]interface{} +// @Failure 400 {object} GenericResponse +// @Router /device/{udid}/accessibility/last [post] +func navigateToLastElement(c *gin.Context) { + axConnMux.RLock() + defer axConnMux.RUnlock() + + if !isAXEnabled || axConn == nil { + c.JSON(http.StatusBadRequest, GenericResponse{Error: "Accessibility service not enabled. Call /enable first"}) + return + } + + axConn.Navigate(accessibility.DirectionLast) + + c.JSON(http.StatusOK, map[string]interface{}{ + "message": "Navigated to last element", + }) +} + +// performDtxAction performs an accessibility action via the DTX channel +// @Summary Perform accessibility action via DTX +// @Description Performs the default action on the current focused element using DTX. Optionally accepts WDA host for alert detection. +// @Tags accessibility +// @Produce json +// @Param udid path string true "Device UDID" +// @Param wda_host query string false "WDA host for alert detection (e.g., http://192.168.2.196:8100)" +// @Success 200 {object} map[string]string +// @Failure 400 {object} GenericResponse +// @Failure 500 {object} GenericResponse +// @Router /device/{udid}/accessibility/perform-action [post] +func performDtxAction(c *gin.Context) { + axConnMux.Lock() + defer axConnMux.Unlock() + + if !isAXEnabled || axConn == nil { + c.JSON(http.StatusBadRequest, GenericResponse{Error: "Accessibility service not enabled"}) + return + } + + // Get WDA host from query parameter + wdaHost := c.Query("wda_host") + log.Infof("performDtxAction called with wda_host: %q", wdaHost) + + // Only set WDA host if explicitly provided + if wdaHost != "" { + axConn.SetWDAHost(wdaHost) + } + // Note: We don't clear the WDA host if not provided - we keep the existing value + + err := axConn.PerformAction("AXAction-2010") + if err != nil { + c.JSON(http.StatusInternalServerError, GenericResponse{Error: err.Error()}) + return + } + + c.JSON(http.StatusOK, map[string]string{"message": "Action performed"}) +} + +// performWDAAction performs an accessibility action via WebDriverAgent +// @Summary Perform accessibility action via WDA +// @Description Performs an action using WebDriverAgent and returns action UUID +// @Tags accessibility +// @Produce json +// @Param udid path string true "Device UDID" +// @Success 200 {object} map[string]string +// @Failure 400 {object} GenericResponse +// @Failure 500 {object} GenericResponse +// @Router /device/{udid}/accessibility/wda/perform-action [post] +func performWDAAction(c *gin.Context) { + axConnMux.RLock() + defer axConnMux.RUnlock() + + if !isAXEnabled || axConn == nil { + c.JSON(http.StatusBadRequest, GenericResponse{Error: "Accessibility service not enabled"}) + return + } + + log.Infof("performWDAAction: Current WDA host is: %q", axConn.GetWDAHost()) + uuid, err := axConn.PerformWDAAction() + if err != nil { + c.JSON(http.StatusInternalServerError, GenericResponse{Error: err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"uuid": uuid}) +} + +// getWDAHostStatus returns the current WDA host configuration +// @Summary Get WDA host status +// @Description Returns the current WDA host configuration for alert detection +// @Tags accessibility +// @Produce json +// @Param udid path string true "Device UDID" +// @Success 200 {object} map[string]string +// @Failure 400 {object} GenericResponse +// @Router /device/{udid}/accessibility/wda/status [get] +func getWDAHostStatus(c *gin.Context) { + axConnMux.RLock() + defer axConnMux.RUnlock() + + if !isAXEnabled || axConn == nil { + c.JSON(http.StatusBadRequest, GenericResponse{Error: "Accessibility service not enabled"}) + return + } + + wdaHost := axConn.GetWDAHost() + c.JSON(http.StatusOK, map[string]interface{}{ + "wda_host": wdaHost, + "enabled": wdaHost != "", + }) +} + +// setElementChangeTimeout sets the timeout for waiting for element changes +// @Summary Set element change timeout +// @Description Sets the timeout for waiting for element changes in accessibility navigation +// @Tags accessibility +// @Produce json +// @Param udid path string true "Device UDID" +// @Param timeout query int false "Timeout in seconds (default: 10)" +// @Success 200 {object} map[string]string +// @Failure 400 {object} GenericResponse +// @Router /device/{udid}/accessibility/timeout [post] +func setElementChangeTimeout(c *gin.Context) { + axConnMux.Lock() + defer axConnMux.Unlock() + + if !isAXEnabled || axConn == nil { + c.JSON(http.StatusBadRequest, GenericResponse{Error: "Accessibility service not enabled"}) + return + } + + timeoutStr := c.Query("timeout") + if timeoutStr == "" { + c.JSON(http.StatusBadRequest, GenericResponse{Error: "timeout parameter is required"}) + return + } + + timeoutSeconds, err := strconv.Atoi(timeoutStr) + if err != nil { + c.JSON(http.StatusBadRequest, GenericResponse{Error: "invalid timeout value, must be a number"}) + return + } + + if timeoutSeconds <= 0 { + c.JSON(http.StatusBadRequest, GenericResponse{Error: "timeout must be greater than 0"}) + return + } + + timeout := time.Duration(timeoutSeconds) * time.Second + axConn.SetElementChangeTimeout(timeout) + + c.JSON(http.StatusOK, map[string]string{ + "message": fmt.Sprintf("Element change timeout set to %d seconds", timeoutSeconds), + }) +} + +// runAccessibilityAudit triggers an accessibility audit and returns found issues +// @Summary Run accessibility audit +// @Description Runs an accessibility audit for all supported types (fetched internally). +// @Tags accessibility +// @Produce json +// @Param udid path string true "Device UDID" +// @Success 200 {object} RunAuditResponse +// @Failure 400 {object} GenericResponse +// @Failure 500 {object} GenericResponse +// @Router /device/{udid}/accessibility/audit/run [post] +func runAccessibilityAudit(c *gin.Context) { + axConnMux.RLock() + defer axConnMux.RUnlock() + + if !isAXEnabled || axConn == nil { + c.JSON(http.StatusBadRequest, GenericResponse{Error: "Accessibility service not enabled"}) + return + } + + issues, err := axConn.RunAudit() + if err != nil { + c.JSON(http.StatusInternalServerError, GenericResponse{Error: err.Error()}) + return + } + + c.JSON(http.StatusOK, RunAuditResponse{Issues: issues}) +} + +// RunAuditResponse is the response body for audit run +// swagger:model +// nolint: revive +// RunAuditResponse wraps the list of issues +// Each issue is returned as provided by the accessibility package. +type RunAuditResponse struct { + Issues []accessibility.AXAuditIssueV1 `json:"issues"` +} + +// Optional: Add cleanup function for graceful shutdown +func cleanupAXService() { + axConnMux.Lock() + defer axConnMux.Unlock() + + if axConn != nil { + axConn.TurnOff() + axConn = nil + isAXEnabled = false + } +} diff --git a/restapi/api/routes.go b/restapi/api/routes.go index b61287c0..e6edf6f4 100644 --- a/restapi/api/routes.go +++ b/restapi/api/routes.go @@ -13,6 +13,7 @@ func registerRoutes(router *gin.RouterGroup) { device.Use(DeviceMiddleware()) simpleDeviceRoutes(device) appRoutes(device) + axServiceRoutes(device) } func simpleDeviceRoutes(device *gin.RouterGroup) { @@ -52,3 +53,24 @@ func appRoutes(group *gin.RouterGroup) { router.POST("/install", InstallApp) router.POST("/uninstall", UninstallApp) } + +func axServiceRoutes(group *gin.RouterGroup) { + router := group.Group("/accessibility") + router.Use(LimitNumClientsUDID()) + router.GET("/enable", enableAXService) + router.GET("/disable", disableAXService) + router.POST("/next", navigateToNextElement) + router.POST("/previous", navigateToPrevElement) + router.POST("/first", navigateToFirstElement) + router.POST("/last", navigateToLastElement) + // ax service performs action + router.POST("/perform-action", performDtxAction) + // wda performs action + router.POST("/wda/perform-action", performWDAAction) + // wda status + router.GET("/wda/status", getWDAHostStatus) + // timeout configuration + router.POST("/timeout", setElementChangeTimeout) + // audits + router.POST("/audit/run", runAccessibilityAudit) +} diff --git a/restapi/go.mod b/restapi/go.mod index ddc08271..d807f030 100644 --- a/restapi/go.mod +++ b/restapi/go.mod @@ -27,13 +27,13 @@ require ( github.com/go-playground/universal-translator v0.18.0 // indirect github.com/go-playground/validator/v10 v10.10.0 // indirect github.com/goccy/go-json v0.9.7 // indirect - github.com/google/uuid v1.3.0 // indirect + github.com/google/uuid v1.3.0 github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/leodido/go-urn v1.2.1 // indirect github.com/mailru/easyjson v0.7.6 // indirect github.com/mattn/go-isatty v0.0.14 // indirect - github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.0.1 // indirect github.com/ugorji/go/codec v1.2.7 // indirect @@ -42,20 +42,33 @@ require ( golang.org/x/sys v0.21.0 // indirect golang.org/x/text v0.16.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect - google.golang.org/protobuf v1.28.0 // indirect + google.golang.org/protobuf v1.32.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect howett.net/plist v1.0.0 // indirect ) require ( github.com/cenkalti/backoff v2.2.1+incompatible // indirect + github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect + github.com/google/btree v1.1.2 // indirect + github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect github.com/grandcat/zeroconf v1.0.0 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/miekg/dns v1.1.57 // indirect + github.com/onsi/ginkgo/v2 v2.9.5 // indirect github.com/pierrec/lz4 v2.6.1+incompatible // indirect + github.com/quic-go/qtls-go1-20 v0.4.1 // indirect + github.com/quic-go/quic-go v0.40.1-0.20231203135336-87ef8ec48d55 // indirect + github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 // indirect + github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9 // indirect go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect + go.uber.org/mock v0.3.0 // indirect + golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 // indirect golang.org/x/mod v0.17.0 // indirect golang.org/x/sync v0.7.0 // indirect + golang.org/x/time v0.5.0 // indirect + golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect + gvisor.dev/gvisor v0.0.0-20240405191320-0878b34101b5 // indirect software.sslmate.com/src/go-pkcs12 v0.2.0 // indirect ) diff --git a/restapi/go.sum b/restapi/go.sum index d499bf76..b5251efa 100644 --- a/restapi/go.sum +++ b/restapi/go.sum @@ -10,6 +10,9 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdko github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -26,6 +29,8 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8= github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= @@ -44,18 +49,27 @@ github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/j github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0= github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM= github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= +github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/grandcat/zeroconf v1.0.0 h1:uHhahLBKqwWBV6WZUDAT71044vwOTL+McW0mBJvo6kE= github.com/grandcat/zeroconf v1.0.0/go.mod h1:lTKmG1zh86XyCoUeIHSA4FJMBwCJiQmGfcP2PdzytEs= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= @@ -82,11 +96,16 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= +github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= +github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= +github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U= github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= @@ -101,6 +120,10 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs= +github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k= +github.com/quic-go/quic-go v0.40.1-0.20231203135336-87ef8ec48d55 h1:I4N3ZRnkZPbDN935Tg8QDf8fRpHp3bZ0U0/L42jBgNE= +github.com/quic-go/quic-go v0.40.1-0.20231203135336-87ef8ec48d55/go.mod h1:PeN7kuVJ4xZbxSv/4OX6S1USOX8MJvydwpTx31vx60c= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= @@ -111,6 +134,8 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8= +github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= @@ -128,6 +153,8 @@ github.com/swaggo/gin-swagger v1.5.2/go.mod h1:Cbj/MlHApPOjZdf4joWFXLLgmZVPyh54G github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ= github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg= github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk= +github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9 h1:aeN+ghOV0b2VCmKKO3gqnDQ8mLbpABZgRR2FVYx4ouI= +github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9/go.mod h1:roo6cZ/uqpwKMuvPG0YmzI5+AmUiMWfjCBZpGXqbTxE= github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= @@ -135,12 +162,16 @@ github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/X github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 h1:CCriYyAfq1Br1aIYettdHZTy8mBTIPo7We18TuO/bak= go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= +go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo= +go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 h1:Di6/M8l0O2lCLc6VVRWhgCiApHV8MnQurBnFSHsQtNY= +golang.org/x/exp v0.0.0-20230725093048-515e97ebf090/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= @@ -164,6 +195,7 @@ golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -183,6 +215,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -194,9 +228,12 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= +golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= +google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -213,6 +250,8 @@ gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gvisor.dev/gvisor v0.0.0-20240405191320-0878b34101b5 h1:DOUDfNS+CFMM46k18FRF5k/0yz5NhZYMiUQxf4xglIU= +gvisor.dev/gvisor v0.0.0-20240405191320-0878b34101b5/go.mod h1:NQHVAzMwvZ+Qe3ElSiHmq9RUm1MdNHpUZ52fiEqvn+0= howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= software.sslmate.com/src/go-pkcs12 v0.2.0 h1:nlFkj7bTysH6VkC4fGphtjXRbezREPgrHuJG20hBGPE=