From 22a300d9b396fd15e8dd94eaae1c0a12e7c129bf Mon Sep 17 00:00:00 2001 From: debugtalk Date: Thu, 22 Dec 2022 00:37:02 +0800 Subject: [PATCH] feat: capture pcap file with PID/ProcName/bundleID --- docs/CHANGELOG.md | 4 +- examples/worldcup/main_test.go | 7 +- hrp/cmd/ios/pcap.go | 24 ++++++- hrp/pkg/gidevice/device.go | 31 ++++++++- hrp/pkg/gidevice/idevice.go | 2 +- hrp/pkg/gidevice/lockdown.go | 4 +- hrp/pkg/gidevice/pcapd.go | 37 +++++++++++ hrp/pkg/gidevice/pcapd_test.go | 66 +++++++++++++++++++ hrp/pkg/gidevice/perfd_test.go | 19 ------ .../gidevice/pkg/libimobiledevice/pcapd.go | 38 ++++++++++- hrp/pkg/uixt/ios_device.go | 47 +++++++++++-- hrp/pkg/uixt/swipe.go | 2 +- 12 files changed, 243 insertions(+), 38 deletions(-) create mode 100644 hrp/pkg/gidevice/pcapd_test.go diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index c0dc5acb..95f7bc5c 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,6 +1,6 @@ # Release History -## v4.3.1 (2022-12-16) +## v4.3.1 (2022-12-22) **go version** @@ -8,7 +8,7 @@ - feat: run xctest before start ios automation - feat: run step with specified loop times - feat: add options for FindTexts -- feat: capture pcap file for iOS, including CLI `hrp ios pcap` and option `uixt.WithIOSPcapOn(true)` +- feat: capture pcap file for iOS, including CLI `hrp ios pcap` and option `uixt.WithIOSPcapOptions(...)` - feat: add performance monitor for iOS, including CLI `hrp ios perf` and options `uixt.WithIOSPerfOptions(...)` - refactor: move all UI APIs to uixt pkg - docs: add examples for UI APIs diff --git a/examples/worldcup/main_test.go b/examples/worldcup/main_test.go index 5166a79b..03fa9e9c 100644 --- a/examples/worldcup/main_test.go +++ b/examples/worldcup/main_test.go @@ -64,7 +64,12 @@ func TestIOSDouyinWorldCupLive(t *testing.T) { uixt.WithIOSPerfNetwork(true), // uixt.WithIOSPerfBundleID("com.ss.iphone.ugc.Aweme"), ), - uixt.WithIOSPcapOn(true), + uixt.WithIOSPcapOptions( + // uixt.WithIOSPcapAll(true), + // uixt.WithIOSPcapPID(1234), + // uixt.WithIOSPcapProcName("Awe"), + uixt.WithIOSPcapBundleID("com.ss.iphone.ugc.Aweme"), + ), ), TestSteps: []hrp.IStep{ hrp.NewStep("启动抖音"). diff --git a/hrp/cmd/ios/pcap.go b/hrp/cmd/ios/pcap.go index 73b5042f..4535483e 100644 --- a/hrp/cmd/ios/pcap.go +++ b/hrp/cmd/ios/pcap.go @@ -18,8 +18,23 @@ var pcapCmd = &cobra.Command{ Use: "pcap", Short: "capture ios network packets", RunE: func(cmd *cobra.Command, args []string) error { + pcapOptions := []uixt.IOSPcapOption{} + if pid > 0 { + pcapOptions = append(pcapOptions, uixt.WithIOSPcapPID(pid)) + } + if procName != "" { + pcapOptions = append(pcapOptions, uixt.WithIOSPcapProcName(procName)) + } + if bundleID != "" { + pcapOptions = append(pcapOptions, uixt.WithIOSPcapBundleID(bundleID)) + } + if len(pcapOptions) == 0 { + pcapOptions = append(pcapOptions, uixt.WithIOSPcapAll(true)) + } + device, err := uixt.NewIOSDevice( uixt.WithUDID(udid), + uixt.WithIOSPcapOptions(pcapOptions...), ) if err != nil { log.Fatal().Err(err).Msg("failed to init ios device") @@ -50,10 +65,17 @@ var pcapCmd = &cobra.Command{ }, } -var timeDuration int +var ( + timeDuration int + pid int + procName string +) func init() { pcapCmd.Flags().StringVarP(&udid, "udid", "u", "", "specify device by udid") + pcapCmd.Flags().IntVarP(&pid, "pid", "p", 0, "specify process ID") + pcapCmd.Flags().StringVarP(&procName, "procName", "n", "", "specify process name") + pcapCmd.Flags().StringVarP(&bundleID, "bundleID", "b", "", "specify bundle ID") pcapCmd.Flags().IntVarP(&timeDuration, "duration", "t", 10, "specify time duraion in seconds") iosRootCmd.AddCommand(pcapCmd) } diff --git a/hrp/pkg/gidevice/device.go b/hrp/pkg/gidevice/device.go index 3ebbe844..64b088d9 100644 --- a/hrp/pkg/gidevice/device.go +++ b/hrp/pkg/gidevice/device.go @@ -10,6 +10,7 @@ import ( "strings" "time" + "github.com/rs/zerolog/log" uuid "github.com/satori/go.uuid" "howett.net/plist" @@ -549,12 +550,38 @@ func (d *device) GetInterfaceOrientation() (orientation libimobiledevice.Orienta return } -func (d *device) PcapStart() (lines <-chan []byte, err error) { +func (d *device) PcapStart(opts ...PcapOption) (lines <-chan []byte, err error) { + pcapOptions := &PcapOptions{} + for _, fn := range opts { + fn(pcapOptions) + } + log.Info().Interface("options", pcapOptions).Msg("pcap start") + + // wait until get pid for bundle id + if pcapOptions.BundleID != "" { + instruments, err := d.newInstrumentsService() + if err != nil { + fmt.Printf("get pid by bundle id failed: %v\n", err) + os.Exit(1) + } + + for { + pid, err := instruments.getPidByBundleID(pcapOptions.BundleID) + if err != nil { + time.Sleep(1 * time.Second) + continue + } + pcapOptions.Pid = pid + break + } + } + if d.pcapd == nil { if _, err = d.lockdownService(); err != nil { return nil, err } - if d.pcapd, err = d.lockdown.PcapdService(); err != nil { + if d.pcapd, err = d.lockdown.PcapdService( + pcapOptions.Pid, pcapOptions.ProcName); err != nil { return nil, err } } diff --git a/hrp/pkg/gidevice/idevice.go b/hrp/pkg/gidevice/idevice.go index dfff07ae..0160b78e 100644 --- a/hrp/pkg/gidevice/idevice.go +++ b/hrp/pkg/gidevice/idevice.go @@ -61,7 +61,7 @@ type Device interface { Syslog() (lines <-chan string, err error) SyslogStop() - PcapStart() (packet <-chan []byte, err error) + PcapStart(opts ...PcapOption) (packet <-chan []byte, err error) PcapStop() Reboot() error diff --git a/hrp/pkg/gidevice/lockdown.go b/hrp/pkg/gidevice/lockdown.go index 2f2d80d7..790d5ed9 100644 --- a/hrp/pkg/gidevice/lockdown.go +++ b/hrp/pkg/gidevice/lockdown.go @@ -449,12 +449,12 @@ func (c *lockdown) SyslogRelayService() (syslogRelay SyslogRelay, err error) { return } -func (c *lockdown) PcapdService() (pcapd Pcapd, err error) { +func (c *lockdown) PcapdService(targetPID int, targetProcName string) (pcapd Pcapd, err error) { var innerConn InnerConn if innerConn, err = c._startService(libimobiledevice.PcapdServiceName, nil); err != nil { return nil, err } - pcapdClient := libimobiledevice.NewPcapdClient(innerConn) + pcapdClient := libimobiledevice.NewPcapdClient(innerConn, targetPID, targetProcName) return newPcapdClient(pcapdClient), nil } diff --git a/hrp/pkg/gidevice/pcapd.go b/hrp/pkg/gidevice/pcapd.go index 6c1c7fcd..4291fd1d 100644 --- a/hrp/pkg/gidevice/pcapd.go +++ b/hrp/pkg/gidevice/pcapd.go @@ -6,6 +6,39 @@ import ( "github.com/httprunner/httprunner/v4/hrp/pkg/gidevice/pkg/libimobiledevice" ) +type PcapOptions struct { + All bool // capture all packets + Pid int // capture packets from specific PID + ProcName string // capture packets from specific process name + BundleID string // convert to PID first, then capture packets from the PID +} + +type PcapOption func(*PcapOptions) + +func WithPcapAll(all bool) PcapOption { + return func(opt *PcapOptions) { + opt.All = all + } +} + +func WithPcapProcName(procName string) PcapOption { + return func(opt *PcapOptions) { + opt.ProcName = procName + } +} + +func WithPcapPID(pid int) PcapOption { + return func(opt *PcapOptions) { + opt.Pid = pid + } +} + +func WithPcapBundleID(bundleID string) PcapOption { + return func(opt *PcapOptions) { + opt.BundleID = bundleID + } +} + type pcapdClient struct { stop chan struct{} c *libimobiledevice.PcapdClient @@ -38,6 +71,10 @@ func (c *pcapdClient) Packet() <-chan []byte { close(packetCh) return } + if raw == nil { + // filtered packet + continue + } res, err := c.c.CreatePacket(raw) if err != nil { log.Println("failed to create packet") diff --git a/hrp/pkg/gidevice/pcapd_test.go b/hrp/pkg/gidevice/pcapd_test.go new file mode 100644 index 00000000..eca4efe6 --- /dev/null +++ b/hrp/pkg/gidevice/pcapd_test.go @@ -0,0 +1,66 @@ +//go:build localtest + +package gidevice + +import ( + "fmt" + "testing" + "time" +) + +func TestPcapWithPID(t *testing.T) { + setupLockdownSrv(t) + + data, err := dev.PcapStart(WithPcapPID(1234)) + if err != nil { + t.Fatal(err) + } + timer := time.NewTimer(time.Duration(time.Second * 10)) + for { + select { + case <-timer.C: + dev.PcapStop() + return + case d := <-data: + fmt.Println(string(d)) + } + } +} + +func TestPcapWithProcName(t *testing.T) { + setupLockdownSrv(t) + + data, err := dev.PcapStart(WithPcapProcName("Awe")) + if err != nil { + t.Fatal(err) + } + timer := time.NewTimer(time.Duration(time.Second * 10)) + for { + select { + case <-timer.C: + dev.PcapStop() + return + case d := <-data: + fmt.Println(string(d)) + } + } +} + +func TestPcapWithBundleID(t *testing.T) { + setupLockdownSrv(t) + + data, err := dev.PcapStart(WithPcapBundleID("com.ss.iphone.ugc.Aweme")) + if err != nil { + t.Fatal(err) + } + timer := time.NewTimer(time.Duration(time.Second * 10)) + for { + select { + case <-timer.C: + dev.PcapStop() + return + case d := <-data: + fmt.Println(string(d)) + } + } +} diff --git a/hrp/pkg/gidevice/perfd_test.go b/hrp/pkg/gidevice/perfd_test.go index 646d829f..d3f5b683 100644 --- a/hrp/pkg/gidevice/perfd_test.go +++ b/hrp/pkg/gidevice/perfd_test.go @@ -159,22 +159,3 @@ func TestPerfAll(t *testing.T) { } } } - -func TestPcap(t *testing.T) { - setupLockdownSrv(t) - - data, err := dev.PcapStart() - if err != nil { - t.Fatal(err) - } - timer := time.NewTimer(time.Duration(time.Second * 10)) - for { - select { - case <-timer.C: - dev.PcapStop() - return - case d := <-data: - fmt.Println(string(d)) - } - } -} diff --git a/hrp/pkg/gidevice/pkg/libimobiledevice/pcapd.go b/hrp/pkg/gidevice/pkg/libimobiledevice/pcapd.go index aba2896e..51ce59ff 100644 --- a/hrp/pkg/gidevice/pkg/libimobiledevice/pcapd.go +++ b/hrp/pkg/gidevice/pkg/libimobiledevice/pcapd.go @@ -4,7 +4,9 @@ import ( "bytes" "encoding/binary" "fmt" + "io" "io/ioutil" + "strings" "time" "github.com/lunixbochs/struc" @@ -12,8 +14,23 @@ import ( const PcapdServiceName = "com.apple.pcapd" -func NewPcapdClient(innerConn InnerConn) *PcapdClient { +func filterPacket(pid int, procName string) func(*IOSPacketHeader) bool { + return func(iph *IOSPacketHeader) bool { + if pid > 0 { + return iph.Pid == int32(pid) || + iph.Pid2 == int32(pid) + } + if procName != "" { + return strings.HasPrefix(iph.ProcName, procName) || + strings.HasPrefix(iph.ProcName2, procName) + } + return true + } +} + +func NewPcapdClient(innerConn InnerConn, targetPID int, targetProcName string) *PcapdClient { return &PcapdClient{ + filter: filterPacket(targetPID, targetProcName), client: newServicePacketClient(innerConn), } } @@ -48,6 +65,11 @@ func (c *PcapdClient) ReceivePacket() (respPkt Packet, err error) { return } +const ( + PacketHeaderSize = uint32(95) +) + +// ref: https://github.com/danielpaulus/go-ios/blob/fc943b9d236571f9775f5c593e3d49bb5bd67afd/ios/pcap/pcap.go#L27 type IOSPacketHeader struct { HdrSize uint32 `struc:"uint32,big"` Version uint8 `struc:"uint8,big"` @@ -78,12 +100,24 @@ func (c *PcapdClient) GetPacket(buf []byte) ([]byte, error) { } } + // support ios 15 beta4 + if iph.HdrSize > PacketHeaderSize { + buf := make([]byte, iph.HdrSize-PacketHeaderSize) + _, err := io.ReadFull(preader, buf) + if err != nil { + return []byte{}, err + } + } + packet, err := ioutil.ReadAll(preader) if err != nil { return packet, err } if iph.FramePreLength == 0 { - ext := []byte{0xbe, 0xfe, 0xbe, 0xfe, 0xbe, 0xfe, 0xbe, 0xfe, 0xbe, 0xfe, 0xbe, 0xfe, 0x08, 0x00} + ext := []byte{ + 0xbe, 0xfe, 0xbe, 0xfe, 0xbe, 0xfe, 0xbe, 0xfe, + 0xbe, 0xfe, 0xbe, 0xfe, 0x08, 0x00, + } return append(ext, packet...), nil } return packet, nil diff --git a/hrp/pkg/uixt/ios_device.go b/hrp/pkg/uixt/ios_device.go index 50260c47..9d301b7b 100644 --- a/hrp/pkg/uixt/ios_device.go +++ b/hrp/pkg/uixt/ios_device.go @@ -66,6 +66,15 @@ var ( WithIOSPerfSystemAttributes = gidevice.WithPerfSystemAttributes ) +type IOSPcapOption = gidevice.PcapOption + +var ( + WithIOSPcapAll = gidevice.WithPcapAll + WithIOSPcapPID = gidevice.WithPcapPID + WithIOSPcapProcName = gidevice.WithPcapProcName + WithIOSPcapBundleID = gidevice.WithPcapBundleID +) + type IOSDeviceOption func(*IOSDevice) func WithUDID(udid string) IOSDeviceOption { @@ -131,9 +140,12 @@ func WithIOSPerfOptions(options ...gidevice.PerfOption) IOSDeviceOption { } } -func WithIOSPcapOn(pcapOn bool) IOSDeviceOption { +func WithIOSPcapOptions(options ...gidevice.PcapOption) IOSDeviceOption { return func(device *IOSDevice) { - device.PcapOn = pcapOn + device.PcapOptions = &gidevice.PcapOptions{} + for _, option := range options { + option(device.PcapOptions) + } } } @@ -183,8 +195,8 @@ func GetIOSDeviceOptions(dev *IOSDevice) (deviceOptions []IOSDeviceOption) { if dev.PerfOptions != nil { deviceOptions = append(deviceOptions, WithIOSPerfOptions(dev.perfOpitons()...)) } - if dev.PcapOn { - deviceOptions = append(deviceOptions, WithIOSPcapOn(true)) + if dev.PcapOptions != nil { + deviceOptions = append(deviceOptions, WithIOSPcapOptions(dev.pcapOpitons()...)) } if dev.XCTestBundleID != "" { deviceOptions = append(deviceOptions, WithXCTest(dev.XCTestBundleID)) @@ -249,11 +261,11 @@ func NewIOSDevice(options ...IOSDeviceOption) (device *IOSDevice, err error) { type IOSDevice struct { d gidevice.Device PerfOptions *gidevice.PerfOptions `json:"perf_options,omitempty" yaml:"perf_options,omitempty"` + PcapOptions *gidevice.PcapOptions `json:"pcap_options,omitempty" yaml:"pcap_options,omitempty"` UDID string `json:"udid,omitempty" yaml:"udid,omitempty"` Port int `json:"port,omitempty" yaml:"port,omitempty"` // WDA remote port MjpegPort int `json:"mjpeg_port,omitempty" yaml:"mjpeg_port,omitempty"` // WDA remote MJPEG port LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"` - PcapOn bool `json:"pcap_on,omitempty" yaml:"pcap_on,omitempty"` XCTestBundleID string `json:"xctest_bundle_id,omitempty" yaml:"xctest_bundle_id,omitempty"` // switch to iOS springboard before init WDA session @@ -334,7 +346,7 @@ func (dev *IOSDevice) NewDriver(capabilities Capabilities) (driverExt *DriverExt } } - if dev.PcapOn { + if dev.PcapOptions != nil { if err := dev.StartPcap(); err != nil { return nil, err } @@ -390,7 +402,7 @@ func (dev *IOSDevice) StopPerf() string { func (dev *IOSDevice) StartPcap() error { log.Info().Msg("start packet capture") - packets, err := dev.d.PcapStart() + packets, err := dev.d.PcapStart(dev.pcapOpitons()...) if err != nil { return err } @@ -536,6 +548,27 @@ func (dev *IOSDevice) perfOpitons() (perfOptions []gidevice.PerfOption) { return } +func (dev *IOSDevice) pcapOpitons() (pcapOptions []gidevice.PcapOption) { + if dev.PcapOptions == nil { + return + } + + if dev.PcapOptions.All { + pcapOptions = append(pcapOptions, gidevice.WithPcapAll(true)) + } + if dev.PcapOptions.Pid > 0 { + pcapOptions = append(pcapOptions, gidevice.WithPcapPID(dev.PcapOptions.Pid)) + } + if dev.PcapOptions.ProcName != "" { + pcapOptions = append(pcapOptions, gidevice.WithPcapProcName(dev.PcapOptions.ProcName)) + } + if dev.PcapOptions.BundleID != "" { + pcapOptions = append(pcapOptions, gidevice.WithPcapBundleID(dev.PcapOptions.BundleID)) + } + + return +} + // NewHTTPDriver creates new remote HTTP client, this will also start a new session. func (dev *IOSDevice) NewHTTPDriver(capabilities Capabilities) (driver WebDriver, err error) { var localPort int diff --git a/hrp/pkg/uixt/swipe.go b/hrp/pkg/uixt/swipe.go index b1f8f6d0..ddb919fb 100644 --- a/hrp/pkg/uixt/swipe.go +++ b/hrp/pkg/uixt/swipe.go @@ -99,7 +99,7 @@ func (dExt *DriverExt) SwipeUntil(direction interface{}, findCondition Action, f time.Sleep(time.Duration(1000*interval) * time.Millisecond) } return errors.Wrap(code.OCRTextNotFoundError, - fmt.Sprintf("swipe %s %d times, match condition failed", direction, maxRetryTimes)) + fmt.Sprintf("swipe %v %d times, match condition failed", direction, maxRetryTimes)) } func (dExt *DriverExt) LoopUntil(findAction, findCondition, foundAction Action, options ...DataOption) error {