diff --git a/go.mod b/go.mod index aef5bd8e..b0ff8b1b 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/danielpaulus/go-ios -go 1.17 +go 1.21 require ( github.com/Masterminds/semver v1.5.0 @@ -8,19 +8,36 @@ require ( github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa github.com/google/gopacket v1.1.19 github.com/google/uuid v1.1.2 + github.com/grandcat/zeroconf v1.0.0 github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 github.com/pierrec/lz4 v2.6.1+incompatible + github.com/quic-go/quic-go v0.40.1-0.20231203135336-87ef8ec48d55 github.com/sirupsen/logrus v1.6.0 + github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 github.com/stretchr/testify v1.6.1 - golang.org/x/crypto v0.0.0-20210812204632-0ba0e8f03122 + github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9 + golang.org/x/crypto v0.15.0 + golang.org/x/net v0.18.0 howett.net/plist v0.0.0-20200419221736-3b63eb3a43b5 ) require ( + github.com/cenkalti/backoff v2.2.1+incompatible // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/frankban/quicktest v1.14.6 // indirect + github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect + github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect github.com/konsorten/go-windows-terminal-sequences v1.0.3 // indirect + github.com/miekg/dns v1.1.57 // indirect + github.com/onsi/ginkgo/v2 v2.9.5 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/quic-go/qtls-go1-20 v0.4.1 // indirect github.com/stretchr/objx v0.1.0 // indirect - golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect - gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect + go.uber.org/mock v0.3.0 // indirect + golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect + golang.org/x/mod v0.14.0 // indirect + golang.org/x/sys v0.14.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/tools v0.15.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 4304f522..a2f763ce 100644 --- a/go.sum +++ b/go.sum @@ -1,64 +1,124 @@ github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +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/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= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +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/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa h1:RDBNVkRviHZtvDvId8XSGPu3rmpmSe+wKRcEWNgsfWU= github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA= +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-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/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= +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.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc= github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg= +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/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/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM= github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +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.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +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 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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= +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-20210812204632-0ba0e8f03122 h1:AOT7vJYHE32m61R8d1WlcqhOO1AocesDsKpcMq+UOaA= -golang.org/x/crypto v0.0.0-20210812204632-0ba0e8f03122/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= +golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= +golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o= +golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= +golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.5.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-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +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.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8= +golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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= howett.net/plist v0.0.0-20200419221736-3b63eb3a43b5 h1:AQkaJpH+/FmqRjmXZPELom5zIERYZfwTjnHpfoVMQEc= howett.net/plist v0.0.0-20200419221736-3b63eb3a43b5/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= diff --git a/ios/appservice/appservice.go b/ios/appservice/appservice.go new file mode 100644 index 00000000..f1be9e25 --- /dev/null +++ b/ios/appservice/appservice.go @@ -0,0 +1,256 @@ +// Package appservice provides functions to Launch and Kill apps on an iOS devices for iOS17+. +package appservice + +import ( + "bytes" + "errors" + "fmt" + "io" + "net" + "path" + "syscall" + + "github.com/danielpaulus/go-ios/ios" + "github.com/danielpaulus/go-ios/ios/coredevice" + "github.com/danielpaulus/go-ios/ios/xpc" + "github.com/google/uuid" + "howett.net/plist" +) + +// Connection represents a connection to the appservice on an iOS device for iOS17+. +// It is used to launch and kill apps and to list processes. +type Connection struct { + conn *xpc.Connection + deviceId string +} + +const ( + // RebootFull is the style for a full reboot of the device. + RebootFull = "full" + // RebootUserspace is the style for a reboot of the userspace of the device. + RebootUserspace = "userspace" +) + +// New creates a new connection to the appservice on the device for iOS17+. +// It returns an error if the connection could not be established. +func New(deviceEntry ios.DeviceEntry) (*Connection, error) { + xpcConn, err := ios.ConnectToXpcServiceTunnelIface(deviceEntry, "com.apple.coredevice.appservice") + if err != nil { + return nil, fmt.Errorf("new: %w", err) + } + + return &Connection{conn: xpcConn, deviceId: uuid.New().String()}, nil +} + +// AppLaunch represents the result of launching an app on the device for iOS17+. +// It contains the PID of the launched app. +type AppLaunch struct { + Pid int +} + +// Process represents a process running on the device for iOS17+. +// It contains the PID and the path of the process. +type Process struct { + Pid int + Path string +} + +// LaunchApp launches an app on the device with the given bundleId and arguments for iOS17+. +func (c *Connection) LaunchApp(bundleId string, args []interface{}, env map[string]interface{}, options map[string]interface{}, terminateExisting bool) (AppLaunch, error) { + msg := buildAppLaunchPayload(c.deviceId, bundleId, args, env, options, terminateExisting) + err := c.conn.Send(msg, xpc.HeartbeatRequestFlag) + if err != nil { + return AppLaunch{}, fmt.Errorf("LaunchApp: failed to send launch-app request: %w", err) + } + m, err := c.conn.ReceiveOnServerClientStream() + if err != nil { + return AppLaunch{}, fmt.Errorf("launchApp2: %w", err) + } + pid, err := pidFromResponse(m) + if err != nil { + return AppLaunch{}, fmt.Errorf("launchApp3: %w", err) + } + return AppLaunch{Pid: int(pid)}, nil +} + +// Close closes the connection to the appservice +func (c *Connection) Close() error { + return c.conn.Close() +} + +// ListProcesses returns a list of processes with their PID and executable path running on the device for iOS17+. +func (c *Connection) ListProcesses() ([]Process, error) { + req := buildListProcessesPayload(c.deviceId) + err := c.conn.Send(req, xpc.HeartbeatRequestFlag) + if err != nil { + return nil, fmt.Errorf("listProcesses send: %w", err) + } + res, err := c.conn.ReceiveOnServerClientStream() + if err != nil { + return nil, fmt.Errorf("listProcesses receive: %w", err) + } + + output, ok := res["CoreDevice.output"].(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("listProcesses output") + } + tokens, ok := output["processTokens"].([]interface{}) + if !ok { + return nil, fmt.Errorf("listProcesses processTokens") + } + processes := make([]Process, len(tokens)) + tokensTyped, err := ios.GenericSliceToType[map[string]interface{}](tokens) + if err != nil { + return nil, fmt.Errorf("listProcesses: %w", err) + } + for i, processMap := range tokensTyped { + var p Process + pid, ok := processMap["processIdentifier"].(int64) + if !ok { + return nil, fmt.Errorf("listProcesses processIdentifier") + } + processPathMap, ok := processMap["executableURL"].(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("listProcesses executableURL") + } + processPath, ok := processPathMap["relative"].(string) + if !ok { + return nil, fmt.Errorf("listProcesses relative") + } + + p.Pid = int(pid) + p.Path = processPath + + processes[i] = p + } + + return processes, nil +} + +// KillProcess kills the process with the given PID for iOS17+. +func (c *Connection) KillProcess(pid int) error { + req := buildSendSignalPayload(c.deviceId, pid, syscall.SIGKILL) + err := c.conn.Send(req, xpc.HeartbeatRequestFlag) + if err != nil { + return fmt.Errorf("killProcess send: %w", err) + } + m, err := c.conn.ReceiveOnServerClientStream() + if err != nil { + return fmt.Errorf("killProcess receive: %w", err) + } + err = getError(m) + if err != nil { + return fmt.Errorf("killProcess: %w", err) + } + return nil +} + +// Reboot performs a full reboot of the device for iOS17+. +// Just calls RebootWithStyle with RebootFull. +func (c *Connection) Reboot() error { + return c.RebootWithStyle(RebootFull) +} + +// RebootWithStyle performs a reboot of the device with the given style for iOS17+. For style use RebootFull or RebootUserSpace. +func (c *Connection) RebootWithStyle(style string) error { + err := c.conn.Send(buildRebootPayload(c.deviceId, style)) + if err != nil { + return fmt.Errorf("reboot send: %w", err) + } + m, err := c.conn.ReceiveOnServerClientStream() + if err != nil { + if errors.Is(err, io.EOF) { + return nil + } + var opErr *net.OpError + if errors.As(err, &opErr) && opErr.Timeout() { + return nil + } + return fmt.Errorf("reboot receive: %w", err) + } + err = getError(m) + if err != nil { + return fmt.Errorf("reboot: %w", err) + } + return nil +} + +// ExecutableName returns the executable name for a process by removing the path. +func (p Process) ExecutableName() string { + _, file := path.Split(p.Path) + return file +} + +func buildAppLaunchPayload(deviceId string, bundleId string, args []interface{}, env map[string]interface{}, options map[string]interface{}, terminateExisting bool) map[string]interface{} { + platformSpecificOptions := bytes.NewBuffer(nil) + plistEncoder := plist.NewBinaryEncoder(platformSpecificOptions) + err := plistEncoder.Encode(options) + if err != nil { + panic(err) + } + + return coredevice.BuildRequest(deviceId, "com.apple.coredevice.feature.launchapplication", map[string]interface{}{ + "applicationSpecifier": map[string]interface{}{ + "bundleIdentifier": map[string]interface{}{ + "_0": bundleId, + }, + }, + "options": map[string]interface{}{ + "arguments": args, + "environmentVariables": env, + "platformSpecificOptions": platformSpecificOptions.Bytes(), + "standardIOUsesPseudoterminals": true, + "startStopped": false, + "terminateExisting": terminateExisting, + "user": map[string]interface{}{ + "active": true, + }, + "workingDirectory": nil, + }, + "standardIOIdentifiers": map[string]interface{}{}, + }) +} + +func buildListProcessesPayload(deviceId string) map[string]interface{} { + return coredevice.BuildRequest(deviceId, "com.apple.coredevice.feature.listprocesses", nil) +} + +func buildRebootPayload(deviceId string, style string) map[string]interface{} { + return coredevice.BuildRequest(deviceId, "com.apple.coredevice.feature.rebootdevice", map[string]interface{}{ + "rebootStyle": map[string]interface{}{ + style: map[string]interface{}{}, + }, + }) +} + +func buildSendSignalPayload(deviceId string, pid int, signal syscall.Signal) map[string]interface{} { + return coredevice.BuildRequest(deviceId, "com.apple.coredevice.feature.sendsignaltoprocess", map[string]interface{}{ + "process": map[string]interface{}{ + "processIdentifier": int64(pid), + }, + "signal": int64(signal), + }) +} + +func pidFromResponse(response map[string]interface{}) (int64, error) { + output, ok := response["CoreDevice.output"].(map[string]interface{}) + if !ok { + return 0, fmt.Errorf("pidFromResponse: could not get pid from response") + } + processToken, ok := output["processToken"].(map[string]interface{}) + if !ok { + return 0, fmt.Errorf("pidFromResponse: could not get processToken from response") + } + pid, ok := processToken["processIdentifier"].(int64) + if !ok { + return 0, fmt.Errorf("pidFromResponse: could not get pid from processToken") + } + return pid, nil +} + +func getError(response map[string]interface{}) error { + if e, ok := response["CoreDevice.error"].(map[string]interface{}); ok { + return fmt.Errorf("device returned error: %+v", e) + } + return nil +} diff --git a/ios/appservice/appservice_integration_test.go b/ios/appservice/appservice_integration_test.go new file mode 100644 index 00000000..3ec898b9 --- /dev/null +++ b/ios/appservice/appservice_integration_test.go @@ -0,0 +1,82 @@ +//go:build !fast + +package appservice_test + +import ( + "github.com/danielpaulus/go-ios/ios" + "github.com/danielpaulus/go-ios/ios/appservice" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "os" + "slices" + "testing" +) + +func TestLaunchAndKillApps(t *testing.T) { + rsdPath := os.Getenv("GO_IOS_RSD") + if len(rsdPath) == 0 { + t.Skipf("GO_IOS_RSD missing") + } + address := os.Getenv("GO_IOS_ADDRESS") + if len(address) == 0 { + t.Skipf("GO_IOS_ADDRESS missing") + } + + f, err := os.Open(rsdPath) + require.NoError(t, err) + defer f.Close() + rsd, err := ios.NewRsdPortProvider(f) + require.NoError(t, err) + + device, err := ios.GetDeviceWithAddress("", address, rsd) + require.NoError(t, err) + version, err := ios.GetProductVersion(device) + require.NoError(t, err) + if version.Major() < 17 { + t.Skipf("Device has version %s. Skipping test on devices lower than iOS 17", version.String()) + } + + t.Run("launch and kill app", func(t *testing.T) { + testLaunchAndKillApp(t, device) + }) + t.Run("kill invalid pid", func(t *testing.T) { + testKillInvalidPidReturnsError(t, device) + }) +} + +func testLaunchAndKillApp(t *testing.T, device ios.DeviceEntry) { + as, err := appservice.New(device) + require.NoError(t, err) + defer as.Close() + + _, err = as.LaunchApp("com.apple.mobilesafari", nil, nil, nil, true) + require.NoError(t, err) + + processes, err := as.ListProcesses() + require.NoError(t, err) + + idx := slices.IndexFunc(processes, func(e appservice.Process) bool { + return e.ExecutableName() == "MobileSafari" + }) + assert.NotEqual(t, -1, idx) + + process := processes[idx] + + err = as.KillProcess(process.Pid) + assert.NoError(t, err) +} + +func testKillInvalidPidReturnsError(t *testing.T, device ios.DeviceEntry) { + as, err := appservice.New(device) + require.NoError(t, err) + defer as.Close() + + launched, err := as.LaunchApp("com.apple.mobilesafari", nil, nil, nil, true) + require.NoError(t, err) + + err = as.KillProcess(launched.Pid) + require.NoError(t, err) + + err = as.KillProcess(launched.Pid) + assert.Error(t, err) +} diff --git a/ios/connect.go b/ios/connect.go index f6cffe0a..784047be 100755 --- a/ios/connect.go +++ b/ios/connect.go @@ -2,6 +2,12 @@ package ios import ( "fmt" + "net" + "time" + + "github.com/danielpaulus/go-ios/ios/http" + + "github.com/danielpaulus/go-ios/ios/xpc" ) type connectMessage struct { @@ -77,7 +83,6 @@ func (muxConn *UsbMuxConnection) ConnectLockdown(deviceID int) (*LockDownConnect return nil, fmt.Errorf("Failed connecting to Lockdown with error code:%d", response.Number) } -// ConnectToService connects to a service on the phone and returns the ready to use DeviceConnectionInterface func ConnectToService(device DeviceEntry, serviceName string) (DeviceConnectionInterface, error) { startServiceResponse, err := StartService(device, serviceName) if err != nil { @@ -99,6 +104,113 @@ func ConnectToService(device DeviceEntry, serviceName string) (DeviceConnectionI return muxConn.ReleaseDeviceConnection(), nil } +// ConnectToServiceTunnelIface connects to a service on an iOS17+ device using a XPC over HTTP2 connection +// It returns a new xpc.Connection +func ConnectToXpcServiceTunnelIface(device DeviceEntry, serviceName string) (*xpc.Connection, error) { + port := device.Rsd.GetPort(serviceName) + + h, err := ConnectToHttp2(device, port) + if err != nil { + return nil, fmt.Errorf("ConnectToXpcServiceTunnelIface: failed to connect to http2: %w", err) + } + return CreateXpcConnection(h) +} + +func ConnectToServiceTunnelIface(device DeviceEntry, serviceName string) (DeviceConnectionInterface, error) { + port := device.Rsd.GetPort(serviceName) + + conn, err := connectToTunnel(device, port) + if err != nil { + return nil, fmt.Errorf("ConnectToServiceTunnelIface: failed to connect to tunnel: %w", err) + } + + return NewDeviceConnectionWithConn(conn), nil +} + +func ConnectToHttp2(device DeviceEntry, port int) (*http.HttpConnection, error) { + addr, err := net.ResolveTCPAddr("tcp6", fmt.Sprintf("[%s]:%d", device.Address, port)) + if err != nil { + return nil, fmt.Errorf("ConnectToHttp2: failed to resolve address: %w", err) + } + + conn, err := net.DialTCP("tcp", nil, addr) + if err != nil { + return nil, fmt.Errorf("ConnectToHttp2: failed to dial: %w", err) + } + + err = conn.SetKeepAlive(true) + if err != nil { + return nil, fmt.Errorf("ConnectToHttp2: failed to set keepalive: %w", err) + } + err = conn.SetKeepAlivePeriod(1 * time.Second) + if err != nil { + return nil, fmt.Errorf("ConnectToHttp2: failed to set keepalive period: %w", err) + } + return http.NewHttpConnection(conn) +} + +func connectToTunnel(device DeviceEntry, port int) (*net.TCPConn, error) { + addr, err := net.ResolveTCPAddr("tcp6", fmt.Sprintf("[%s]:%d", device.Address, port)) + if err != nil { + return nil, fmt.Errorf("connectToTunnel: failed to resolve address: %w", err) + } + + conn, err := net.DialTCP("tcp", nil, addr) + if err != nil { + return nil, fmt.Errorf("connectToTunnel: failed to dial: %w", err) + } + + err = conn.SetKeepAlive(true) + if err != nil { + return nil, fmt.Errorf("connectToTunnel: failed to set keepalive: %w", err) + } + err = conn.SetKeepAlivePeriod(1 * time.Second) + if err != nil { + return nil, fmt.Errorf("connectToTunnel: failed to set keepalive period: %w", err) + } + + return conn, nil +} + +func ConnectToHttp2WithAddr(a string, port int) (*http.HttpConnection, error) { + addr, err := net.ResolveTCPAddr("tcp6", fmt.Sprintf("[%s]:%d", a, port)) + if err != nil { + return nil, fmt.Errorf("ConnectToHttp2WithAddr: failed to resolve address: %w", err) + } + + conn, err := net.DialTCP("tcp", nil, addr) + if err != nil { + return nil, fmt.Errorf("ConnectToHttp2WithAddr: failed to dial: %w", err) + } + + err = conn.SetKeepAlive(true) + if err != nil { + return nil, fmt.Errorf("ConnectToHttp2WithAddr: failed to set keepalive: %w", err) + } + err = conn.SetKeepAlivePeriod(1 * time.Second) + if err != nil { + return nil, fmt.Errorf("ConnectToHttp2WithAddr: failed to set keepalive period: %w", err) + } + return http.NewHttpConnection(conn) +} + +func CreateXpcConnection(h *http.HttpConnection) (*xpc.Connection, error) { + err := initializeXpcConnection(h) + if err != nil { + return nil, fmt.Errorf("CreateXpcConnection: failed to initialize xpc connection: %w", err) + } + + clientServerChannel := http.NewStreamReadWriter(h, http.ClientServer) + serverClientChannel := http.NewStreamReadWriter(h, http.ServerClient) + + xpcConn, err := xpc.New(clientServerChannel, serverClientChannel, h) + if err != nil { + return nil, fmt.Errorf("CreateXpcConnection: failed to create xpc connection: %w", err) + } + + return xpcConn, nil +} + // connectWithStartServiceResponse issues a Connect Message to UsbMuxd for the given deviceID on the given port // enabling the newCodec for it. It also enables SSL on the new service connection if requested by StartServiceResponse. // It returns an error containing the UsbMux error code should the connect fail. @@ -145,3 +257,52 @@ func ConnectLockdownWithSession(device DeviceEntry) (*LockDownConnection, error) } return lockdownConnection, nil } + +func initializeXpcConnection(h *http.HttpConnection) error { + csWriter := http.NewStreamReadWriter(h, http.ClientServer) + ssWriter := http.NewStreamReadWriter(h, http.ServerClient) + + err := xpc.EncodeMessage(csWriter, xpc.Message{ + Flags: xpc.AlwaysSetFlag, + Body: map[string]interface{}{}, + Id: 0, + }) + if err != nil { + return fmt.Errorf("initializeXpcConnection: failed to encode message: %w", err) + } + + _, err = xpc.DecodeMessage(csWriter) // TODO : figure out if need to act on this frame + if err != nil { + return fmt.Errorf("initializeXpcConnection: failed to decode message: %w", err) + } + + err = xpc.EncodeMessage(ssWriter, xpc.Message{ + Flags: xpc.InitHandshakeFlag | xpc.AlwaysSetFlag, + Body: nil, + Id: 0, + }) + if err != nil { + return fmt.Errorf("initializeXpcConnection: failed to encode message 2: %w", err) + } + + _, err = xpc.DecodeMessage(ssWriter) // TODO : figure out if need to act on this frame + if err != nil { + return fmt.Errorf("initializeXpcConnection: failed to decode message 2: %w", err) + } + + err = xpc.EncodeMessage(csWriter, xpc.Message{ + Flags: 0x201, // alwaysSetFlag | 0x200 + Body: nil, + Id: 0, + }) + if err != nil { + return fmt.Errorf("initializeXpcConnection: failed to encode message 3: %w", err) + } + + _, err = xpc.DecodeMessage(csWriter) // TODO : figure out if need to act on this frame + if err != nil { + return fmt.Errorf("initializeXpcConnection: failed to decode message 3: %w", err) + } + + return nil +} diff --git a/ios/coredevice/coredevice.go b/ios/coredevice/coredevice.go new file mode 100644 index 00000000..fbec4482 --- /dev/null +++ b/ios/coredevice/coredevice.go @@ -0,0 +1,20 @@ +package coredevice + +import "github.com/google/uuid" + +func BuildRequest(deviceId, feature string, input map[string]interface{}) map[string]interface{} { + u := uuid.New() + return map[string]interface{}{ + "CoreDevice.CoreDeviceDDIProtocolVersion": int64(0), + "CoreDevice.action": map[string]interface{}{}, + "CoreDevice.coreDeviceVersion": map[string]interface{}{ + "components": []interface{}{uint64(0x15c), uint64(0x1), uint64(0x0), uint64(0x0), uint64(0x0)}, + "originalComponentsCount": int64(2), + "stringValue": "348.1", + }, + "CoreDevice.deviceIdentifier": deviceId, + "CoreDevice.featureIdentifier": feature, + "CoreDevice.input": input, + "CoreDevice.invocationIdentifier": u.String(), + } +} diff --git a/ios/discover.go b/ios/discover.go new file mode 100644 index 00000000..db39e48c --- /dev/null +++ b/ios/discover.go @@ -0,0 +1,71 @@ +package ios + +import ( + "context" + "fmt" + "github.com/grandcat/zeroconf" + log "github.com/sirupsen/logrus" + "net" +) + +// FindDeviceInterfaceAddress tries to find the address of the device by browsing through all network interfaces. +// It uses mDNS to discover the "_remoted._tcp" service on the local. domain. Then tries to connect to the RemoteServiceDiscovery +// and checks if the udid of the device matches the udid of the device we are looking for. +func FindDeviceInterfaceAddress(ctx context.Context, device DeviceEntry) (string, error) { + ifaces, err := net.Interfaces() + if err != nil { + return "", fmt.Errorf("FindDeviceInterfaceAddress: failed to get network interfaces: %w", err) + } + + result := make(chan string) + defer close(result) + + for _, iface := range ifaces { + resolver, err := zeroconf.NewResolver(zeroconf.SelectIfaces([]net.Interface{iface}), zeroconf.SelectIPTraffic(zeroconf.IPv6)) + if err != nil { + log.WithField("interface", iface.Name). + WithField("err", err). + Debug("failed to initialize resolver") + continue + } + entries := make(chan *zeroconf.ServiceEntry) + resolver.Browse(ctx, "_remoted._tcp", "local.", entries) + go checkEntry(ctx, device, iface.Name, entries, result) + + } + + select { + case <-ctx.Done(): + return "", ctx.Err() + case r := <-result: + log.WithField("udid", device.Properties.SerialNumber).WithField("address", r).Debug("found device address") + return r, nil + } +} + +// checkEntry connects to all remote service discoveries and tests which one belongs to this device' udid. +func checkEntry(ctx context.Context, device DeviceEntry, interfaceName string, entries chan *zeroconf.ServiceEntry, result chan<- string) { + for { + select { + case <-ctx.Done(): + return + case entry := <-entries: + for _, ip6 := range entry.AddrIPv6 { + log.WithField("adrr", ip6).WithField("ifce", interfaceName).Info("query addr") + addr := fmt.Sprintf("%s%%%s", ip6.String(), interfaceName) + s, err := NewWithAddr(addr) + if err != nil { + continue + } + defer s.Close() + h, err := s.Handshake() + if err != nil { + continue + } + if device.Properties.SerialNumber == h.Udid { + result <- addr + } + } + } + } +} diff --git a/ios/http/http.go b/ios/http/http.go new file mode 100644 index 00000000..8b33770b --- /dev/null +++ b/ios/http/http.go @@ -0,0 +1,197 @@ +package http + +import ( + "bytes" + "fmt" + log "github.com/sirupsen/logrus" + "golang.org/x/net/http2" + "io" + "sync/atomic" +) + +type StreamId uint32 + +const ( + InitStream = StreamId(0) + ClientServer = StreamId(1) + ServerClient = StreamId(3) +) + +// HttpConnection is a wrapper around a http2.Framer that provides a simple interface to read and write http2 streams for iOS17+. +type HttpConnection struct { + framer *http2.Framer + clientServerStream *bytes.Buffer + serverClientStream *bytes.Buffer + closer io.Closer + csIsOpen *atomic.Bool + scIsOpen *atomic.Bool +} + +func (r *HttpConnection) Close() error { + return r.closer.Close() +} + +func NewHttpConnection(rw io.ReadWriteCloser) (*HttpConnection, error) { + framer := http2.NewFramer(rw, rw) + + _, err := rw.Write([]byte("PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n")) + if err != nil { + return nil, fmt.Errorf("NewHttpConnection: could not write PRI. %w", err) + } + + err = framer.WriteSettings( + http2.Setting{ID: http2.SettingMaxConcurrentStreams, Val: 100}, + http2.Setting{ID: http2.SettingInitialWindowSize, Val: 1048576}, + ) + if err != nil { + return nil, fmt.Errorf("NewHttpConnection: could not write settings. %w", err) + } + + err = framer.WriteWindowUpdate(uint32(InitStream), 983041) + if err != nil { + return nil, fmt.Errorf("NewHttpConnection: could not write window update. %w", err) + } + // + frame, err := framer.ReadFrame() + if err != nil { + return nil, fmt.Errorf("NewHttpConnection: could not read frame. %w", err) + } + if frame.Header().Type == http2.FrameSettings { + settings := frame.(*http2.SettingsFrame) + v, ok := settings.Value(http2.SettingInitialWindowSize) + if ok { + framer.SetMaxReadFrameSize(v) + } + err := framer.WriteSettingsAck() + if err != nil { + return nil, fmt.Errorf("NewHttpConnection: could not write settings ack. %w", err) + } + } else { + log.WithField("frame", frame.Header().String()). + Warn("expected setttings frame") + } + + return &HttpConnection{ + framer: framer, + clientServerStream: bytes.NewBuffer(nil), + serverClientStream: bytes.NewBuffer(nil), + closer: rw, + csIsOpen: &atomic.Bool{}, + scIsOpen: &atomic.Bool{}, + }, nil +} + +func (r *HttpConnection) ReadClientServerStream(p []byte) (int, error) { + for r.clientServerStream.Len() < len(p) { + err := r.readDataFrame() + if err != nil { + return 0, fmt.Errorf("ReadClientServerStream: %w", err) + } + } + return r.clientServerStream.Read(p) +} + +func (r *HttpConnection) WriteClientServerStream(p []byte) (int, error) { + return r.write(p, uint32(ClientServer), r.csIsOpen) +} + +func (r *HttpConnection) WriteServerClientStream(p []byte) (int, error) { + return r.write(p, uint32(ServerClient), r.scIsOpen) +} + +func (r *HttpConnection) write(p []byte, stream uint32, isOpen *atomic.Bool) (int, error) { + if isOpen.CompareAndSwap(false, true) { + err := r.framer.WriteHeaders(http2.HeadersFrameParam{ + StreamID: stream, + EndHeaders: true, + }) + if err != nil { + return 0, fmt.Errorf("write: could not send headers. %w", err) + } + } + return r.Write(p, stream) +} + +func (r *HttpConnection) Write(p []byte, streamId uint32) (int, error) { + err := r.framer.WriteData(streamId, false, p) + if err != nil { + return 0, fmt.Errorf("Write: could not write data. %w", err) + } + return len(p), nil +} + +func (r *HttpConnection) readDataFrame() error { + for { + f, err := r.framer.ReadFrame() + if err != nil { + return fmt.Errorf("readDataFrame: could not read frame. %w", err) + } + switch f.Header().Type { + case http2.FrameData: + d := f.(*http2.DataFrame) + switch d.StreamID { + case 1: + r.clientServerStream.Write(d.Data()) + case 3: + r.serverClientStream.Write(d.Data()) + default: + return fmt.Errorf("readDataFrame: unknown stream id %d", d.StreamID) + } + return nil + case http2.FrameGoAway: + return fmt.Errorf("received GOAWAY") + case http2.FrameSettings: + s := f.(*http2.SettingsFrame) + if s.Flags&http2.FlagSettingsAck != http2.FlagSettingsAck { + err := r.framer.WriteSettingsAck() + if err != nil { + return fmt.Errorf("readDataFrame: could not write settings ack. %w", err) + } + } + default: + break + } + } +} + +func (r *HttpConnection) ReadServerClientStream(p []byte) (int, error) { + for r.serverClientStream.Len() < len(p) { + err := r.readDataFrame() + if err != nil { + return 0, err + } + } + return r.serverClientStream.Read(p) +} + +type HttpStreamReadWriter struct { + h *HttpConnection + streamId uint32 +} + +func NewStreamReadWriter(h *HttpConnection, streamId StreamId) HttpStreamReadWriter { + return HttpStreamReadWriter{ + h: h, + streamId: uint32(streamId), + } +} + +func (h HttpStreamReadWriter) Read(p []byte) (n int, err error) { + if h.streamId == 1 { + return h.h.ReadClientServerStream(p) + } + if h.streamId == 3 { + return h.h.ReadServerClientStream(p) + } + return 0, fmt.Errorf("Read: unknown stream id %d", h.streamId) +} + +func (h HttpStreamReadWriter) Write(p []byte) (n int, err error) { + if h.streamId == 1 { + return h.h.WriteClientServerStream(p) + } + if h.streamId == 3 { + return h.h.WriteServerClientStream(p) + } + return 0, fmt.Errorf("Write: unknown stream id %d", h.streamId) +} diff --git a/ios/listdevices.go b/ios/listdevices.go index 2505eeda..acf6fdb5 100755 --- a/ios/listdevices.go +++ b/ios/listdevices.go @@ -56,6 +56,8 @@ type DeviceEntry struct { DeviceID int MessageType string Properties DeviceProperties + Address string + Rsd RsdPortProvider } // DeviceProperties contains important device related info like the udid which is named SerialNumber diff --git a/ios/rsd.go b/ios/rsd.go new file mode 100644 index 00000000..c9fddd43 --- /dev/null +++ b/ios/rsd.go @@ -0,0 +1,193 @@ +package ios + +import ( + "encoding/json" + "fmt" + "io" + "strconv" + + "github.com/danielpaulus/go-ios/ios/xpc" + log "github.com/sirupsen/logrus" +) + +// RsdPortProvider is an interface to get a port for a service, or a service for a port from the Remote Service Discovery on the device. +// Used in iOS17+ +type RsdPortProvider interface { + GetPort(service string) int + GetService(p int) string +} + +type RsdPortProviderJson map[string]service + +type service struct { + Port string +} + +func NewRsdPortProvider(input io.Reader) (RsdPortProviderJson, error) { + decoder := json.NewDecoder(input) + parse := struct { + Services map[string]service + }{} + + err := decoder.Decode(&parse) + if err != nil { + return nil, fmt.Errorf("NewRsdPortProvider: failed to parse rsd response: %w", err) + } + + return parse.Services, nil +} + +func (r RsdPortProviderJson) GetPort(service string) int { + p := r[service].Port + if p == "" { + shim := fmt.Sprintf("%s.shim.remote", service) + if r[shim].Port != "" { + log.Debugf("returning port of '%s'-shim", service) + return r.GetPort(shim) + } + } + port, err := strconv.ParseInt(p, 10, 64) + if err != nil { + return 0 + } + return int(port) +} + +func (r RsdPortProviderJson) GetService(p int) string { + for name, s := range r { + port, err := strconv.ParseInt(s.Port, 10, 64) + if err != nil { + log.Errorf("GetService: failed to parse port: %v", err) + return "" + } + if port == int64(p) { + return name + } + } + return "" +} + +func RsdCheckin(rw io.ReadWriter) error { + req := map[string]interface{}{ + "Label": "go-ios", + "ProtocolVersion": "2", + "Request": "RSDCheckin", + } + codec := NewPlistCodec() + b, err := codec.Encode(req) + if err != nil { + return err + } + _, err = rw.Write(b) + if err != nil { + return err + } + res, err := codec.Decode(rw) + if err != nil { + return err + } + log.Debugf("got rsd checkin response: %v", res) + return nil +} + +const port = 58783 + +type RsdService struct { + xpc *xpc.Connection + c io.Closer +} + +func (s RsdService) Close() error { + return s.c.Close() +} + +type RsdServiceEntry struct { + Port uint32 +} + +// RsdHandshakeResponse is the response to the RSDCheckin request and contains the UDID +// and the services available on the device. +type RsdHandshakeResponse struct { + Udid string + Services map[string]RsdServiceEntry +} + +// GetService returns the service name for the given port. +func (r RsdHandshakeResponse) GetService(p int) string { + for name, s := range r.Services { + if s.Port == uint32(p) { + return name + } + } + return "" +} + +// GetPort returns the port for the given service. +func (r RsdHandshakeResponse) GetPort(service string) int { + if s, ok := r.Services[service]; ok { + return int(s.Port) + } + return 0 +} + +// NewWithAddr creates a new RsdService with the given address and port 58783 using a HTTP2 based XPC connection. +func NewWithAddr(addr string) (RsdService, error) { + return NewWithAddrPort(addr, port) +} + +// NewWithAddrPort creates a new RsdService with the given address and port using a HTTP2 based XPC connection. +func NewWithAddrPort(addr string, port int) (RsdService, error) { + h, err := ConnectToHttp2WithAddr(addr, port) + if err != nil { + return RsdService{}, fmt.Errorf("NewWithAddrPort: failed to connect to http2: %w", err) + } + + x, err := CreateXpcConnection(h) + if err != nil { + return RsdService{}, fmt.Errorf("NewWithAddrPort: failed to create xpc connection: %w", err) + } + + return RsdService{ + xpc: x, + c: h, + }, nil +} + +// Handshake sends a handshake request to the device and returns the RsdHandshakeResponse +// which contains the UDID and the services available on the device. +func (s RsdService) Handshake() (RsdHandshakeResponse, error) { + log.Debug("execute handshake") + m, err := s.xpc.ReceiveOnClientServerStream() + if err != nil { + return RsdHandshakeResponse{}, fmt.Errorf("Handshake: failed to receive handshake response. %w", err) + } + udid := "" + if properties, ok := m["Properties"].(map[string]interface{}); ok { + if u, ok := properties["UniqueDeviceID"].(string); ok { + udid = u + } + } + if udid == "" { + return RsdHandshakeResponse{}, fmt.Errorf("Handshake: could not read UDID") + } + if m["MessageType"] == "Handshake" { + servicesMap := m["Services"].(map[string]interface{}) + res := make(map[string]RsdServiceEntry) + for s, m := range servicesMap { + s2 := m.(map[string]interface{})["Port"].(string) + p, err := strconv.ParseInt(s2, 10, 32) + if err != nil { + return RsdHandshakeResponse{}, fmt.Errorf("Handshake: failed to parse port: %w", err) + } + res[s] = RsdServiceEntry{ + Port: uint32(p), + } + } + return RsdHandshakeResponse{ + Services: res, + Udid: udid, + }, nil + } else { + return RsdHandshakeResponse{}, fmt.Errorf("Handshake: unknown response") + } +} diff --git a/ios/rsd_test.go b/ios/rsd_test.go new file mode 100644 index 00000000..8ecd5eb6 --- /dev/null +++ b/ios/rsd_test.go @@ -0,0 +1,484 @@ +package ios + +import ( + "github.com/stretchr/testify/assert" + "strings" + "testing" +) + +func TestGetRsdPorts(t *testing.T) { + rsd, err := NewRsdPortProvider(strings.NewReader(rsdOutput)) + assert.NoError(t, err) + + t.Run("exact match", func(t *testing.T) { + testmanagerd := rsd.GetPort("com.apple.dt.testmanagerd.remote") + assert.Equal(t, 50340, testmanagerd) + }) + t.Run("fall back to shim", func(t *testing.T) { + syslog := rsd.GetPort("com.apple.syslog_relay") + assert.Equal(t, 50343, syslog) + }) +} + +const rsdOutput = ` +{ + "MessageType": "Handshake", + "MessagingProtocolVersion": 3, + "Properties": { + "AppleInternal": false, + "BoardId": 26, + "BootSessionUUID": "aeaf223a-9447-4771-bc4c-1572f330b68c", + "BuildVersion": "21A360", + "CPUArchitecture": "arm64e", + "CertificateProductionStatus": true, + "CertificateSecurityMode": true, + "ChipID": 32800, + "DeviceClass": "iPhone", + "DeviceColor": "1", + "DeviceEnclosureColor": "1", + "DeviceSupportsLockdown": true, + "EffectiveProductionStatusAp": true, + "EffectiveProductionStatusSEP": true, + "EffectiveSecurityModeAp": true, + "EffectiveSecurityModeSEP": true, + "EthernetMacAddress": "90:e1:7b:21:7b:84", + "HWModel": "D331pAP", + "HardwarePlatform": "t8020", + "HasSEP": true, + "HumanReadableProductVersionString": "17.0.3", + "Image4CryptoHashMethod": "sha2-384", + "Image4Supported": true, + "IsUIBuild": true, + "IsVirtualDevice": false, + "MobileDeviceMinimumVersion": "1600", + "ModelNumber": "MT502", + "OSInstallEnvironment": false, + "OSVersion": "17.0.3", + "ProductName": "iPhone OS", + "ProductType": "iPhone11,6", + "RegionCode": "ZD", + "RegionInfo": "ZD/A", + "RemoteXPCVersionFlags": 72057594037927940, + "RestoreLongVersion": "21.1.360.0.0,0", + "SecurityDomain": 1, + "SensitivePropertiesVisible": true, + "SerialNumber": "FFMXNFFJKPH1", + "SigningFuse": true, + "StoreDemoMode": false, + "SupplementalBuildVersion": "21A360", + "ThinningProductType": "iPhone11,6", + "UniqueChipID": 7125711553429550, + "UniqueDeviceID": "00008020-001950CC01EA002E" + }, + "Services": { + "com.apple.GPUTools.MobileService.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "50358" + }, + "com.apple.PurpleReverseProxy.Conn.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "50335" + }, + "com.apple.PurpleReverseProxy.Ctrl.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "50357" + }, + "com.apple.RestoreRemoteServices.restoreserviced": { + "Entitlement": "com.apple.private.RestoreRemoteServices.restoreservice.remote", + "Port": "50332", + "Properties": { + "ServiceVersion": 2, + "UsesRemoteXPC": true + } + }, + "com.apple.accessibility.axAuditDaemon.remoteserver.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "50373" + }, + "com.apple.afc.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "50351" + }, + "com.apple.amfi.lockdown.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "50374" + }, + "com.apple.atc.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "50371" + }, + "com.apple.atc2.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "50364" + }, + "com.apple.backgroundassets.lockdownservice.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "50365" + }, + "com.apple.bluetooth.BTPacketLogger.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "50356" + }, + "com.apple.carkit.remote-iap.service": { + "Entitlement": "AppleInternal", + "Port": "50370", + "Properties": { + "UsesRemoteXPC": true + } + }, + "com.apple.carkit.service.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "50317" + }, + "com.apple.commcenter.mobile-helper-cbupdateservice.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "50325" + }, + "com.apple.companion_proxy.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "50320" + }, + "com.apple.corecaptured.remoteservice": { + "Entitlement": "com.apple.corecaptured.remoteservice-access", + "Port": "50363", + "Properties": { + "UsesRemoteXPC": true + } + }, + "com.apple.coredevice.appservice": { + "Entitlement": "com.apple.private.CoreDevice.canInstallCustomerContent", + "Port": "50353", + "Properties": { + "Features": [ + "com.apple.coredevice.feature.launchapplication", + "com.apple.coredevice.feature.spawnexecutable", + "com.apple.coredevice.feature.monitorprocesstermination", + "com.apple.coredevice.feature.installapp", + "com.apple.coredevice.feature.uninstallapp", + "com.apple.coredevice.feature.listroots", + "com.apple.coredevice.feature.installroot", + "com.apple.coredevice.feature.uninstallroot", + "com.apple.coredevice.feature.sendsignaltoprocess", + "com.apple.coredevice.feature.sendmemorywarningtoprocess", + "com.apple.coredevice.feature.listprocesses", + "com.apple.coredevice.feature.rebootdevice", + "com.apple.coredevice.feature.listapps", + "com.apple.coredevice.feature.fetchappicons" + ], + "ServiceVersion": 1, + "UsesRemoteXPC": true + } + }, + "com.apple.coredevice.deviceinfo": { + "Entitlement": "com.apple.private.CoreDevice.canRetrieveDeviceInfo", + "Port": "50333", + "Properties": { + "Features": [ + "com.apple.coredevice.feature.getdisplayinfo", + "com.apple.coredevice.feature.getdeviceinfo", + "com.apple.coredevice.feature.querymobilegestalt", + "com.apple.coredevice.feature.getlockstate" + ], + "ServiceVersion": 1, + "UsesRemoteXPC": true + } + }, + "com.apple.coredevice.diagnosticsservice": { + "Entitlement": "com.apple.private.CoreDevice.canObtainDiagnostics", + "Port": "50378", + "Properties": { + "Features": [ + "com.apple.coredevice.feature.capturesysdiagnose" + ], + "ServiceVersion": 1, + "UsesRemoteXPC": true + } + }, + "com.apple.coredevice.fileservice.control": { + "Entitlement": "com.apple.private.CoreDevice.canTransferFilesToDevice", + "Port": "50347", + "Properties": { + "Features": [ + "com.apple.coredevice.feature.transferFiles" + ], + "ServiceVersion": 1, + "UsesRemoteXPC": true + } + }, + "com.apple.coredevice.fileservice.data": { + "Entitlement": "com.apple.private.CoreDevice.canTransferFilesToDevice", + "Port": "50337", + "Properties": { + "Features": [], + "ServiceVersion": 1, + "UsesRemoteXPC": true + } + }, + "com.apple.coredevice.openstdiosocket": { + "Entitlement": "com.apple.private.CoreDevice.canInstallCustomerContent", + "Port": "50380", + "Properties": { + "Features": [], + "ServiceVersion": 1, + "UsesRemoteXPC": true + } + }, + "com.apple.crashreportcopymobile.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "50368" + }, + "com.apple.crashreportmover.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "50375" + }, + "com.apple.dt.ViewHierarchyAgent.remote": { + "Entitlement": "com.apple.private.dt.ViewHierarchyAgent.client", + "Port": "50345", + "Properties": { + "UsesRemoteXPC": true + } + }, + "com.apple.dt.remoteFetchSymbols": { + "Entitlement": "com.apple.private.dt.remoteFetchSymbols.client", + "Port": "50334", + "Properties": { + "Features": [ + "com.apple.dt.remoteFetchSymbols.dyldSharedCacheFiles" + ], + "ServiceVersion": 1, + "UsesRemoteXPC": true + } + }, + "com.apple.dt.remotepairingdeviced.lockdown.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "50328" + }, + "com.apple.dt.testmanagerd.remote": { + "Entitlement": "com.apple.private.dt.testmanagerd.client", + "Port": "50340", + "Properties": { + "UsesRemoteXPC": false + } + }, + "com.apple.dt.testmanagerd.remote.automation": { + "Entitlement": "AppleInternal", + "Port": "50344", + "Properties": { + "UsesRemoteXPC": false + } + }, + "com.apple.fusion.remote.service": { + "Entitlement": "com.apple.fusion.remote.service", + "Port": "50339", + "Properties": { + "ServiceVersion": 1, + "UsesRemoteXPC": true + } + }, + "com.apple.gputools.remote.agent": { + "Entitlement": "com.apple.private.gputoolstransportd", + "Port": "50367", + "Properties": { + "ServiceVersion": 1, + "UsesRemoteXPC": true + } + }, + "com.apple.idamd.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "50319" + }, + "com.apple.instruments.dtservicehub": { + "Entitlement": "com.apple.private.dt.instruments.dtservicehub.client", + "Port": "50338", + "Properties": { + "Features": [ + "com.apple.dt.profile" + ], + "version": 1 + } + }, + "com.apple.internal.devicecompute.CoreDeviceProxy": { + "Entitlement": "AppleInternal", + "Port": "50349", + "Properties": { + "ServiceVersion": 1, + "UsesRemoteXPC": false + } + }, + "com.apple.internal.dt.coredevice.untrusted.tunnelservice": { + "Entitlement": "com.apple.dt.coredevice.tunnelservice.client", + "Port": "50321", + "Properties": { + "ServiceVersion": 2, + "UsesRemoteXPC": true + } + }, + "com.apple.internal.dt.remote.debugproxy": { + "Entitlement": "com.apple.private.CoreDevice.canDebugApplicationsOnDevice", + "Port": "50369", + "Properties": { + "Features": [ + "com.apple.coredevice.feature.debugserverproxy" + ], + "ServiceVersion": 1, + "UsesRemoteXPC": true + } + }, + "com.apple.iosdiagnostics.relay.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "50346" + }, + "com.apple.misagent.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "50382" + }, + "com.apple.mobile.MCInstall.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "50326" + }, + "com.apple.mobile.assertion_agent.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "50323" + }, + "com.apple.mobile.diagnostics_relay.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "50377" + }, + "com.apple.mobile.file_relay.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "50327" + }, + "com.apple.mobile.heartbeat.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "50366" + }, + "com.apple.mobile.house_arrest.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "50342" + }, + "com.apple.mobile.insecure_notification_proxy.remote": { + "Entitlement": "com.apple.mobile.insecure_notification_proxy.remote", + "Port": "50318", + "Properties": { + "ServiceVersion": 1, + "UsesRemoteXPC": true + } + }, + "com.apple.mobile.insecure_notification_proxy.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.untrusted", + "Port": "50348" + }, + "com.apple.mobile.installation_proxy.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "50336" + }, + "com.apple.mobile.lockdown.remote.trusted": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "50372", + "Properties": { + "ServiceVersion": 1, + "UsesRemoteXPC": true + } + }, + "com.apple.mobile.lockdown.remote.untrusted": { + "Entitlement": "com.apple.mobile.lockdown.remote.untrusted", + "Port": "50329", + "Properties": { + "ServiceVersion": 1, + "UsesRemoteXPC": true + } + }, + "com.apple.mobile.mobile_image_mounter.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "50322" + }, + "com.apple.mobile.notification_proxy.remote": { + "Entitlement": "com.apple.mobile.notification_proxy.remote", + "Port": "50341", + "Properties": { + "ServiceVersion": 1, + "UsesRemoteXPC": true + } + }, + "com.apple.mobile.notification_proxy.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "50354" + }, + "com.apple.mobile.storage_mounter_proxy.bridge": { + "Entitlement": "com.apple.private.mobile_storage.remote.allowedSPI", + "Port": "50331", + "Properties": { + "ServiceVersion": 1, + "UsesRemoteXPC": true + } + }, + "com.apple.mobileactivationd.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "50355" + }, + "com.apple.mobilebackup2.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "50376" + }, + "com.apple.mobilesync.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "50330" + }, + "com.apple.os_trace_relay.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "50316" + }, + "com.apple.osanalytics.logTransfer": { + "Entitlement": "com.apple.ReportCrash.antenna-access", + "Port": "50360", + "Properties": { + "UsesRemoteXPC": true + } + }, + "com.apple.pcapd.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "50379" + }, + "com.apple.preboardservice.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "50359" + }, + "com.apple.preboardservice_v2.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "50350" + }, + "com.apple.remote.installcoordination_proxy": { + "Entitlement": "com.apple.private.InstallCoordinationRemote", + "Port": "50361", + "Properties": { + "ServiceVersion": 1, + "UsesRemoteXPC": true + } + }, + "com.apple.springboardservices.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "50381" + }, + "com.apple.streaming_zip_conduit.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "50362" + }, + "com.apple.sysdiagnose.remote": { + "Entitlement": "com.apple.private.sysdiagnose.remote", + "Port": "50352", + "Properties": { + "UsesRemoteXPC": true + } + }, + "com.apple.syslog_relay.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "50343" + }, + "com.apple.webinspector.shim.remote": { + "Entitlement": "com.apple.mobile.lockdown.remote.trusted", + "Port": "50324" + } + }, + "UUID": "d09e3fd1-62aa-47bc-b01a-23df97017d34" +} +` diff --git a/ios/utils.go b/ios/utils.go index 66f1626a..d329392e 100755 --- a/ios/utils.go +++ b/ios/utils.go @@ -64,6 +64,10 @@ func Ntohs(port uint16) uint16 { // if the env variable 'udid' is specified, the device with that udid // otherwise it returns the first device in the list. func GetDevice(udid string) (DeviceEntry, error) { + return GetDeviceWithAddress(udid, "", nil) +} + +func GetDeviceWithAddress(udid string, address string, provider RsdPortProvider) (DeviceEntry, error) { if udid == "" { udid = os.Getenv("udid") if udid != "" { @@ -79,12 +83,17 @@ func GetDevice(udid string) (DeviceEntry, error) { if len(deviceList.DeviceList) == 0 { return DeviceEntry{}, errors.New("no iOS devices are attached to this host") } - log.WithFields(log.Fields{"udid": deviceList.DeviceList[0].Properties.SerialNumber}). + device := deviceList.DeviceList[0] + log.WithFields(log.Fields{"udid": device.Properties.SerialNumber}). Info("no udid specified using first device in list") - return deviceList.DeviceList[0], nil + device.Address = address + device.Rsd = provider + return device, nil } for _, device := range deviceList.DeviceList { if device.Properties.SerialNumber == udid { + device.Address = address + device.Rsd = provider return device, nil } } @@ -104,6 +113,10 @@ func PathExists(path string) (bool, error) { return false, err } +func IOS17() *semver.Version { + return semver.MustParse("17.0") +} + func IOS14() *semver.Version { return semver.MustParse("14.0") } @@ -152,3 +165,18 @@ func InterfaceToStringSlice(intfSlice interface{}) []string { } return result } + +// GenericSliceToType tries to convert a slice of interfaces to a slice of the given type. +// It returns an error if the conversion fails but will not panic. +// Example: var b []bool; b, err = GenericSliceToType[bool]([]interface{}{true, false}) +func GenericSliceToType[T any](input []interface{}) ([]T, error) { + result := make([]T, len(input)) + for i, intf := range input { + if t, ok := intf.(T); ok { + result[i] = t + } else { + return []T{}, fmt.Errorf("GenericSliceToType: could not convert %v to %T", intf, result[i]) + } + } + return result, nil +} diff --git a/ios/utils_test.go b/ios/utils_test.go index 5202477c..ec96f255 100644 --- a/ios/utils_test.go +++ b/ios/utils_test.go @@ -24,6 +24,15 @@ type SampleData struct { FloatValue float64 } +func TestGenericSliceToType(t *testing.T) { + slice := []interface{}{5, 3, 2} + v, err := ios.GenericSliceToType[int](slice) + assert.Nil(t, err) + assert.Equal(t, 3, v[1]) + _, err = ios.GenericSliceToType[string](slice) + assert.NotNil(t, err) +} + func TestNtohs(t *testing.T) { assert.Equal(t, uint16(62078), ios.Ntohs(ios.Lockdownport)) } diff --git a/ios/xpc/encoding.go b/ios/xpc/encoding.go index b3d9b134..769a5cf5 100644 --- a/ios/xpc/encoding.go +++ b/ios/xpc/encoding.go @@ -8,6 +8,9 @@ import ( "math" "reflect" "strings" + "time" + + "github.com/google/uuid" ) const bodyVersion = uint32(0x00000005) @@ -21,21 +24,27 @@ type xpcType uint32 // TODO: there are more types available and need to be added still when observed const ( - nullType = xpcType(0x00001000) - boolType = xpcType(0x00002000) - int64Type = xpcType(0x00003000) - uint64Type = xpcType(0x00004000) - dataType = xpcType(0x00008000) - stringType = xpcType(0x00009000) - arrayType = xpcType(0x0000e000) - dictionaryType = xpcType(0x0000f000) + nullType = xpcType(0x00001000) + boolType = xpcType(0x00002000) + int64Type = xpcType(0x00003000) + uint64Type = xpcType(0x00004000) + doubleType = xpcType(0x00005000) + dateType = xpcType(0x00007000) + dataType = xpcType(0x00008000) + stringType = xpcType(0x00009000) + uuidType = xpcType(0x0000a000) + arrayType = xpcType(0x0000e000) + dictionaryType = xpcType(0x0000f000) + fileTransferType = xpcType(0x0001a000) ) const ( - alwaysSetFlag = uint32(0x00000001) - dataFlag = uint32(0x00000100) - heartbeatRequestFlag = uint32(0x00010000) - heartbeatReplyFlag = uint32(0x00020000) + AlwaysSetFlag = uint32(0x00000001) + DataFlag = uint32(0x00000100) + HeartbeatRequestFlag = uint32(0x00010000) + HeartbeatReplyFlag = uint32(0x00020000) + FileOpenFlag = uint32(0x00100000) + InitHandshakeFlag = uint32(0x00400000) ) type wrapperHeader struct { @@ -47,36 +56,59 @@ type wrapperHeader struct { type Message struct { Flags uint32 Body map[string]interface{} + Id uint64 +} + +func (m Message) IsFileOpen() bool { + return m.Flags&FileOpenFlag > 0 +} + +type FileTransfer struct { + MsgId uint64 + TransferSize uint64 } // DecodeMessage expects a full RemoteXPC message and decodes the message body into a map func DecodeMessage(r io.Reader) (Message, error) { var magic uint32 if err := binary.Read(r, binary.LittleEndian, &magic); err != nil { - return Message{}, err + return Message{}, fmt.Errorf("DecodeMessage: failed to read magic number: %w", err) } if magic != wrapperMagic { - return Message{}, fmt.Errorf("wrong magic number") + return Message{}, fmt.Errorf("DecodeMessage: wrong magic number 0x%x", magic) } wrapper, err := decodeWrapper(r) - return wrapper, err + if err != nil { + return Message{}, fmt.Errorf("DecodeMessage: failed to decode wrapper: %w", err) + } + return wrapper, nil } -// EncodeData creates a RemoteXPC message with the data flag set, if data is present (an empty dictionary is considered -// to be no data) -func EncodeData(w io.Writer, body map[string]interface{}) error { - if body == nil { - return encodeMessageWithoutBody(w) +// EncodeMessage creates a RemoteXPC message encoded with the body and flags provided +func EncodeMessage(w io.Writer, message Message) error { + if message.Body == nil { + wrapper := struct { + magic uint32 + h wrapperHeader + }{ + magic: wrapperMagic, + h: wrapperHeader{ + Flags: message.Flags, + BodyLen: 0, + MsgId: message.Id, + }, + } + + err := binary.Write(w, binary.LittleEndian, wrapper) + if err != nil { + return fmt.Errorf("EncodeMessage: failed to write empty message: %w", err) + } + return nil } buf := bytes.NewBuffer(nil) - err := encodeDictionary(buf, body) + err := encodeDictionary(buf, message.Body) if err != nil { - return err - } - - flags := alwaysSetFlag - if len(body) > 0 { - flags |= dataFlag + return fmt.Errorf("EncodeMessage: failed to encode dictionary: %w", err) } wrapper := struct { @@ -89,9 +121,9 @@ func EncodeData(w io.Writer, body map[string]interface{}) error { }{ magic: wrapperMagic, h: wrapperHeader{ - Flags: flags, + Flags: message.Flags, BodyLen: uint64(buf.Len() + 8), - MsgId: 0, + MsgId: message.Id, }, body: struct { magic uint32 @@ -104,18 +136,22 @@ func EncodeData(w io.Writer, body map[string]interface{}) error { err = binary.Write(w, binary.LittleEndian, wrapper) if err != nil { - return err + return fmt.Errorf("EncodeMessage: failed to write xpc wrapper: %w", err) } _, err = io.Copy(w, buf) - return err + if err != nil { + return fmt.Errorf("EncodeMessage: failed to write message body: %w", err) + } + return nil + } func decodeWrapper(r io.Reader) (Message, error) { var h wrapperHeader err := binary.Read(r, binary.LittleEndian, &h) if err != nil { - return Message{}, err + return Message{}, fmt.Errorf("decodeWrapper: failed to decode header wrapper: %w", err) } if h.BodyLen == 0 { return Message{ @@ -123,10 +159,13 @@ func decodeWrapper(r io.Reader) (Message, error) { }, nil } body, err := decodeBody(r, h) + if err != nil { + return Message{}, fmt.Errorf("decodeWrapper: failed to decode body: %w", err) + } return Message{ Flags: h.Flags, Body: body, - }, err + }, nil } func decodeBody(r io.Reader, h wrapperHeader) (map[string]interface{}, error) { @@ -135,26 +174,27 @@ func decodeBody(r io.Reader, h wrapperHeader) (map[string]interface{}, error) { Version uint32 }{} if err := binary.Read(r, binary.LittleEndian, &bodyHeader); err != nil { - return nil, err + return nil, fmt.Errorf("decodeBody: failed to decode header: %w", err) } if bodyHeader.Magic != objectMagic { - return nil, fmt.Errorf("cant decode") + return nil, fmt.Errorf("decodeBody: invalid object magic number 0x%x", bodyHeader.Magic) } if bodyHeader.Version != bodyVersion { - return nil, fmt.Errorf("expected version 0x%x but got 0x%x", bodyVersion, bodyHeader.Version) + return nil, fmt.Errorf("decodeBody: expected version 0x%x but got 0x%x", bodyVersion, bodyHeader.Version) } - body := make([]byte, h.BodyLen-8) + bodyPayloadLength := h.BodyLen - 8 + body := make([]byte, bodyPayloadLength) n, err := r.Read(body) if err != nil { - return nil, err + return nil, fmt.Errorf("decodeBody:: failed to read body data: %w", err) } - if uint64(n) != (h.BodyLen - 8) { - return nil, fmt.Errorf("not enough data") + if uint64(n) != bodyPayloadLength { + return nil, fmt.Errorf("decodeBody: could not read full body. only %d instead of %d were read", n, bodyPayloadLength) } bodyBuf := bytes.NewReader(body) res, err := decodeObject(bodyBuf) if err != nil { - return nil, err + return nil, fmt.Errorf("decodeBody: failed to decode body: %w", err) } return res.(map[string]interface{}), nil } @@ -163,7 +203,7 @@ func decodeObject(r io.Reader) (interface{}, error) { var t xpcType err := binary.Read(r, binary.LittleEndian, &t) if err != nil { - return nil, err + return nil, fmt.Errorf("decodeObject: could not read type: %w", err) } switch t { case nullType: @@ -174,16 +214,64 @@ func decodeObject(r io.Reader) (interface{}, error) { return decodeInt64(r) case uint64Type: return decodeUint64(r) + case doubleType: + return decodeDouble(r) + case dateType: + return decodeDate(r) case dataType: return decodeData(r) case stringType: return decodeString(r) + case uuidType: + return decodeUuid(r) case arrayType: return decodeArray(r) case dictionaryType: return decodeDictionary(r) + case fileTransferType: + return decodeFileTransfer(r) default: - return nil, fmt.Errorf("can't handle unknown type 0x%08x", t) + return nil, fmt.Errorf("decodeObject: can't handle unknown type 0x%08x", t) + } +} + +func decodeUuid(r io.Reader) (uuid.UUID, error) { + b := make([]byte, 16) + _, err := r.Read(b) + if err != nil { + return [16]byte{}, fmt.Errorf("decodeUuid: failed to read data: %w", err) + } + u, err := uuid.FromBytes(b) + if err != nil { + return [16]byte{}, fmt.Errorf("decodeUuid: failed to parse UUID: %w", err) + } + return u, nil +} + +func decodeFileTransfer(r io.Reader) (FileTransfer, error) { + header := struct { + MsgId uint64 // always 1 + }{} + err := binary.Read(r, binary.LittleEndian, &header) + if err != nil { + return FileTransfer{}, fmt.Errorf("decodeFileTransfer: failed to read data: %w", err) + } + d, err := decodeObject(r) + if err != nil { + return FileTransfer{}, fmt.Errorf("decodeFileTransfer: failed to decode object: %w", err) + } + if dict, ok := d.(map[string]interface{}); ok { + // the transfer length is always stored in a property 's' + if transferLen, ok := dict["s"].(uint64); ok { + return FileTransfer{ + MsgId: header.MsgId, + TransferSize: transferLen, + }, nil + } else { + return FileTransfer{}, fmt.Errorf("decodeFileTransfer: expected uint64 for transfer length") + } + } else { + return FileTransfer{}, fmt.Errorf("decodeFileTransfer: expected a dictionary but got %T", d) } } @@ -191,21 +279,21 @@ func decodeDictionary(r io.Reader) (map[string]interface{}, error) { var l, numEntries uint32 err := binary.Read(r, binary.LittleEndian, &l) if err != nil { - return nil, err + return nil, fmt.Errorf("decodeDictionary: failed to read data: %w", err) } err = binary.Read(r, binary.LittleEndian, &numEntries) if err != nil { - return nil, err + return nil, fmt.Errorf("decodeDictionary: failed to read number of entries: %w", err) } dict := make(map[string]interface{}) for i := uint32(0); i < numEntries; i++ { key, err := readDictionaryKey(r) if err != nil { - return nil, err + return nil, fmt.Errorf("decodeDictionary: failed to read dictionary key: %w", err) } dict[key], err = decodeObject(r) if err != nil { - return nil, err + return nil, fmt.Errorf("decodeDictionary: failed to decode object for key '%s': %w", key, err) } } return dict, nil @@ -217,13 +305,16 @@ func readDictionaryKey(r io.Reader) (string, error) { for { _, err := r.Read(buf) if err != nil { - return "", err + return "", fmt.Errorf("readDictionaryKey: failed to read character: %w", err) } if buf[0] == 0 { s := b.String() toSkip := calcPadding(len(s) + 1) _, err := io.CopyN(io.Discard, r, toSkip) - return s, err + if err != nil { + return "", fmt.Errorf("readDictionaryKey: failed to discard padding: %w", err) + } + return s, nil } b.Write(buf) } @@ -233,17 +324,17 @@ func decodeArray(r io.Reader) ([]interface{}, error) { var l, numEntries uint32 err := binary.Read(r, binary.LittleEndian, &l) if err != nil { - return nil, err + return nil, fmt.Errorf("decodeArray: failed to read payload length: %w", err) } err = binary.Read(r, binary.LittleEndian, &numEntries) if err != nil { - return nil, err + return nil, fmt.Errorf("decodeArray: failed to read number of entries: %w", err) } arr := make([]interface{}, numEntries) for i := uint32(0); i < numEntries; i++ { arr[i], err = decodeObject(r) if err != nil { - return nil, err + return nil, fmt.Errorf("decodeArray: failed to decode object at index %d: %w", i, err) } } return arr, nil @@ -253,16 +344,19 @@ func decodeString(r io.Reader) (string, error) { var l uint32 err := binary.Read(r, binary.LittleEndian, &l) if err != nil { - return "", err + return "", fmt.Errorf("decodeString: failed to read string length: %w", err) } s := make([]byte, l) _, err = r.Read(s) if err != nil { - return "", err + return "", fmt.Errorf("decodeString: failed to read string: %w", err) } res := strings.Trim(string(s), "\000") toSkip := calcPadding(int(l)) - _, _ = io.CopyN(io.Discard, r, toSkip) + _, err = io.CopyN(io.Discard, r, toSkip) + if err != nil { + return "", fmt.Errorf("decodeString: faile to skip padding bytes: %w", err) + } return res, nil } @@ -270,29 +364,41 @@ func decodeData(r io.Reader) ([]byte, error) { var l uint32 err := binary.Read(r, binary.LittleEndian, &l) if err != nil { - return nil, err + return nil, fmt.Errorf("decodeData: failed to read payload length: %w", err) } b := make([]byte, l) _, err = r.Read(b) if err != nil { - return nil, err + return nil, fmt.Errorf("decodeData: failed to read payload: %w", err) } toSkip := calcPadding(int(l)) _, _ = io.CopyN(io.Discard, r, toSkip) return b, nil } +func decodeDouble(r io.Reader) (interface{}, error) { + var d float64 + err := binary.Read(r, binary.LittleEndian, &d) + if err != nil { + return 0, fmt.Errorf("decodeDouble: failed to read data: %w", err) + } + return d, nil +} + func decodeUint64(r io.Reader) (uint64, error) { var i uint64 err := binary.Read(r, binary.LittleEndian, &i) - return i, err + if err != nil { + return 0, fmt.Errorf("decodeUint64: failed to read data: %w", err) + } + return i, nil } func decodeInt64(r io.Reader) (int64, error) { var i int64 err := binary.Read(r, binary.LittleEndian, &i) if err != nil { - return 0, err + return 0, fmt.Errorf("decodeInt64: failed to read data: %w", err) } return i, nil } @@ -301,12 +407,22 @@ func decodeBool(r io.Reader) (bool, error) { var b bool err := binary.Read(r, binary.LittleEndian, &b) if err != nil { - return false, err + return false, fmt.Errorf("decodeBool: failed to read data: %w", err) } _, _ = io.CopyN(io.Discard, r, 3) return b, nil } +func decodeDate(r io.Reader) (time.Time, error) { + var i int64 + err := binary.Read(r, binary.LittleEndian, &i) + if err != nil { + return time.Time{}, fmt.Errorf("decodeDate: failed to read date payload: %w", err) + } + t := time.Unix(0, i) + return t, nil +} + func calcPadding(l int) int64 { c := int(math.Ceil(float64(l) / 4.0)) return int64(c*4 - l) @@ -315,37 +431,41 @@ func calcPadding(l int) int64 { func encodeDictionary(w io.Writer, v map[string]interface{}) error { buf := bytes.NewBuffer(nil) + err := binary.Write(buf, binary.LittleEndian, uint32(len(v))) + if err != nil { + return fmt.Errorf("encodeDictionary: failed to write number of dictionary entries: %w", err) + } + for k, e := range v { err := encodeDictionaryKey(buf, k) if err != nil { - return err + return fmt.Errorf("encodeDictionary: failed to encode dictionary key '%s': %w", k, err) } - err2 := encodeObject(buf, e) - if err2 != nil { - return err2 + err = encodeObject(buf, e) + if err != nil { + return fmt.Errorf("encodeDictionary: failed to encode object: %w", err) } } - err := binary.Write(w, binary.LittleEndian, dictionaryType) + err = binary.Write(w, binary.LittleEndian, dictionaryType) if err != nil { - return err + return fmt.Errorf("encodeDictionary: failed to write dictionary type: %w", err) } err = binary.Write(w, binary.LittleEndian, uint32(buf.Len())) if err != nil { - return err + return fmt.Errorf("encodeDictionary: failed to write payload length: %w", err) } - err = binary.Write(w, binary.LittleEndian, uint32(len(v))) + _, err = w.Write(buf.Bytes()) if err != nil { - return err + return fmt.Errorf("encodeDictionary: failed to write ") } - _, err = w.Write(buf.Bytes()) - return err + return nil } func encodeObject(w io.Writer, e interface{}) error { if e == nil { if err := binary.Write(w, binary.LittleEndian, nullType); err != nil { - return err + return fmt.Errorf("encodeObject: failed to encode null objecdt: %w", err) } return nil } @@ -375,10 +495,22 @@ func encodeObject(w io.Writer, e interface{}) error { if err := encodeUint64(w, e.(uint64)); err != nil { return err } + case float64: + if err := encodeDouble(w, e.(float64)); err != nil { + return err + } case string: if err := encodeString(w, e.(string)); err != nil { return err } + case uuid.UUID: + if err := encodeUuid(w, e.(uuid.UUID)); err != nil { + return err + } + case time.Time: + if err := encodeDate(w, e.(time.Time)); err != nil { + return err + } case map[string]interface{}: if err := encodeDictionary(w, e.(map[string]interface{})); err != nil { return err @@ -389,11 +521,23 @@ func encodeObject(w io.Writer, e interface{}) error { return nil } +func encodeUuid(w io.Writer, u uuid.UUID) error { + out := struct { + t xpcType + u uuid.UUID + }{uuidType, u} + err := binary.Write(w, binary.LittleEndian, out) + if err != nil { + return fmt.Errorf("encodeUuid: failed to write UUID payload: %w", err) + } + return nil +} + func encodeArray(w io.Writer, slice []interface{}) error { buf := bytes.NewBuffer(nil) - for _, e := range slice { + for i, e := range slice { if err := encodeObject(buf, e); err != nil { - return err + return fmt.Errorf("encodeArray: failed to encode array object at index %d: %w", i, err) } } @@ -403,10 +547,10 @@ func encodeArray(w io.Writer, slice []interface{}) error { numObjects uint32 }{arrayType, uint32(buf.Len()), uint32(len(slice))} if err := binary.Write(w, binary.LittleEndian, header); err != nil { - return err + return fmt.Errorf("encodeArray: failed to write array header: %w", err) } if _, err := io.Copy(w, buf); err != nil { - return err + return fmt.Errorf("encodeArray: failed to copy array payload: %w", err) } return nil } @@ -418,16 +562,15 @@ func encodeString(w io.Writer, s string) error { }{stringType, uint32(len(s) + 1)} err := binary.Write(w, binary.LittleEndian, header) if err != nil { - return err - } - _, err = w.Write([]byte(s)) - if err != nil { - return err + return fmt.Errorf("encodeString: failed to write header: %w", err) } + toPad := calcPadding(int(header.l)) - _, err = w.Write(make([]byte, toPad+1)) + padded := make([]byte, len(s)+int(toPad)+1) + copy(padded, s) + _, err = w.Write(padded) if err != nil { - return err + return fmt.Errorf("encodeString: failed to write string payload: %w", err) } return nil } @@ -439,16 +582,16 @@ func encodeData(w io.Writer, b []byte) error { }{dataType, uint32(len(b))} err := binary.Write(w, binary.LittleEndian, header) if err != nil { - return err + return fmt.Errorf("encodeData: failed to write data length: %w", err) } _, err = w.Write(b) if err != nil { - return err + return fmt.Errorf("encodeData: failed to write data: %w", err) } toPad := calcPadding(int(header.l)) _, err = w.Write(make([]byte, toPad)) if err != nil { - return err + return fmt.Errorf("encodeData: failed to write padding: %w", err) } return nil } @@ -460,7 +603,7 @@ func encodeUint64(w io.Writer, i uint64) error { }{uint64Type, i} err := binary.Write(w, binary.LittleEndian, out) if err != nil { - return err + return fmt.Errorf("encodeUint64: failed to write data: %w", err) } return nil } @@ -472,7 +615,19 @@ func encodeInt64(w io.Writer, i int64) error { }{int64Type, i} err := binary.Write(w, binary.LittleEndian, out) if err != nil { - return err + return fmt.Errorf("encodeInt64: failed to write data: %w", err) + } + return nil +} + +func encodeDouble(w io.Writer, d float64) error { + out := struct { + t xpcType + d float64 + }{doubleType, d} + err := binary.Write(w, binary.LittleEndian, out) + if err != nil { + return fmt.Errorf("encodeDouble: failed to write data: %w", err) } return nil } @@ -488,34 +643,31 @@ func encodeBool(w io.Writer, b bool) error { } err := binary.Write(w, binary.LittleEndian, out) if err != nil { - return err + return fmt.Errorf("encodeBool: failed to write data: %w", err) } return nil } -func encodeDictionaryKey(w io.Writer, k string) error { - toPad := calcPadding(len(k) + 1) - _, err := w.Write(append([]byte(k), 0x0)) +func encodeDate(w io.Writer, t time.Time) error { + out := struct { + t xpcType + i int64 + }{dateType, t.UnixNano()} + err := binary.Write(w, binary.LittleEndian, out) if err != nil { - return err + return fmt.Errorf("encodeDate: failed to write data: %w", err) } - pad := make([]byte, toPad) - _, err = w.Write(pad) - return err + return nil } -func encodeMessageWithoutBody(w io.Writer) error { - wrapper := struct { - magic uint32 - h wrapperHeader - }{ - magic: wrapperMagic, - h: wrapperHeader{ - Flags: alwaysSetFlag, - BodyLen: 0, - MsgId: 0, - }, +func encodeDictionaryKey(w io.Writer, k string) error { + strLen := len(k) + 1 + toPad := calcPadding(strLen) + content := make([]byte, strLen+int(toPad)) + copy(content, k) + _, err := w.Write(content) + if err != nil { + return fmt.Errorf("encodeDictionaryKey: failed to write data: %w", err) } - err := binary.Write(w, binary.LittleEndian, wrapper) - return err + return nil } diff --git a/ios/xpc/encoding_test.go b/ios/xpc/encoding_test.go index 63b201dd..a04d72f9 100644 --- a/ios/xpc/encoding_test.go +++ b/ios/xpc/encoding_test.go @@ -3,6 +3,7 @@ package xpc import ( "bytes" "encoding/base64" + "github.com/google/uuid" "github.com/stretchr/testify/assert" "os" "path" @@ -15,7 +16,7 @@ func TestEmptyDictionary(t *testing.T) { res, err := DecodeMessage(bytes.NewReader(b)) assert.NoError(t, err) assert.Equal(t, Message{ - Flags: alwaysSetFlag, + Flags: AlwaysSetFlag, Body: map[string]interface{}{}, }, res) } @@ -26,7 +27,7 @@ func TestDictionary(t *testing.T) { res, err := DecodeMessage(bytes.NewReader(b)) assert.NoError(t, err) assert.Equal(t, Message{ - Flags: alwaysSetFlag | dataFlag | heartbeatRequestFlag, + Flags: AlwaysSetFlag | DataFlag | HeartbeatRequestFlag, Body: map[string]interface{}{ "CoreDevice.CoreDeviceDDIProtocolVersion": int64(0), "CoreDevice.action": map[string]interface{}{}, @@ -82,12 +83,12 @@ func TestEncodeDecode(t *testing.T) { { name: "empty dict", input: map[string]interface{}{}, - expectedFlags: alwaysSetFlag, + expectedFlags: AlwaysSetFlag | DataFlag, }, { name: "no xpc body", input: nil, - expectedFlags: alwaysSetFlag, + expectedFlags: AlwaysSetFlag | DataFlag, }, { name: "keys without padding", @@ -95,7 +96,7 @@ func TestEncodeDecode(t *testing.T) { "key": "value", "key-key": "value", }, - expectedFlags: alwaysSetFlag | dataFlag, + expectedFlags: AlwaysSetFlag | DataFlag, }, { name: "nested values", @@ -106,30 +107,55 @@ func TestEncodeDecode(t *testing.T) { "int64": int64(123), "uint64": uint64(321), "data": []byte{0x1}, + "double": float64(1.2), }, }, - expectedFlags: alwaysSetFlag | dataFlag, + expectedFlags: AlwaysSetFlag | DataFlag, }, { name: "null entry", input: map[string]interface{}{ "null": nil, }, - expectedFlags: alwaysSetFlag | dataFlag, + expectedFlags: AlwaysSetFlag | DataFlag, }, { name: "dictionary with array", input: map[string]interface{}{ "array": []interface{}{uint64(1), uint64(2), uint64(3)}, }, - expectedFlags: alwaysSetFlag | dataFlag, + expectedFlags: AlwaysSetFlag | DataFlag, + }, + { + name: "encode uuid", + input: map[string]interface{}{ + "uuidvalue": func() uuid.UUID { + u, _ := uuid.FromBytes(base64Decode("RYjS2yNAbEG+Y0WWxq5/4w==")) + return u + }(), + }, + expectedFlags: AlwaysSetFlag | DataFlag, + }, + { + name: "encode uuid", + input: map[string]interface{}{ + "uuidvalue": func() uuid.UUID { + u, _ := uuid.FromBytes(base64Decode("RYjS2yNAbEG+Y0WWxq5/4w==")) + return u + }(), + }, + expectedFlags: AlwaysSetFlag | DataFlag, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { buf := bytes.NewBuffer(nil) - err := EncodeData(buf, tt.input) + err := EncodeMessage(buf, Message{ + Flags: AlwaysSetFlag | DataFlag, + Body: tt.input, + Id: 0, + }) assert.NoError(t, err) res, err := DecodeMessage(buf) assert.NoError(t, err) diff --git a/ios/xpc/xpc.go b/ios/xpc/xpc.go new file mode 100644 index 00000000..3ff2cb15 --- /dev/null +++ b/ios/xpc/xpc.go @@ -0,0 +1,71 @@ +// Package xpc contains a connection stuct and the codec for the xpc protocol. +// The xpc protocol is used to communicate with services on iOS17+ devices. +package xpc + +import ( + "fmt" + "io" + + "golang.org/x/net/http2" +) + +// Connection represents a http2 based connection to an XPC service on an iOS17 device. +type Connection struct { + connectionCloser io.Closer + framer *http2.Framer + msgId uint64 + clientServer io.ReadWriter + serverClient io.ReadWriter +} + +// New creates a new connection to an XPC service on an iOS17 device. +func New(clientServer io.ReadWriter, serverClient io.ReadWriter, closer io.Closer) (*Connection, error) { + return &Connection{ + connectionCloser: closer, + msgId: 1, + clientServer: clientServer, + serverClient: serverClient, + }, nil +} + +func (c *Connection) ReceiveOnServerClientStream() (map[string]interface{}, error) { + msg, err := DecodeMessage(c.serverClient) + if err != nil { + return nil, fmt.Errorf("ReceiveOnServerClientStream: %w", err) + } + return msg.Body, nil +} + +func (c *Connection) ReceiveOnClientServerStream() (map[string]interface{}, error) { + return c.receiveOnStream(c.clientServer) +} + +func (c *Connection) receiveOnStream(r io.Reader) (map[string]interface{}, error) { + msg, err := DecodeMessage(r) + if err != nil { + return nil, fmt.Errorf("receiveOnStream: %w", err) + } + return msg.Body, nil +} + +// Send sends the passed data as XPC message. +// Additional flags can be passed via the flags argument (the default ones are AlwaysSetFlag and if data != nil DataFlag) +func (c *Connection) Send(data map[string]interface{}, flags ...uint32) error { + f := AlwaysSetFlag + if data != nil { + f |= DataFlag + } + for _, flag := range flags { + f |= flag + } + msg := Message{ + Flags: f, + Body: data, + Id: c.msgId, + } + return EncodeMessage(c.clientServer, msg) +} + +func (c *Connection) Close() error { + return c.connectionCloser.Close() +}