├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── bin ├── .gitkeep └── bins.json ├── cache └── .gitkeep ├── check-versions.pl ├── config.json.example ├── coordinator ├── .gitignore ├── Makefile ├── app │ └── Info.plist ├── config.go ├── coordinator.go ├── firewall.go ├── go.mod ├── heartbeat.go ├── http_server.go ├── idevice.go ├── launchctl.go ├── log.go ├── network.go ├── periodic.go ├── ports.go ├── proc_backoff.go ├── proc_device_trigger.go ├── proc_device_unit.go ├── proc_generic.go ├── proc_h264_to_jpeg.go ├── proc_ios_video_pull.go ├── proc_ios_video_stream.go ├── proc_ivf.go ├── proc_stf_provider.go ├── proc_video_enabler.go ├── proc_vnc_proxy.go ├── proc_wdaproxy.go ├── shutdown.go ├── stf_control.go ├── video_app.go ├── vpn.go ├── wda.go └── zmq.go ├── get-version-info.sh ├── get-wda-build-path.sh ├── init.sh ├── logs └── .gitkeep ├── makefile_preflight.pl ├── offline └── Makefile ├── run ├── runner ├── Makefile ├── gencert.pl ├── go.mod ├── go.sum ├── http_server.go ├── pass_check.go ├── proc_backoff.go ├── runner.go ├── runner.json ├── runnercert.tmpl ├── shutdown.go ├── update.go └── versions.go ├── server ├── .env ├── README.md ├── cert │ ├── gencert.sh │ ├── server.conf │ ├── server.crt │ └── server.key ├── docker-compose.yml ├── nginx │ ├── Dockerfile │ ├── entrypoint.sh │ └── nginx.conf ├── runcli.js └── storage-temp │ └── Dockerfile ├── stf_ios_support.rb ├── tblick-info.sh ├── temp └── .gitkeep ├── update_server ├── Makefile ├── go.mod ├── main.go └── updates │ ├── index.html │ ├── remote.pl │ ├── updates.json │ └── v1.json ├── util ├── alf2.pl ├── brewser.pl ├── signers.pl └── tcc.pl ├── version.json ├── video_pipes └── .gitkeep ├── view_log.go └── wda_wrapper ├── Makefile ├── go.mod └── wda_wrapper.go /.gitignore: -------------------------------------------------------------------------------- 1 | 10 2 | .DS_Store 3 | repos 4 | bin 5 | offline 6 | config.json 7 | logs 8 | view_log 9 | wda_wrapper/go.sum 10 | temp 11 | dist.tgz 12 | cache/* 13 | runner/*.crt 14 | runner/*.key 15 | runner/runnercert.conf 16 | runner/runner 17 | update_server/server 18 | **/*~ 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 David Helkowski 2 | Free Use Anti-Corruption License 3 | https://faircoding.com/license 4 | 5 | ======================================================================= 6 | 7 | The content of this project is licensed for free conditional use by LICENSEE 8 | as defined below. The conditional aspect is detailed in THE RESTRICTION below. 9 | 10 | A DIRECT RESTRICTED ENTITY is defined to be any of the following: 11 | - Any company present on the current online list: https://faircoding.com/license/list 12 | - Accenture https://en.wikipedia.org/wiki/Accenture 13 | - Amazon https://en.wikipedia.org/wiki/Amazon_(company) 14 | - Apple 15 | - AptEdge https://aptedge.io 16 | - Avalara https://www.avalara.com 17 | - Baltimore Sun 18 | - BCG https://en.wikipedia.org/wiki/Boston_Consulting_Group 19 | - BrowserStack https://www.browserstack.com 20 | - The Canton Group https://cantongroup.com 21 | - Comcast 22 | - Cruise LLC https://en.wikipedia.org/wiki/Cruise_(autonomous_vehicle) 23 | - Disney 24 | - Ebay 25 | - Epic Games 26 | - EQT Partners https://en.wikipedia.org/wiki/EQT_Partners 27 | - Equifax 28 | - Experian 29 | - Extrahop https://www.extrahop.com 30 | - Facebook 31 | - FBI https://www.fbi.gov 32 | - Fox Entertainment Group 33 | - Google 34 | - Headspin https://www.headspin.io 35 | - IBM 36 | - Insight Global https://www.insightglobal.com 37 | - Jamf https://www.jamf.com 38 | - Kobiton https://www.kobiton.com 39 | - Logic 20/20 https://www.logic2020.com 40 | - Micro Focus https://en.wikipedia.org/wiki/Micro_Focus 41 | - MIT https://www.mit.edu 42 | - NBC 43 | - Nintendo https://en.wikipedia.org/wiki/Nintendo 44 | - Oracle Corporation https://en.wikipedia.org/wiki/Oracle_Corporation 45 | - Palantir 46 | - Perfecto https://www.perfecto.io 47 | - ProKarma https://pkglobal.com 48 | - Reddit https://reddit.com 49 | - Sauce Labs https://saucelabs.com 50 | - Sinclair https://en.wikipedia.org/wiki/Sinclair_Broadcast_Group 51 | - Smartbear https://smartbear.com 52 | - Sony Corporation https://en.wikipedia.org/wiki/Sony 53 | - Steam https://steampowered.com 54 | - Systems Alliance https://www.systemsalliance.com 55 | - SUSE https://en.wikipedia.org/wiki/SUSE 56 | - TEKsystems https://www.teksystems.com 57 | - Tesla https://www.tesla.com 58 | - TransUnion 59 | - T. Rowe Price https://en.wikipedia.org/wiki/T._Rowe_Price 60 | - UMB https://www.umaryland.edu 61 | - UMBC https://umbc.edu 62 | - UMCP https://www.umd.edu 63 | - Verizon 64 | - Wells Fargo 65 | - Ycombinator https://www.ycombinator.com 66 | - Zulily 67 | 68 | The links given are to clarify which entities exactly are meant. Should 69 | those links become invalid see the online list to find the latest updated 70 | website for the company/group. 71 | 72 | The online list may be updated at any time. If a company is added, causing 73 | a party to become a RESTRICTED ENTITY, then that party remains free to use 74 | versions of THE CONTENT created prior to becoming a RESTRICTED ENTITY under 75 | the license terms previous to the change. If this is a concern, it is suggested 76 | that LICENSEE fork THE CONTENT and opt to make use of the option to remove 77 | the online list described below in condition point 6. 78 | 79 | Changes made to THE CONTENT of any sort after the date of addition will be 80 | forbidden from use in that case, and those new changes are only licensed under 81 | the new license terms with the updated DIRECT RESTRICTED ENTITY list. 82 | 83 | A RESTRICTED ENTITY is defined to be any of the following: 84 | 1. A DIRECT RESTRICTED ENTITY 85 | 2. An owner of a RESTRICTED ENTITY 86 | 3. A subsidiary or any functional business unit of a RESTRICTED ENTITY 87 | 4. Any entity of which 50% or more of its annual revenue comes from work done 88 | for a RESTRICTED ENTITY 89 | 5. An entity that broke off from a RESTRICTED ENTITY ( such as a company 90 | splitting up into different legal entities ) 91 | 6. An international counterpart of a RESTRICTED ENTITY. 92 | 7. An employee of a RESTRICTED ENTITY 93 | 8. A contractor delivering work to a RESTRICTED ENTITY at more than 20hrs per week. 94 | 95 | A PARTIALLY RESTRICTED ENTITY is defined to be any of the following: 96 | 1. A contractor delivering work to a RESTRICTED ENTITY at less than 20hrs per week. 97 | 2. A person or legal entity acting under command, request, or legal contract 98 | by/with a RESTRICTED ENTITY. 99 | 100 | Should a PARTIALLY RESTRICTED ENTITY fall under any of the points defining them 101 | as A RESTRICTED ENTITY, they are not a PARTIALLY RESTRICTED ENTITY. 102 | 103 | A PARTIALLY RESTRICTED ENTITY shall be considered a RESTRICTED ENTITY to the extent 104 | that their usage of THE CONTENT is related to commands, requests, or legal contract 105 | with a RESTRICTED ENTITY. That is, A PARTIALLY RESTRICTED ENTITY cannot utilize 106 | THE CONTENT in any way in relation to a RESTRICTED ENTITY. 107 | 108 | A PARTIALLY RESTRICTED ENTITY remains able to be a LICENSEE of the content, to the 109 | extent that all usage and access to THE CONTENT remains within the license terms. 110 | That is, a PARTIALLY RESTRICTED ENTITY may utilize THE CONTENT in work related to 111 | other LICENSEE. They remain bound by THE RESTRICTION, prevented from enabling use 112 | of THE CONTENT by any RESTRICTED ENTITY. 113 | 114 | LICENSEE refers to any party ( person or legal entity ) who/that is not a 115 | RESTRICTED ENTITY and accepts this license by making use of THE CONTENT or 116 | utilizing any of the permissions provided by the license. 117 | 118 | THE CONTENT refers to the content in full, any portion of it, and anything 119 | derived from it. 120 | 121 | LICENSEE agrees not to provide THE CONTENT to any RESTRICTED ENTITY, nor to 122 | allow THE CONTENT to be used in any way by any RESTRICTED ENTITY. 123 | 124 | THE RESTRICTION is defined to be this license in full, specifically to 125 | refer to the way the license restricts use of THE CONTENT in any way by 126 | any RESTRICTED ENTITY. 127 | 128 | THE RESTRICTION shall apply to all projects with Copyright 129 | ownership by any RESTRICTED ENTITY. This shall remain true even if the 130 | project contributions themselves are done by a LICENSEE. LICENSEE 131 | are forbidden from contributing THE CONTENT to a project Copyrighted by 132 | any RESTRICTED ENTITY. 133 | 134 | Should any portion of this license be found to be unenforceable, the remaining 135 | portion of the license shall continue to be in effect, preserving the intent. 136 | The intent is to prevent any DIRECT RESTRICTED ENTITY from benefiting from 137 | THE CONTENT in any way. 138 | 139 | Should it be found or enacted into law that using such a restriction list is 140 | illegal as a whole, then the entire license shall dissolve and zero permissions 141 | given to anyone by the broken license. 142 | 143 | ======================================================================= 144 | 145 | Permission is hereby granted, free of charge, to LICENSEE, to conditionally 146 | use, modify, publish, distribute, or sell copies of THE CONTENT. 147 | 148 | These permissions are granted with the following conditions: 149 | 150 | 1. THE CONTENT may not be distributed or sold to any RESTRICTED ENTITY. 151 | 152 | 2. Access to a service containing or utilizing THE CONTENT may not be 153 | given, sold, or licensed to any RESTRICTED ENTITY. 154 | 155 | 3. Any service containing or utilizing THE CONTENT shall be considered a 156 | derivative of this license and therefore by bound by THE RESTRICTION. 157 | This includes use of THE CONTENT by way of dynamic libraries, indirect calls, 158 | scripting, virtualization, containerization, or instantiation as a remote 159 | callable service. Such restricted software must be prevented from being 160 | used by any RESTRICTED ENTITY. 161 | 162 | 4. THE CONTENT cannot be sold to any RESTRICTED ENTITY. 163 | 164 | 5. THE CONTENT may not be published to or distributed through a system 165 | owned or controlled by any RESTRICTED ENTITY. 166 | 167 | 6. Any derivatives of THE CONTENT must be licensed under this same license. 168 | By same license this means the contents of this LICENSE file in its 169 | entirety. The license "duplicate" may have the online restricted list removed, 170 | effectively fixing the DIRECT RESTRICTED ENTITY from that point onward for 171 | that derivative. At the moment in time of that removal, the online list must 172 | be checked, and any new DIRECT RESTRICTED ENTITY present in the online list 173 | added to the new updated fixed list in the license file. Besides that single 174 | permitted modification, the LICENSE may not be modified in any other way. 175 | 176 | 7. Any derivatives of THE CONTENT must be publicly released within 2 months 177 | of the change or discarded. 178 | 179 | 8. The above copyright notice and this LICENSE file must be included in 180 | all copies and derivative forms of THE CONTENT. 181 | 182 | 9. LICENSEE understands and agrees that this content is provided "as is", 183 | without warranty of any kind, express or implied, including but not limited 184 | to the warranties of merchantability, fitness for a particular purpose and 185 | noninfringement. In no event shall the authors or copyright holders be liable 186 | for any claim, damages or other liability, whether in an action of contract, 187 | tort or otherwise, arising from, out of or in connection with THE CONTENT or 188 | the use or other dealings of it. 189 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## STF IOS Support 2 | 3 | ### Prerequisites 4 | 1. A machine running MacOS ( to build and run the "provider" ) 5 | 1. A machine running Linux with Docker container support ( to run the STF server ) 6 | 7 | ### Build machine setup 8 | 1. Clone this repo down to your build machine 9 | 1. Install XCode 10 | 1. Add your developer Apple ID to XCode 11 | 12 | 1. XCode -> XCode menu -> Preferences -> Accounts Tab 13 | 1. Click `+` under `Apple IDs` list 14 | 1. Choose `Apple ID` 15 | 1. Login to your account 16 | 1. Download a "Apple Development certificate" for your user 17 | 18 | 1. Continue from previous step, right after logging into your Developer account in Xcode 19 | 1. Select `Manage Certificates` 20 | 1. Click `+` in the lower left corner 21 | 1. Select `Apple Development` 22 | 1. Clone the various needed repos ( includes WebDriverAgent ) 23 | 24 | 1. Run `make clone` 25 | 1. Configure WebDriverAgent to use your identity for signing 26 | 27 | 1. Open `repos/WebDriverAgent/WebDriverAgent.xcodeproj` in XCode 28 | 1. Select the WebDriverAgentLib target 29 | 1. Go to the `Signing & Capabilities` tab 30 | 1. Select your team under `Team` 31 | 1. Select the WebDriverAgentRunner target 32 | 1. Go to the `Signing & Capabilities` tab 33 | 1. Select your team under `Team` 34 | 1. Run `./init.sh` 35 | 36 | ### Deploy server side: 37 | 1. On your Linux machine 38 | 1. Copy `server` folder to your Linux machine 39 | 1. Run `server/cert/gencert.sh` to generate a self-signed cert ( or use your own ) 40 | 1. Note! Plain http STF server is not supported. It can be done, but it shouldn't and tickets to make it so will be closed. 41 | 1. Update `server/.env` to reflect the IP and hostname for your server 42 | 1. Start STF 43 | 44 | 1. docker-compose up 45 | 46 | ### Using a standard OpenSTF server: 47 | 1. Setup your server as normal following upstream instructions 48 | 1. Create an SSL certificate for your server using the method you desire. 49 | 1. Configure the OpenSTF server for SSL 50 | 1. Alter stf_ios_support/coordinator/proc_stf_provider --connect-sub and --connect-push lines to match your server config 51 | 52 | ### Build provider files: 53 | 1. Copy the first {} block from `config.json.example` into `config.json`. Do not include any comment lines starting with // 54 | 1. Update config.json 55 | 56 | 1. Update `xcode_dev_team_id` to be the OU of your developer account. If you add your account into Xcode first, you can then run 57 | `make ou` to display what the OU is. You can also find it by opening the keychain, selecting the Apple Development certificate 58 | for your account, and then looking at what the `Organization Unit` is. 59 | 1. Update `root_path` to be where provider code should be installed, such as `/Users/user/stf` 60 | 1. Update `config_path` to match that, such as `/Users/user/stf/config.json` 61 | 1. Run `make` then `make dist` 62 | 63 | 1. dist.tgz will be created 64 | 65 | ### Deploy provider setup: 66 | 1. Copy `dist.tgz` from build machine 67 | 1. Run `tar -xf dist.tgz` 68 | 1. Tweak `config.json` as desired 69 | 70 | ### Starting provider 71 | 1. Register(provision) your IOS device to your developer account as a developer device. 72 | 73 | 1. Use the API -or- 74 | 75 | 1. Follow https://developer.apple.com/documentation/appstoreconnectapi/creating_api_keys_for_app_store_connect_api to create 76 | an app store connect API key. Give it Developer access. 77 | 1. Gain a session using JSON Web Tokens 78 | 1. Create a provisioning profile if none exist using profiles: https://developer.apple.com/documentation/appstoreconnectapi/profiles 79 | See also https://github.com/cidertool/asc-go/blob/f08b8151f7fd92ff54924480338dafbf8a383255/asc/provisioning_profiles.go 80 | 1. Post to the devices endpoint to register a device: https://developer.apple.com/documentation/appstoreconnectapi/devices 81 | See also https://github.com/cidertool/asc-go/blob/f08b8151f7fd92ff54924480338dafbf8a383255/asc/provisioning_devices.go 82 | 1. Follow these instructions: https://www.telerik.com/blogs/how-to-add-ios-devices-to-your-developer-profile 83 | I couldn't find updated instructions on Apple's website. If you find them please let me know so I can link to them. 84 | 1. Plug your IOS device in 85 | 1. Pair it with your system 86 | 87 | 1. Run `idevicepair pair` 88 | 1. Accept pairing on IOS device screen 89 | 1. Have Xcode setup the "developer image" on your IOS device: 90 | 91 | 1. Open Xcode 92 | 1. Go to Windows... Devices and Simulators 93 | 1. Wait while Developer Image is installed to your phone 94 | 1. Run `./bin/ios_video_pull -devices -decimal` to determine the PID ( product ID ) of your IOS device in decimal 95 | 1. Run `./bin/devreset [decimal product ID] 1452` to reset the video streaming status of your IOS device 96 | 1. Run `./run` ( and leave it running ) 97 | 1. Permissions dialog boxes appear for coordinator to listen on various ports; select accept for all of them 98 | 1. Device shows up in STF with video and can be controlled. Yay 99 | 100 | ### Using runner 101 | Runner is a command that runs coordinator in a loop and also enables installation/update of a distribution. 102 | Runner is not necessary to use stf_ios_support. It is provided to make it easier to remotely maintain a cluster 103 | of providers. 104 | To use it: 105 | 1. Run `make` to build all the things 106 | 1. Run `runner/runner -pass [some password]` to generate crypted password of your choice 107 | 1. Adjust `runner/runner.json` 108 | 109 | 1. Update user password with the crypted output of previous step 110 | 1. Update user to something else ( default user/pass are both replaceme ) 111 | 1. Update update_server to be host/IP address of the server you will use to run update_server 112 | 1. Update updates path to be path on a provider machine where you want downloaded updates to be saved/cached 113 | 1. Update install_dir path to be the path where you want `coordinator` to be installed 114 | 1. Update config path to be a path where `config.json` for `coordinator` will be located on provider machine 115 | 1. Rerun `make` to rebuild `runner.tgz` 116 | 1. Run `make updatedist` to build `update_server.tgz` 117 | 1. Copy `update_server.tgz` to a server 118 | 1. Extract it 119 | 120 | 1. `tar -xf update_server` 121 | 1. Run it and leave it running 122 | 123 | 1. `update_server/server` 124 | 1. Copy `runner.tgz` to a provider machine 125 | 1. Extract it 126 | 127 | 1. `tar -xf runner.tgz` 128 | 1. Stop any instance you may be running of `coordinator` already on the provider 129 | 1. Run it in a visual GUI MacOS session 130 | 1. Go to `https://[provider ip/host]:8021` in your browser 131 | 1. Accept the self-signed cert ( or make your own non-self signed cert and adjust updaet_server config ) 132 | 1. Click the update button to download/install/start `coordinator` on the provider 133 | 134 | ### Known Issues 135 | 1. libimobiledevice won't install properly right now from brew 136 | 137 | 1. `./init.sh` 138 | 1. `make libimd` ( init.sh actually already runs this ) 139 | 1. Video streaming will sometimes be left in a "stuck" state 140 | 141 | 1. ios_video_pull sub-process of coordinator depends on quicktime_video_hack upstream repo/library. That library 142 | does not properly "stop" itself if you start and then stop reading video from an IOS device. As a result, if 143 | you run coordinator, stop it, then start it again, it won't be able to start up again correctly. 144 | 1. To fix this you can use devreset. This is why the devreset command is mentioned above currently to run before 145 | starting coordinator. devreset effectively stops the video streaming entirely, resetting it so that it can 146 | be started up again. 147 | 148 | ### Setting up with VPN 149 | 1. Install openvpn-server on your STF server machine 150 | 1. Create client certificate(s) using your favorite process... 151 | 1. Create ovpn file(s) with those client certs 152 | 1. Deploy those cert(s) to your provider machines; setting them up in Tunnelblick 153 | 1. Alter config.json on each provider to have the name of the cert setup in Tunnelblick 154 | 1. Start openvpn server on STF server 155 | 1. Start coordinator/provider on each provider machine 156 | 157 | ### Handling video not working 158 | 1. Run `./view_log proc ios_video_pull` to check for errors fetching h264 data from the IOS device 159 | 1. Run `./view_log -proc ios_video_stream` to check for errors streaming jpegs via websocket to browser 160 | 1. Reboot your IOS device and try again 161 | 162 | ### Debugging 163 | 1. run `./view_log` to see list of things that log 164 | 1. run `./view_log -proc [one from list]` 165 | 166 | ### FAQ 167 | See https://github.com/devicefarmer/stf_ios_support/wiki/FAQ -------------------------------------------------------------------------------- /bin/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dryark/stf_ios_support/2f32cbbf1506a740eb21a496c1cdc11d875558f7/bin/.gitkeep -------------------------------------------------------------------------------- /bin/bins.json: -------------------------------------------------------------------------------- 1 | { 2 | "bins": [ 3 | { 4 | "short": "coordinator", 5 | "name": "Coordinator", 6 | "cmd": "coordinator -version" 7 | }, 8 | { 9 | "short": "ivf_pull", 10 | "name": "IOS AVF Pull", 11 | "cmd": "ivf_pull version" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /cache/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dryark/stf_ios_support/2f32cbbf1506a740eb21a496c1cdc11d875558f7/cache/.gitkeep -------------------------------------------------------------------------------- /check-versions.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | use strict; 3 | use Data::Dumper; 4 | 5 | my $main = `git log -1 --date=unix`; 6 | my $mainT = 0; 7 | if( $main =~ m/Date:\s+([0-9]+)/ ) { 8 | $mainT = $1; 9 | } 10 | my $arg = $ARGV[0]; 11 | if( -e "temp/check-ok-$mainT" && ( !$arg || $arg ne 'force' ) ) { 12 | print "Versions already checked; skipping version check\n"; 13 | exit; 14 | } 15 | 16 | my $versions = `./get-version-info.sh --unix --wdasource`; 17 | open( my $vfile, ">temp/current_versions.json" ); 18 | print $vfile $versions; 19 | close( $vfile ); 20 | $versions =~ s/:/=>/g; 21 | $versions =~ s/"/'/g; 22 | 23 | my $ob = eval( $versions ); 24 | 25 | my $have_issues = 0; 26 | my $reqs = { 27 | # h264_to_jpeg is no longer the primary video mechanism 28 | # h264_to_jpeg => { min => 1588831486 }, 29 | 30 | ios_video_stream => { min => 1597980831, message => "Then run `make cleanivs` them `make`" }, 31 | wdaproxy => { min => 1594408035, message => "Then run `make cleanwdaproxy` then `make`" }, 32 | wda => { min => 1596738353, message => "Then run `make cleanwda` them `make`", name => 'WebDriverAgent' }, 33 | ios_avf_pull => { min => 1597380907 }, 34 | stf => { min => 1597980993, name => 'stf-ios-provider' }, 35 | device_trigger => { min => 1578609558, name => 'osx_ios_device_trigger' } 36 | }; 37 | for my $name ( keys %$reqs ) { 38 | my $repo = $ob->{ $name }; 39 | if( !$repo ) { 40 | $have_issues = 1; 41 | print "repos/$name is missing\n"; 42 | next; 43 | } 44 | my $error = $repo->{error}; 45 | if( $error ) { 46 | $have_issues = 1; 47 | print "$name; error: $error\n"; 48 | next; 49 | } 50 | my $remote = $repo->{remote}; 51 | my $date = $repo->{date}; 52 | my $dirname = $repo->{name} || $name; 53 | $remote =~ s/=>/:/; 54 | my $req = $reqs->{ $name }; 55 | if( $req->{ min } ) { 56 | my $min = $req->{ min }; 57 | if( $date < $min ) { 58 | my $msg = $req->{ message } || ''; 59 | print STDERR "repos/$name is out of date. Please git pull it. $msg\n"; 60 | $have_issues = 1; 61 | } 62 | } 63 | } 64 | if( !$have_issues ) { 65 | open( my $fh, ">temp/check-ok-$mainT" ); 66 | print $fh 1; 67 | close( $fh ); 68 | } -------------------------------------------------------------------------------- /config.json.example: -------------------------------------------------------------------------------- 1 | // Minimal Example; DO NOT COPY THIS LINE OR ANY LINE STARTING WITH // 2 | // To get your xcode dev org, view "Apple Development" cert in keychain, and look at "Organizational Unit" 3 | { 4 | "xcode_dev_team_id": "[your xcode developer org; ~10 char alphanumeric]", 5 | "stf": { 6 | "ip": "[your stf server ip]", 7 | "hostname": "[your stf server hostname]" 8 | }, 9 | "video": { 10 | "enabled": true, 11 | "use_vnc": true, 12 | "vnc_scale": 2, 13 | "vnc_password": "", 14 | "frame_rate": 10 15 | }, 16 | "install": { 17 | "root_path": "[desired stf provider install folder]", 18 | "config_path": "[desired stf provider install folder]/config.json", 19 | "set_working_dir": false 20 | } 21 | } 22 | 23 | // HERE AND BELOW IS TO SHOW DEFAULTS AND ALL OPTIONS 24 | // DO NOT COPY THIS INTO YOUR config.json FILE 25 | // Defaults 26 | { 27 | "wda_folder": "./bin/wda", 28 | "xcode_dev_team_id": "", 29 | "network": { 30 | "coordinator_port": 8027, 31 | "video": "8000-8005", 32 | "dev_ios": "9240-9250", 33 | "vnc": "5901-5911", 34 | "wda": "8100-8105", 35 | "interface": "auto" 36 | }, 37 | "stf": { 38 | "ip": "", 39 | "hostname": "" 40 | }, 41 | "video": { 42 | "enabled": true, 43 | "use_vnc": false, 44 | "vnc_scale": 2, 45 | "vnc_password": "", 46 | "frame_rate": 5 47 | }, 48 | "install": { 49 | "root_path": "", 50 | "config_path": "", 51 | "set_working_dir": false 52 | }, 53 | "log": { 54 | "main": "logs/coordinator", 55 | "proc_lines": "logs/procs", 56 | "wda_wrapper_stdout": "./logs/wda_wrapper_stdout", 57 | "wda_wrapper_stderr": "./logs/wda_wrapper_stderr" 58 | }, 59 | "vpn": { 60 | "type": "none", 61 | "ovpn_working_dir": "/usr/local/etc/openvpn", 62 | "tblick_name": "" 63 | }, 64 | "bin_paths": { 65 | "wdaproxy": "bin/wdaproxy", 66 | "device_trigger": "bin/osx_ios_device_trigger", 67 | "video_enabler": "bin/osx_ios_video_enabler", 68 | "mirrorfeed": "bin/mirrorfeed", 69 | "openvpn": "/usr/local/opt/openvpn/sbin/openvpn", 70 | "iproxy": "/usr/local/bin/iproxy", 71 | "wdawrapper": "bin/wda_wrapper", 72 | "ffmpeg": "bin/ffmpeg" 73 | }, 74 | "repos": { 75 | "stf": "https://github.com/nanoscopic/stf-ios-provider.git", 76 | "wda": "https://github.com/appium/WebDriverAgent.git" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /coordinator/.gitignore: -------------------------------------------------------------------------------- 1 | go.mod 2 | go.sum 3 | -------------------------------------------------------------------------------- /coordinator/Makefile: -------------------------------------------------------------------------------- 1 | TARGET = ../bin/coordinator 2 | 3 | all: $(TARGET) 4 | 5 | coordinator_sources := $(wildcard *.go) 6 | 7 | $(TARGET): $(coordinator_sources) go.sum 8 | go build -o $(TARGET) -ldflags "-X main.GitCommit=$(GIT_COMMIT) -X main.GitDate=$(GIT_DATE) -X main.GitRemote=$(GIT_REMOTE) -X main.EasyVersion=$(EASY_VERSION)" . 9 | 10 | go.sum: 11 | go get 12 | go get . 13 | 14 | clean: 15 | $(RM) $(TARGET) go.sum -------------------------------------------------------------------------------- /coordinator/app/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleName 6 | STF Coordinator 7 | 8 | CFBundleExecutable 9 | coordinator 10 | 11 | CFBundleIconFile 12 | icon.icns 13 | 14 | CFBundleIdentifier 15 | com.tmobile.coordinator 16 | 17 | NSHighResolutionCapable 18 | 19 | 20 | LSUIElement 21 | 22 | 23 | CFBundleInfoDictionaryVersion 24 | 6.0 25 | 26 | CFBundlePackageType 27 | APPL 28 | 29 | -------------------------------------------------------------------------------- /coordinator/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | 9 | log "github.com/sirupsen/logrus" 10 | uj "github.com/nanoscopic/ujsonin/mod" 11 | ) 12 | 13 | type Config struct { 14 | WdaFolder string `json:"wda_folder"` 15 | Network NetConfig `json:"network"` 16 | Stf STFConfig `json:"stf"` 17 | Video VideoConfig `json:"video"` 18 | FrameServer FrameServerConfig `json:"frameserver"` 19 | Install InstallConfig `json:"install"` 20 | Log LogConfig `json:"log"` 21 | BinPaths BinPathConfig `json:"bin_paths"` 22 | Vpn VPNConfig `json:"vpn"` 23 | Timing TimingConfig `json:"timing"` 24 | ConfigPath string `json:"config_path"` 25 | DeviceDetector string `json:"device_detector"` 26 | IosCLI string `json:"ios_cli"` 27 | // The following are only used internally 28 | WDAProxyPort int 29 | MirrorFeedPort int 30 | DevIosPort int 31 | VncPort int 32 | UsbmuxdPort int 33 | DecodeInPort int 34 | DecodeOutPort int 35 | ujson * uj.JNode 36 | } 37 | 38 | type NetConfig struct { 39 | Coordinator int `json:"coordinator_port"` 40 | Mirrorfeed int `json:"mirrorfeed_port"` 41 | Video string `json:"video_ports"` 42 | DevIos string `json:"dev_ios_ports"` 43 | Vnc string `json:"vnc_ports"` 44 | Wda string `json:"proxy_ports"` 45 | Decode string `json:"decode_ports"` 46 | Usbmuxd string `json:"usbmuxd_ports"` 47 | Iface string `json:"interface"` 48 | } 49 | 50 | type STFConfig struct { 51 | Ip string `json:"ip"` 52 | HostName string `json:"hostname"` 53 | Location string `json:"location"` 54 | AdminToken string `json:"admin_token"` 55 | } 56 | 57 | type VideoConfig struct { 58 | Enabled bool `json:"enabled"` 59 | Method string `json:"method"` 60 | UseVnc bool `json:"use_vnc"` 61 | VncScale int `json:"vnc_scale"` 62 | VncPassword string `json:"vnc_password"` 63 | FrameRate int `json:"frame_rate"` 64 | } 65 | 66 | type InstallConfig struct { 67 | RootPath string `json:"root_path"` 68 | ConfigPath string `json:"config_path"` 69 | SetWorkingDir bool `json:"set_working_dir"` 70 | } 71 | 72 | type LogConfig struct { 73 | Main string `json:"main"` 74 | MainApp string `json:"main_app"` 75 | ProcLines string `json:"proc_lines"` 76 | WDAWrapperStdout string `json:"wda_wrapper_stdout"` 77 | WDAWrapperStderr string `json:"wda_wrapper_stderr"` 78 | OpenVPN string `json:"openvpn"` 79 | } 80 | 81 | type BinPathConfig struct { 82 | WdaProxy string `json:"wdaproxy"` 83 | DeviceTrigger string `json:"device_trigger"` 84 | IosVideoStream string `json:"ios_video_stream"` 85 | IosVideoPull string `json:"ios_video_pull"` 86 | H264ToJpeg string `json:"h264_to_jpeg"` 87 | Openvpn string `json:"openvpn"` 88 | Iproxy string `json:"iproxy"` 89 | WdaWrapper string `json:"wdawrapper"` 90 | IVF string `json:"ivf"` 91 | VideoEnabler string `json:"video_enabler"` 92 | IosDeploy string `json:"ios-deploy"` 93 | } 94 | 95 | type VPNConfig struct { 96 | VpnType string `json:"type"` 97 | TblickName string `json:"tblick_name"` 98 | OvpnWd string `json:"ovpn_working_dir"` 99 | OvpnConfig string `json:"ovpn_config"` 100 | } 101 | 102 | type FrameServerConfig struct { 103 | Secure bool `json:"secure"` 104 | Cert string `json:"cert"` 105 | Key string `json:"key"` 106 | Width int `json:"width"` 107 | Height int `json:"height"` 108 | } 109 | 110 | type TimingConfig struct { 111 | WdaRestart int `json:"wda_restart"` 112 | } 113 | 114 | type DeviceConfig struct { 115 | Width int 116 | Height int 117 | } 118 | 119 | func get_device_config( config *Config, udid string ) ( *DeviceConfig ) { 120 | dev := DeviceConfig{} 121 | 122 | devs := config.ujson.Get("devices") 123 | if devs == nil { 124 | return nil 125 | } 126 | 127 | /*devs.ForEach( func( conf *uj.JNode ) { 128 | oneid := conf.Get("udid").String() 129 | if oneid == udid { 130 | dev.Width = conf.Get("width").Int() 131 | dev.Height = conf.Get("height").Int() 132 | } 133 | } )*/ 134 | dev.Width = 735 135 | dev.Height = 1134 136 | 137 | return &dev 138 | } 139 | 140 | func read_config( configPath string ) *Config { 141 | var config Config 142 | 143 | for { 144 | fh, serr := os.Stat( configPath ) 145 | if serr != nil { 146 | log.WithFields( log.Fields{ 147 | "type": "err_read_config", 148 | "error": serr, 149 | "config_path": configPath, 150 | } ).Fatal("Could not read specified config path") 151 | } 152 | 153 | var configFile string 154 | switch mode := fh.Mode(); { 155 | case mode.IsDir(): 156 | configFile = fmt.Sprintf("%s/config.json", configPath) 157 | case mode.IsRegular(): 158 | configFile = configPath 159 | } 160 | 161 | configFh, err := os.Open( configFile ) 162 | if err != nil { 163 | log.WithFields( log.Fields{ 164 | "type": "err_read_config", 165 | "config_file": configFile, 166 | "error": err, 167 | } ).Fatal("failed reading config file") 168 | } 169 | defer configFh.Close() 170 | 171 | jsonBytes, _ := ioutil.ReadAll( configFh ) 172 | 173 | defaultJson := `{ 174 | "wda_folder": "./bin/wda", 175 | "device_detector": "api", 176 | "ios_cli": "ios-deploy", 177 | "xcode_dev_team_id": "", 178 | "network": { 179 | "coordinator_port": 8027, 180 | "video_ports": "8000-8005", 181 | "dev_ios_ports": "9240-9250", 182 | "vnc_ports": "5901-5911", 183 | "proxy_ports": "8100-8105", 184 | "decode_ports": "7878-7888", 185 | "usbmuxd_ports": "9920-9930", 186 | "interface": "auto" 187 | }, 188 | "stf":{ 189 | "ip": "", 190 | "hostname": "", 191 | "location": "", 192 | "admin_token": "" 193 | }, 194 | "video":{ 195 | "enabled": true, 196 | "method": "avfoundation", 197 | "use_vnc": false, 198 | "vnc_scale": 2, 199 | "vnc_password": "", 200 | "frame_rate": 5, 201 | "app_name": "vidtest2", 202 | "app_bundle_id": "com.dryark.vidtest2" 203 | }, 204 | "frameserver":{ 205 | "secure": false, 206 | "cert": "", 207 | "key": "", 208 | "width": 0, 209 | "height": 0 210 | }, 211 | "install":{ 212 | "root_path": "", 213 | "set_working_dir": false, 214 | "config_path": "" 215 | }, 216 | "log":{ 217 | "main": "logs/coordinator", 218 | "main_app": "logs/app", 219 | "proc_lines": "logs/procs", 220 | "wda_wrapper_stdout": "./logs/wda_wrapper_stdout", 221 | "wda_wrapper_stderr": "./logs/wda_wrapper_stderr", 222 | "openvpn": "logs/openvpn.log" 223 | }, 224 | "vpn":{ 225 | "type": "none", 226 | "ovpn_working_dir": "/usr/local/etc/openvpn", 227 | "tblick_name": "" 228 | }, 229 | "bin_paths":{ 230 | "wdaproxy": "bin/wdaproxy", 231 | "device_trigger": "bin/osx_ios_device_trigger", 232 | "openvpn": "/usr/local/opt/openvpn/sbin/openvpn", 233 | "iproxy": "/usr/local/bin/iproxy", 234 | "wdawrapper": "bin/wda_wrapper", 235 | "ios_video_stream":"bin/ios_video_stream", 236 | "ios_video_pull":"bin/ios_video_pull", 237 | "h264_to_jpeg": "bin/decode", 238 | "ivf": "bin/ivf_pull", 239 | "video_enabler": "bin/video_enabler", 240 | "ios-deploy": "bin/ios-deploy" 241 | }, 242 | "repos":{ 243 | "stf": "https://github.com/nanoscopic/stf-ios-provider.git", 244 | "wda": "https://github.com/nanoscopic/WebDriverAgent.git" 245 | }, 246 | "timing":{ 247 | "wda_restart": 240 248 | }, 249 | "devices":[ 250 | ] 251 | }` 252 | 253 | config = Config{ 254 | MirrorFeedPort: 8000, 255 | WDAProxyPort: 8100, 256 | DevIosPort: 9240, 257 | VncPort: 5901, 258 | DecodeOutPort: 7878, 259 | DecodeInPort: 7879, 260 | UsbmuxdPort: 9920, 261 | } 262 | 263 | err = json.Unmarshal( []byte( defaultJson ), &config ) 264 | if err != nil { 265 | log.Fatal( "1 ", err ) 266 | } 267 | 268 | err = json.Unmarshal( jsonBytes, &config ) 269 | if err != nil { 270 | log.Fatal( "2 ", err ) 271 | } 272 | 273 | config.ujson, _ = uj.Parse( jsonBytes ) 274 | 275 | //jsonCombined, _ := json.MarshalIndent(config, "", " ") 276 | //fmt.Printf("Combined config:%s\n", string( jsonCombined ) ) 277 | //os.Exit(0) 278 | 279 | if config.ConfigPath != "" { 280 | configPath = config.ConfigPath 281 | continue 282 | } 283 | break 284 | } 285 | return &config 286 | } -------------------------------------------------------------------------------- /coordinator/firewall.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "strings" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | func firewall_ensureperm( findBin string ) { 12 | hasPerm := firewall_hasperm( findBin ) 13 | if hasPerm { 14 | log.Warn("App already has firewall permissions: ", findBin ) 15 | return 16 | } 17 | firewall_stop() 18 | firewall_addperm( findBin ) 19 | firewall_start() 20 | } 21 | 22 | func firewall_hasperm( findBin string ) (bool) { 23 | curBins := firewall_getperms() 24 | for _, bin := range curBins { 25 | if bin == findBin { 26 | return true 27 | } 28 | } 29 | return false 30 | } 31 | 32 | func firewall_addperm( binary string ) { 33 | cmd := exec.Command( "/usr/libexec/ApplicationFirewall/socketfilterfw", "--add", binary ) 34 | cmd.Stderr = os.Stderr 35 | cmd.Stdout = os.Stdout 36 | err := cmd.Run() 37 | if err != nil { 38 | log.Fatal( err ) 39 | } 40 | } 41 | 42 | func firewall_stop() { 43 | cmd := exec.Command( "/usr/libexec/ApplicationFirewall/socketfilterfw", "--setglobalstate", "off" ) 44 | cmd.Stderr = os.Stderr 45 | cmd.Stdout = os.Stdout 46 | err := cmd.Run() 47 | if err != nil { 48 | log.Fatal( err ) 49 | } 50 | } 51 | 52 | func firewall_start() { 53 | cmd := exec.Command( "/usr/libexec/ApplicationFirewall/socketfilterfw", "--setglobalstate", "on" ) 54 | cmd.Stderr = os.Stderr 55 | cmd.Stdout = os.Stdout 56 | err := cmd.Run() 57 | if err != nil { 58 | log.Fatal( err ) 59 | } 60 | } 61 | 62 | func firewall_delperm( binary string ) { 63 | cmd := exec.Command( "/usr/libexec/ApplicationFirewall/socketfilterfw", "--remove", binary ) 64 | cmd.Stderr = os.Stderr 65 | cmd.Stdout = os.Stdout 66 | err := cmd.Run() 67 | if err != nil { 68 | log.Fatal( err ) 69 | } 70 | } 71 | 72 | func firewall_showperms() { 73 | bytes, _ := exec.Command( "/usr/libexec/ApplicationFirewall/socketfilterfw", "--listapps" ).Output() 74 | fmt.Printf( string( bytes ) ) 75 | } 76 | 77 | func firewall_getperms() ( [] string ) { 78 | bytes, _ := exec.Command( "/usr/libexec/ApplicationFirewall/socketfilterfw", "--listapps" ).Output() 79 | 80 | lines := strings.Split( string(bytes), "\n" ) 81 | 82 | var apps []string 83 | app := "" 84 | for _, line := range lines { 85 | colonPos := strings.Index( line, ":" ) 86 | if colonPos != -1 { 87 | app = line[ colonPos + 3 : len( line ) - 1 ] 88 | } else { 89 | allowPos := strings.Index( line, "( Allow" ) 90 | if allowPos != -1 { 91 | apps = append( apps, app ) 92 | } 93 | } 94 | } 95 | log.Debug( "Cureent apps with permissions", apps ) 96 | return apps 97 | } -------------------------------------------------------------------------------- /coordinator/go.mod: -------------------------------------------------------------------------------- 1 | module coordinator.go 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/elastic/go-sysinfo v1.4.0 7 | github.com/fsnotify/fsnotify v1.4.7 8 | github.com/go-cmd/cmd v1.2.0 9 | github.com/jviney/go-proc v0.2.0 10 | github.com/nanoscopic/ujsonin v1.9.0 11 | github.com/sirupsen/logrus v1.4.2 12 | github.com/zeromq/goczmq v4.1.0+incompatible 13 | ) 14 | -------------------------------------------------------------------------------- /coordinator/heartbeat.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | //log "github.com/sirupsen/logrus" 6 | ) 7 | 8 | func coro_heartbeat( uuid string, pubEventCh chan<- PubEvent ) ( chan<- bool ) { 9 | count := 1 10 | stopChannel := make(chan bool) 11 | 12 | // Start heartbeat 13 | go func() { 14 | done := false 15 | for { 16 | select { 17 | case <-stopChannel: 18 | done = true 19 | default: 20 | } 21 | if done { 22 | break 23 | } 24 | 25 | if count >= 2 { 26 | count = 0 27 | 28 | beatEvent := PubEvent{} 29 | beatEvent.action = 2 30 | beatEvent.uuid = uuid 31 | beatEvent.name = "" 32 | beatEvent.wdaPort = 0 33 | beatEvent.vidPort = 0 34 | pubEventCh <- beatEvent 35 | 36 | /*log.WithFields( log.Fields{ 37 | "type": "heartbeat", 38 | } ).Info("Heartbeat")*/ 39 | } 40 | time.Sleep( time.Second * 5 ) 41 | count++; 42 | } 43 | }() 44 | 45 | return stopChannel 46 | } -------------------------------------------------------------------------------- /coordinator/idevice.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os/exec" 5 | "strings" 6 | "time" 7 | log "github.com/sirupsen/logrus" 8 | uj "github.com/nanoscopic/ujsonin/mod" 9 | ) 10 | 11 | func getDeviceName( config *Config, uuid string ) (string) { 12 | i := 0 13 | var nameStr string 14 | for { 15 | i++ 16 | if i > 10 { return "" } 17 | 18 | var name []byte 19 | if config.IosCLI == "ios-deploy" { 20 | name, _ = exec.Command( config.BinPaths.IosDeploy, "-i", uuid, "-g", "DeviceName" ).Output() 21 | } else { 22 | name, _ = exec.Command( "/usr/local/bin/idevicename", "-u", uuid ).Output() 23 | } 24 | if name == nil || len(name) == 0 { 25 | log.WithFields( log.Fields{ 26 | "type": "ilib_getname_fail", 27 | "uuid": uuid, 28 | "try": i, 29 | } ).Debug("idevicename returned nothing") 30 | 31 | time.Sleep( time.Millisecond * 100 ) 32 | continue 33 | } 34 | nameStr = string( name ) 35 | break 36 | } 37 | nameStr = nameStr[:len(nameStr)-1] 38 | return nameStr 39 | } 40 | 41 | func getAllDeviceInfo( config *Config, uuid string ) map[string] string { 42 | info := make( map[string] string ) 43 | 44 | if config.IosCLI == "ios-deploy" { 45 | mainKeys := "DeviceName,EthernetAddress,ModelNumber,HardwareModel,PhoneNumber,ProductType,ProductVersion,UniqueDeviceID,InternationalCircuitCardIdentity,InternationalMobileEquipmentIdentity,InternationalMobileSubscriberIdentity" 46 | keyArr := strings.Split( mainKeys, "," ) 47 | output, _ := exec.Command( config.BinPaths.IosDeploy, "-j", "-i", uuid, "-g", mainKeys ).Output() 48 | root, _ := uj.Parse( output ) 49 | for _, key := range keyArr { 50 | node := root.Get( key ) 51 | if node != nil { 52 | info[ key ] = node.String() 53 | } 54 | } 55 | return info 56 | } 57 | 58 | rawInfo := getDeviceInfo( config, uuid, "" ) 59 | lines := strings.Split( rawInfo, "\n" ) 60 | 61 | for _, line := range lines { 62 | char1 := line[0:1] 63 | if char1 == " " { continue } 64 | colonPos := strings.Index( line, ":" ) 65 | key := line[0:colonPos] 66 | val := line[(colonPos+2):] 67 | info[ key ] = val 68 | } 69 | return info 70 | } 71 | 72 | func getDeviceInfo( config *Config, uuid string, keyName string ) (string) { 73 | i := 0 74 | var nameStr string 75 | for { 76 | i++ 77 | if i > 20 { 78 | log.WithFields( log.Fields{ 79 | "type": "ilib_getinfo_fail", 80 | "uuid": uuid, 81 | "key": keyName, 82 | "try": i, 83 | } ).Error("ideviceinfo failed after 20 attempts over 20 seconds") 84 | return "" 85 | } 86 | 87 | ops := []string{} 88 | if uuid != "" { 89 | ops = append( ops, "-u", uuid ) 90 | } 91 | if keyName != "" { 92 | ops = append( ops, "-k", keyName ) 93 | } 94 | 95 | log.WithFields( log.Fields{ 96 | "type": "ilib_getinfo_call", 97 | "ops": ops, 98 | } ).Info("ideviceinfo call") 99 | 100 | var name []byte 101 | 102 | if config.IosCLI == "ios-deploy" { 103 | if( keyName == "" ) { 104 | keyName = "DeviceName,EthernetAddress,ModelNumber,HardwareModel,PhoneNumber,ProductType,ProductVersion,UniqueDeviceID,InternationalCircuitCardIdentity,InternationalMobileEquipmentIdentity,InternationalMobileSubscriberIdentity" 105 | } 106 | name, _ = exec.Command( config.BinPaths.IosDeploy, "-i", uuid, "-g", keyName ).Output() 107 | } else { 108 | name, _ = exec.Command( "/usr/local/bin/ideviceinfo", ops... ).Output() 109 | } 110 | 111 | if name == nil || len(name) == 0 { 112 | log.WithFields( log.Fields{ 113 | "type": "ilib_getinfo_fail", 114 | "uuid": uuid, 115 | "key": keyName, 116 | "try": i, 117 | } ).Debug("ideviceinfo returned nothing") 118 | 119 | time.Sleep( time.Millisecond * 1000 ) 120 | continue 121 | } 122 | nameStr = string( name ) 123 | break 124 | } 125 | nameStr = nameStr[:len(nameStr)-1] 126 | return nameStr 127 | } 128 | 129 | func getFirstDeviceId( config *Config ) ( string ) { 130 | deviceIds := getDeviceIds( config ) 131 | return deviceIds[0] 132 | } 133 | 134 | func getDeviceIds( config *Config ) ( []string ) { 135 | if config.IosCLI == "ios-deploy" { 136 | ids := []string{} 137 | jsonText, _ := exec.Command( config.BinPaths.IosDeploy, "-d", "-j", "-t", "1" ).Output() 138 | root, _ := uj.Parse( []byte( "[" + string(jsonText) + "]" ) ) 139 | 140 | root.ForEach( func( evNode *uj.JNode ) { 141 | ev := evNode.Get("Event").String() 142 | if ev == "DeviceDetected" { 143 | dev := evNode.Get("Device") 144 | ids = append( ids, dev.Get("DeviceIdentifier").String() ) 145 | } 146 | } ) 147 | return ids 148 | } 149 | output, _ := exec.Command( "/usr/local/bin/idevice_id", "-l" ).Output() 150 | lines := strings.Split( string(output), "\n" ) 151 | return lines 152 | } -------------------------------------------------------------------------------- /coordinator/launchctl.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "os/exec" 9 | "os/user" 10 | "strconv" 11 | "strings" 12 | "sync" 13 | "text/template" 14 | log "github.com/sirupsen/logrus" 15 | //ps "github.com/jviney/go-proc" 16 | ) 17 | 18 | type Launcher struct { 19 | label string 20 | arguments []string 21 | keepalive bool 22 | stdout string 23 | stderr string 24 | cwd string 25 | file string 26 | asRoot bool 27 | lock sync.Mutex 28 | } 29 | 30 | func NewLauncher( label string, arguments []string, keepalive bool, cwd string, asRoot bool ) (*Launcher) { 31 | file := label // strings.ReplaceAll( label, ".", "_" ) 32 | user, _ := user.Current() 33 | if asRoot == true { 34 | file = fmt.Sprintf("/Library/LaunchDaemons/%s.plist", file) 35 | } else { 36 | file = fmt.Sprintf("%s/Library/LaunchAgents/%s.plist", user.HomeDir, file) 37 | } 38 | //strings.Replace 39 | launcher := Launcher{ 40 | label: label, 41 | arguments: arguments, 42 | keepalive: keepalive, 43 | stdout: "/dev/null", 44 | stderr: "/dev/null", 45 | cwd: cwd, 46 | file: file, 47 | asRoot: asRoot, 48 | } 49 | return &launcher 50 | } 51 | 52 | func ( self *Launcher ) pid() (pid int) { 53 | //user, _ := user.Current() 54 | pid = 0 55 | 56 | //log.WithFields( log.Fields{ "type": "blah", "asroot": self.asRoot, "user": user.Username } ).Info("fdfsdfds") 57 | 58 | if self.asRoot { //&& user.Username != "root" { 59 | // trying to find information on a root owned plist, but not running as root 60 | // cannot use launchctl as a result :( 61 | 62 | fullCmdLine := strings.Join( self.arguments, " " ) 63 | 64 | // This code doesn't work. Why? Who knows. Apparently go-proc can't retrieve processes run by root??? 65 | /*procs := ps.GetAllProcessesInfo() 66 | for _, proc := range procs { 67 | testCmd := proc.Command + " " + strings.Join( proc.CommandLine, " " ) 68 | //log.WithFields( log.Fields{ "type": "proc", "proc": testCmd } ).Info("proc") 69 | if proc.Pid == 15454 { //strings.Contains( testCmd, "openvpn" ) { 70 | log.WithFields( log.Fields{ "type": "testeq", "find": fullCmdLine, "have": testCmd } ).Info("testeq") 71 | } 72 | if testCmd == fullCmdLine { 73 | return proc.Pid 74 | } 75 | }*/ 76 | log.WithFields( log.Fields{ "type": "launch_ps_scan", "cmdLine": fullCmdLine } ). 77 | Debug( "Scanning ps -Af for Cmd" ) 78 | 79 | cmd := exec.Command("/bin/ps","-Af") 80 | output, _ := cmd.Output() 81 | 82 | lines := strings.Split( string( output ), "\n" ) 83 | for _, line := range lines { 84 | parts := strings.Split( line, " " ) 85 | var nodup [] string 86 | for _, part := range parts { 87 | if part != "" { 88 | nodup = append( nodup, part ) 89 | } 90 | } 91 | line = strings.Join( nodup, " " ) 92 | 93 | if strings.Contains( line, fullCmdLine ) { 94 | sp1 := strings.Index( line, " " ) 95 | rest := line[ sp1: ] 96 | sp2 := strings.Index( rest, " " ) 97 | rest = rest[ sp2 + 1 : ] 98 | sp3 := strings.Index( rest, " " ) 99 | pid := rest[ 0: sp3 - 1 ] 100 | pidNum, _ := strconv.Atoi( pid ) 101 | return pidNum 102 | } 103 | } 104 | } else { 105 | output, _ := exec.Command(fmt.Sprintf("/bin/launchctl list %s", self.label)).Output() 106 | lines := strings.Split( string(output), "\n" ) 107 | 108 | log.WithFields( log.Fields{ 109 | "type": "launch_ctl_list", 110 | "label": self.label, 111 | "output": output, 112 | } ).Debug( "Fetching launchctl info to get PID" ) 113 | 114 | for _, line := range lines { 115 | if strings.Contains( line, "\"PID\"" ) { 116 | pos := strings.Index( line, "\"PID\"" ) 117 | pos = pos + 7 118 | 119 | val := line[pos:len(line)-2] 120 | pid, _ = strconv.Atoi( val ) 121 | } 122 | } 123 | } 124 | return pid 125 | } 126 | 127 | func ( self *Launcher ) load() { 128 | // unload the service if it is already loaded 129 | pid := self.pid() 130 | if pid != 0 { 131 | self.unload() 132 | } 133 | 134 | argx := "" 135 | for _, arg := range self.arguments { 136 | argx += fmt.Sprintf("%s", arg) 137 | } 138 | 139 | keepaliveX := "" 140 | if self.keepalive { 141 | keepaliveX = "" 142 | } 143 | 144 | limitSession := "" 145 | if self.asRoot != true { 146 | limitSession = "LimitLoadToSessionType\n Aqua\n" 147 | } 148 | 149 | var data bytes.Buffer 150 | launchTpl.Execute( &data, map[string] string { 151 | "label": self.label, 152 | "arguments": argx, 153 | "keepalive": keepaliveX, 154 | "stdout": self.stdout, 155 | "stderr": self.stderr, 156 | "cwd": self.cwd, 157 | "limitSession": limitSession, 158 | } ) 159 | 160 | // create / recreate the plist file 161 | log.WithFields( log.Fields{ 162 | "type": "launch_plist_write", 163 | "file": self.file, 164 | } ).Debug("Writing plist file") 165 | err := ioutil.WriteFile( self.file, data.Bytes(), 0600 ) 166 | if err != nil { 167 | log.WithFields( log.Fields{ 168 | "type": "launch_err", 169 | "file": self.file, 170 | "error": err, 171 | } ).Error("Error writing plist file") 172 | } 173 | 174 | // load it 175 | log.WithFields( log.Fields{ 176 | "type": "launch_plist_load", 177 | "file": self.file, 178 | } ).Debug("Loading LaunchAgent") 179 | self.lock.Lock() 180 | c := exec.Command("/bin/launchctl","load",self.file) 181 | c.Stdout = os.Stdout 182 | c.Stderr = os.Stderr 183 | c.Run() 184 | self.lock.Unlock() 185 | } 186 | 187 | func ( self *Launcher ) unload() { 188 | // unload 189 | self.lock.Lock() 190 | exec.Command("/bin/launchctl","unload",self.file).Run() 191 | 192 | // delete the file 193 | os.Remove(self.file) 194 | self.lock.Unlock() 195 | } 196 | 197 | var launchTpl = template.Must(template.New("launchfile").Parse(` 198 | 199 | 200 | 201 | 202 | Label 203 | {{.label}} 204 | 205 | ProgramArguments 206 | 207 | {{.arguments}} 208 | 209 | 210 | KeepAlive 211 | {{.keepalive}} 212 | 213 | StandardOutPath 214 | {{.stdout}} 215 | 216 | StandardErrorPath 217 | {{.stderr}} 218 | 219 | WorkingDirectory 220 | {{.cwd}} 221 | 222 | {{.limitSession}} 223 | 224 | 225 | `)) -------------------------------------------------------------------------------- /coordinator/log.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | "strings" 9 | "syscall" 10 | "sync" 11 | "container/list" 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | type ProcTracker struct { 16 | que *list.List 17 | length int 18 | } 19 | 20 | type InMemTracker struct { 21 | procTrackers map[string] *ProcTracker 22 | } 23 | 24 | type JSONLog struct { 25 | file *os.File 26 | fileName string 27 | formatter *log.JSONFormatter 28 | failed bool 29 | hupData *HupData 30 | id int 31 | inMemTracker *InMemTracker 32 | mutex sync.Mutex 33 | } 34 | 35 | type HupData struct { 36 | hupA bool 37 | hupB bool 38 | mutex sync.Mutex 39 | } 40 | 41 | func ( hook *JSONLog ) Fire( entry *log.Entry ) error { 42 | // If we have failed to write to the file; don't bother trying 43 | if hook.failed { return nil } 44 | 45 | jsonformat, _ := hook.formatter.Format( entry ) 46 | 47 | fh := hook.file 48 | 49 | doHup := false 50 | hupData := hook.hupData 51 | hupData.mutex.Lock() 52 | if hook.id == 1 { 53 | doHup = hupData.hupA 54 | if doHup { hupData.hupA = false } 55 | } else if hook.id == 2 { 56 | doHup = hupData.hupB 57 | if doHup { hupData.hupB = false } 58 | } 59 | hupData.mutex.Unlock() 60 | 61 | if doHup { 62 | fh.Close() 63 | fhnew, err := os.OpenFile( hook.fileName, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666 ) 64 | if err != nil { 65 | fmt.Fprintf( os.Stderr, "Unable to open file for writing: %v", err ) 66 | fh = nil 67 | } 68 | fh = fhnew 69 | hook.file = fh 70 | 71 | log.WithFields( log.Fields{ 72 | "type": "sighup", 73 | "state": "reopen", 74 | "file": hook.fileName, 75 | } ).Info("HUP requested") 76 | //fmt.Fprintf( os.Stdout, "Hup %s\n", hook.fileName ) 77 | } 78 | 79 | var err error 80 | if entry.Context != nil { 81 | // There is context; this is meant for the lines logfile 82 | str := string( jsonformat ) 83 | str = strings.Replace( str, "\"level\":\"info\",", "", 1 ) 84 | str = strings.Replace( str, "\"msg\":\"\",", "", 1 ) 85 | _, err = fh.WriteString( str ) 86 | 87 | // Possibly better to us a coroutine to accept new log messages 88 | // rather than lock on every one. 89 | hook.mutex.Lock() 90 | hook.inMemTracker.addEntry( entry, str ) 91 | hook.mutex.Unlock() 92 | } else { 93 | _, err = fh.WriteString( string( jsonformat ) ) 94 | } 95 | 96 | if err != nil { 97 | hook.failed = true 98 | fmt.Fprintf( os.Stderr, "Cannot write to logfile: %v", err ) 99 | return err 100 | } 101 | 102 | return nil 103 | } 104 | func (tracker *InMemTracker) addEntry( entry *log.Entry, json string ) { 105 | if proc, ok := entry.Data["proc"]; ok { 106 | procS := proc.(string) 107 | var ok2 bool 108 | var pt *ProcTracker 109 | if pt, ok2 = tracker.procTrackers[ procS ]; !ok2 { 110 | pt = &ProcTracker{ 111 | que: list.New(), 112 | length: 0, 113 | } 114 | tracker.procTrackers[ procS ] = pt 115 | } 116 | 117 | pt.length = pt.length + 1 118 | pt.que.PushBack( json ) 119 | 120 | // Max out at 20 elements per queue 121 | if pt.length > 20 { 122 | e := pt.que.Front() 123 | pt.que.Remove(e) 124 | } 125 | } 126 | } 127 | func (hook *JSONLog) Levels() []log.Level { 128 | return []log.Level{ log.PanicLevel, log.FatalLevel, log.ErrorLevel, log.WarnLevel, log.InfoLevel, log.DebugLevel } 129 | } 130 | func AddJSONLog( logger *log.Logger, fileName string, id int, hupData *HupData ) ( *JSONLog ) { 131 | logFile, err := os.OpenFile( fileName, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666 ) 132 | if err != nil { 133 | fmt.Fprintf( os.Stderr, "Unable to open file for writing: %v", err ) 134 | } 135 | 136 | fileHook := JSONLog{ 137 | file: logFile, 138 | fileName: fileName, 139 | formatter: &log.JSONFormatter{}, 140 | failed: false, 141 | hupData: hupData, 142 | id: id, 143 | inMemTracker: NewInMemTracker(), 144 | } 145 | 146 | if logger == nil { 147 | log.AddHook( &fileHook ) 148 | } else { 149 | logger.AddHook( &fileHook ) 150 | } 151 | return &fileHook 152 | } 153 | func NewInMemTracker() ( *InMemTracker ) { 154 | newt := InMemTracker{ 155 | procTrackers: make( map [string] *ProcTracker ), 156 | } 157 | return &newt 158 | } 159 | type DummyWriter struct { 160 | } 161 | func (self *DummyWriter) Write( p[]byte) (n int, err error) { 162 | return len(p), nil 163 | } 164 | 165 | func setup_log( config *Config, debug bool, jsonLog bool ) (*log.Entry, *InMemTracker) { 166 | if jsonLog { 167 | log.SetFormatter( &log.JSONFormatter{} ) 168 | } 169 | 170 | lineLogger1 := log.New() 171 | dummyWriter := DummyWriter{} 172 | lineLogger1.SetOutput( &dummyWriter ) 173 | lineLogger := lineLogger1.WithContext( context.Background() ) 174 | 175 | if debug { 176 | log.WithFields( log.Fields{ "type": "debug_status" } ).Warn("Debugging enabled") 177 | log.SetLevel( log.DebugLevel ) 178 | lineLogger1.SetLevel( log.DebugLevel ) 179 | } else { 180 | log.SetLevel( log.InfoLevel ) 181 | lineLogger1.SetLevel( log.InfoLevel ) 182 | } 183 | 184 | hupData := coro_sighup() 185 | 186 | AddJSONLog( nil, config.Log.Main, 1, hupData ) 187 | lineJsonLog := AddJSONLog( lineLogger1, config.Log.ProcLines, 2, hupData ) 188 | lineTracker := lineJsonLog.inMemTracker 189 | 190 | return lineLogger, lineTracker 191 | } 192 | 193 | func coro_sighup() ( *HupData ) { 194 | hupData := HupData{ 195 | hupA: false, 196 | hupB: false, 197 | } 198 | c := make(chan os.Signal, 2) 199 | signal.Notify(c, syscall.SIGHUP) 200 | go func() { 201 | for { 202 | <- c 203 | log.WithFields( log.Fields{ 204 | "type": "sighup", 205 | "state": "begun", 206 | } ).Info("HUP requested") 207 | hupData.mutex.Lock() 208 | hupData.hupA = true 209 | hupData.hupB = true 210 | hupData.mutex.Unlock() 211 | } 212 | }() 213 | return &hupData 214 | } -------------------------------------------------------------------------------- /coordinator/network.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "os" 7 | "os/exec" 8 | "strings" 9 | "regexp" 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | func getDefaultIf() ( string ) { 14 | out, err := exec.Command( "/usr/sbin/netstat", "-nr", "-f", "inet" ).Output() 15 | if err != nil { 16 | fmt.Printf("Error from netstat: %s\n", err ) 17 | return "" 18 | } 19 | lines := strings.Split( string(out), "\n" ) 20 | iFace := "" 21 | space := regexp.MustCompile(`\s+`) 22 | 23 | for _, line := range lines { 24 | if strings.Contains( line, "default " ) { 25 | line = space.ReplaceAllString( line, " " ) 26 | 27 | parts := strings.Split( line, " " ) 28 | if parts[0] == "default" { 29 | iFace = parts[3] 30 | } 31 | } 32 | } 33 | return iFace 34 | } 35 | 36 | func ifAddr( ifName string ) ( addrOut string, okay bool ) { 37 | ifaces, err := net.Interfaces() 38 | if err != nil { 39 | fmt.Printf( err.Error() ) 40 | os.Exit( 1 ) 41 | } 42 | 43 | addrOut = "" 44 | for _, iface := range ifaces { 45 | addrs, err := iface.Addrs() 46 | if err != nil { 47 | fmt.Printf( err.Error() ) 48 | os.Exit( 1 ) 49 | } 50 | for _, addr := range addrs { 51 | var ip net.IP 52 | switch v := addr.(type) { 53 | case *net.IPNet: 54 | ip = v.IP 55 | case *net.IPAddr: 56 | ip = v.IP 57 | default: 58 | fmt.Printf("Unknown type\n") 59 | } 60 | if iface.Name == ifName { 61 | str := ip.String() 62 | if !strings.Contains(str,":") { 63 | addrOut = str 64 | } 65 | } 66 | } 67 | } 68 | if addrOut != "" { 69 | return addrOut, true 70 | } 71 | fmt.Printf("Network interface %s not found, exiting\n", ifName ) 72 | return "", false 73 | } 74 | 75 | func get_net_info( config *Config ) ( string, string, bool ) { 76 | var vpnMissing bool = false 77 | 78 | // This information comes from Tunnelblick log 79 | // It may no longer be active 80 | tunName, curIP, err := vpn_info( config ) 81 | 82 | if err != "" { 83 | log.WithFields( log.Fields{ 84 | "type": "vpn_err", 85 | "err": err, 86 | } ).Info( err ) 87 | return "", "", true 88 | } 89 | 90 | log.WithFields( log.Fields{ 91 | "type": "info_vpn", 92 | "interface_name": tunName, 93 | } ).Info("VPN Info - interface") 94 | 95 | ipConfirm := getTunIP( tunName ) 96 | if ipConfirm != curIP { 97 | // The tunnel is no longer active 98 | vpnMissing = true 99 | } else { 100 | log.WithFields( log.Fields{ 101 | "type": "info_vpn", 102 | "interface_name": tunName, 103 | "ip": curIP, 104 | } ).Info("VPN Info - ip") 105 | } 106 | 107 | return tunName, curIP, vpnMissing 108 | } 109 | 110 | func ifaceCurIP( tunName string ) string { 111 | ipStr, _ := ifAddr( tunName ) 112 | if ipStr != "" { 113 | log.WithFields( log.Fields{ 114 | "type": "net_interface_info", 115 | "interface_name": tunName, 116 | "ip": ipStr, 117 | } ).Debug("Interface Details") 118 | } else { 119 | log.WithFields( log.Fields{ 120 | "type": "err_net_interface", 121 | "interface_name": tunName, 122 | } ).Fatal("Could not find interface") 123 | } 124 | 125 | return ipStr 126 | } -------------------------------------------------------------------------------- /coordinator/periodic.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | func do_restart( config *Config, devd *RunningDev ) { 8 | if config.Stf.AdminToken != "" { 9 | stf_reserve( config, devd.uuid ) 10 | } 11 | restart_wdaproxy( devd, false ) 12 | wait_wdaup( devd ) 13 | if config.Stf.AdminToken != "" { 14 | stf_release( config, devd.uuid ) 15 | } 16 | } 17 | 18 | func test_restart_on_release( devd *RunningDev ) { 19 | restart_closure := func() { do_restart( devd.confDup, devd ) } 20 | stf_on_release( restart_closure ) 21 | } 22 | 23 | func periodic_start( config *Config, devd *RunningDev ) { 24 | endChan := devd.periodic 25 | wdaRestartMinutes := config.Timing.WdaRestart 26 | go func() { 27 | minute := 0 28 | stop := false 29 | for { 30 | time.Sleep( time.Minute * 1 ) 31 | minute++ 32 | if wdaRestartMinutes != 0 { 33 | if ( minute % wdaRestartMinutes ) == 0 { // every 4 hours by default 34 | if devd.owner == "" { 35 | do_restart( config, devd ) 36 | } else { 37 | restart_closure := func() { do_restart( config, devd ) } 38 | stf_on_release( restart_closure ) 39 | } 40 | } 41 | } 42 | select { 43 | case <- endChan: 44 | stop = true 45 | break 46 | default: 47 | } 48 | if stop { break } 49 | } 50 | } () 51 | } 52 | 53 | func periodic_stop( devd *RunningDev ) { 54 | endChan := devd.periodic 55 | endChan <- true 56 | } -------------------------------------------------------------------------------- /coordinator/ports.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "sort" 5 | "strconv" 6 | "strings" 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | type PortItem struct { 11 | available bool 12 | } 13 | 14 | type PortMap struct { 15 | wdaPorts map[int] *PortItem 16 | vidPorts map[int] *PortItem 17 | devIosPorts map[int] *PortItem 18 | vncPorts map[int] *PortItem 19 | usbmuxdPorts map[int] *PortItem 20 | decodePorts map[int] *PortItem 21 | } 22 | 23 | func NewPortMap( config *Config ) ( *PortMap ) { 24 | wdaPorts := construct_ports( "WDA", config, config.Network.Wda ) 25 | vidPorts := construct_ports( "Video", config, config.Network.Video ) 26 | devIosPorts := construct_ports( "Dev IOS", config, config.Network.DevIos ) 27 | vncPorts := construct_ports( "VNC", config, config.Network.Vnc ) 28 | decodePorts := construct_ports( "Decode", config, config.Network.Decode ) 29 | usbmuxdPorts := construct_ports( "usbmuxd", config, config.Network.Usbmuxd ) 30 | portMap := PortMap { 31 | wdaPorts: wdaPorts, 32 | vidPorts: vidPorts, 33 | devIosPorts: devIosPorts, 34 | vncPorts: vncPorts, 35 | decodePorts: decodePorts, 36 | usbmuxdPorts: usbmuxdPorts, 37 | } 38 | return &portMap 39 | } 40 | 41 | func construct_ports( name string, config *Config, spec string ) ( map [int] *PortItem ) { 42 | ports := make( map [int] *PortItem ) 43 | if strings.Contains( spec, "-" ) { 44 | parts := strings.Split( spec, "-" ) 45 | from, _ := strconv.Atoi( parts[0] ) 46 | to, _ := strconv.Atoi( parts[1] ) 47 | for i := from; i <= to; i++ { 48 | portItem := PortItem{ 49 | available: true, 50 | } 51 | ports[ i ] = &portItem 52 | } 53 | } else { 54 | log.WithFields( log.Fields{ 55 | "type": "portmap", 56 | "related": name, 57 | "spec": spec, 58 | } ).Fatal("Invalid ports spec") 59 | } 60 | return ports 61 | } 62 | 63 | func map_keys( amap map[int] *PortItem ) ( []int ) { 64 | arr := make( []int, len(amap) ) 65 | i := 0 66 | for k := range amap { 67 | arr[i] = k 68 | i++ 69 | } 70 | sort.Ints( arr ) 71 | return arr 72 | } 73 | 74 | func assign_port( amap map[int] *PortItem ) (int) { 75 | arr := map_keys( amap ) 76 | for _,port := range arr { 77 | portItem := amap[port] 78 | if portItem.available { 79 | portItem.available = false 80 | return port 81 | } 82 | } 83 | return 0 84 | } 85 | 86 | func assign_ports( gConfig *Config, portMap *PortMap ) ( int,int,int,int,int,int,int,*Config ) { 87 | dupConfig := *gConfig 88 | 89 | wdaPort := assign_port( portMap.wdaPorts ) 90 | dupConfig.WDAProxyPort = wdaPort 91 | 92 | vidPort := assign_port( portMap.vidPorts ) 93 | dupConfig.MirrorFeedPort = vidPort 94 | 95 | devIosPort := assign_port( portMap.devIosPorts ) 96 | dupConfig.DevIosPort = devIosPort 97 | 98 | vncPort := assign_port( portMap.vncPorts ) 99 | dupConfig.VncPort = vncPort 100 | 101 | usbmuxdPort := assign_port( portMap.usbmuxdPorts ) 102 | dupConfig.UsbmuxdPort = usbmuxdPort 103 | 104 | nanoOutPort := assign_port( portMap.decodePorts ) 105 | nanoInPort := assign_port( portMap.decodePorts ) 106 | dupConfig.DecodeOutPort = nanoOutPort 107 | dupConfig.DecodeInPort = nanoInPort 108 | 109 | return wdaPort, vidPort, devIosPort, vncPort, usbmuxdPort, nanoOutPort, nanoInPort, &dupConfig 110 | } 111 | 112 | func free_ports( 113 | wdaPort int, 114 | vidPort int, 115 | devIosPort int, 116 | vncPort int, 117 | usbmuxdPort int, 118 | portMap *PortMap ) { 119 | wdaItem := portMap.wdaPorts[ wdaPort ] 120 | wdaItem.available = true 121 | 122 | vidItem := portMap.vidPorts[ vidPort ] 123 | vidItem.available = true 124 | 125 | dItem := portMap.devIosPorts[ devIosPort ] 126 | dItem.available = true 127 | 128 | vncItem := portMap.vncPorts[ vncPort ] 129 | vncItem.available = true 130 | 131 | usbmuxdItem := portMap.usbmuxdPorts[ usbmuxdPort ] 132 | usbmuxdItem.available = true 133 | } -------------------------------------------------------------------------------- /coordinator/proc_backoff.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "time" 4 | 5 | type Backoff struct { 6 | fails int 7 | start time.Time 8 | elapsedSeconds float64 9 | } 10 | 11 | func ( self *Backoff ) markStart() { 12 | self.start = time.Now() 13 | } 14 | 15 | func ( self *Backoff ) markEnd() ( float64 ) { 16 | elapsed := time.Since( self.start ) 17 | seconds := elapsed.Seconds() 18 | self.elapsedSeconds = seconds 19 | return seconds 20 | } 21 | 22 | func ( self *Backoff ) wait() { 23 | sleeps := []int{ 0, 0, 2, 5, 10 } 24 | numSleeps := len( sleeps ) 25 | if self.elapsedSeconds < 20 { 26 | self.fails = self.fails + 1 27 | index := self.fails 28 | if index >= numSleeps { 29 | index = numSleeps - 1 30 | } 31 | sleepLen := sleeps[ index ] 32 | if sleepLen != 0 { 33 | time.Sleep( time.Second * time.Duration( sleepLen ) ) 34 | } 35 | } else { 36 | self.fails = 0 37 | } 38 | } -------------------------------------------------------------------------------- /coordinator/proc_device_trigger.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | ) 5 | 6 | func proc_device_trigger( o ProcOptions ) { 7 | o.procName = "device_trigger" 8 | 9 | conf := o.config 10 | if conf.DeviceDetector == "api" { 11 | o.binary = o.config.BinPaths.IosDeploy 12 | o.args = []string{ 13 | "-d", 14 | "-n", "test", 15 | "-t", "0", 16 | } 17 | } else { 18 | o.binary = o.config.BinPaths.DeviceTrigger 19 | } 20 | 21 | proc_generic( o ) 22 | } -------------------------------------------------------------------------------- /coordinator/proc_device_unit.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | "strings" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | func restart_device_unit( devd *RunningDev ) { 12 | restart_proc_generic( devd, "stf_device_ios" ) 13 | } 14 | 15 | var onRelease func() 16 | func stf_on_release( newOnRelease func() ) { 17 | onRelease = newOnRelease 18 | } 19 | 20 | func proc_device_ios_unit( o ProcOptions, uuid string, curIP string) { 21 | vncPort := 0 22 | if o.config.Video.UseVnc && o.config.Video.Enabled { 23 | vncPort = o.devd.vncPort 24 | } 25 | 26 | secure := o.config.FrameServer.Secure 27 | var frameServer string 28 | if secure { 29 | frameServer = fmt.Sprintf("wss://%s:%d/echo", curIP, o.devd.vidPort) 30 | } else { 31 | frameServer = fmt.Sprintf("ws://%s:%d/echo", curIP, o.devd.vidPort) 32 | } 33 | 34 | curDir, _ := os.Getwd() 35 | 36 | o.args = []string{ 37 | fmt.Sprintf("--inspect=0.0.0.0:%d", o.devd.devIosPort), 38 | "runmod.js" , "device-ios", 39 | "--serial" , uuid, 40 | "--name" , o.devd.name, 41 | "--connect-push" , fmt.Sprintf("tcp://%s:7270", o.config.Stf.Ip), 42 | "--connect-sub" , fmt.Sprintf("tcp://%s:7250", o.config.Stf.Ip), 43 | "--connect-port" , strconv.Itoa( o.devd.usbmuxdPort ), 44 | "--public-ip" , curIP, 45 | "--wda-port" , strconv.Itoa( o.devd.wdaPort ), 46 | "--storage-url" , fmt.Sprintf("https://%s", o.config.Stf.HostName), 47 | "--screen-ws-url-pattern", fmt.Sprintf("wss://%s/frames/%s/%d/x", o.config.Stf.HostName, curIP, o.devd.vidPort), 48 | //"--screen-ws-url-pattern", frameServer, 49 | "--vnc-password" , o.config.Video.VncPassword, 50 | "--vnc-port" , strconv.Itoa( vncPort ), 51 | "--vnc-scale" , strconv.Itoa( o.config.Video.VncScale ), 52 | "--stream-width" , strconv.Itoa( o.devd.streamWidth ), 53 | "--stream-height" , strconv.Itoa( o.devd.streamHeight ), 54 | "--click-width" , strconv.Itoa( o.devd.clickWidth ), 55 | "--click-height" , strconv.Itoa( o.devd.clickHeight ), 56 | "--click-scale" , strconv.Itoa( o.devd.clickScale ), 57 | "--ios-deploy-path" , ( curDir + "/" + o.config.BinPaths.IosDeploy ), 58 | } 59 | o.startFields = log.Fields { 60 | "server_ip": o.config.Stf.Ip, 61 | "client_ip": curIP, 62 | "server_host": o.config.Stf.HostName, 63 | "video_port": o.devd.vidPort, 64 | "node_port": o.devd.devIosPort, 65 | "device_name": o.devd.name, 66 | "vnc_scale": o.config.Video.VncScale, 67 | "stream_width": o.devd.streamWidth, 68 | "stream_height": o.devd.streamHeight, 69 | "clickScale": o.devd.clickScale, 70 | "clickWidth": o.devd.clickWidth, 71 | "clickHeight": o.devd.clickHeight, 72 | "frame_server": frameServer, 73 | } 74 | 75 | devd := o.devd 76 | o.stderrHandler = func( line string, plog *log.Entry ) (bool) { 77 | if strings.Contains( line, "Now owned by" ) { 78 | pos := strings.Index( line, "Now owned by" ) 79 | pos += len( "Now owned by" ) + 2 80 | ownedStr := line[ pos: ] 81 | endpos := strings.Index( ownedStr, "\"" ) 82 | owner := ownedStr[ :endpos ] 83 | plog.WithFields( log.Fields{ 84 | "type": "wda_owner_start", 85 | "owner": owner, 86 | } ).Info("Device Owner Start") 87 | devd.owner = owner 88 | } 89 | if strings.Contains( line, "No longer owned by" ) { 90 | pos := strings.Index( line, "No longer owned by" ) 91 | pos += len( "No longer owned by" ) + 2 92 | ownedStr := line[ pos: ] 93 | endpos := strings.Index( ownedStr, "\"" ) 94 | owner := ownedStr[ :endpos ] 95 | plog.WithFields( log.Fields{ 96 | "type": "wda_owner_stop", 97 | "owner": owner, 98 | } ).Info("Device Owner Stop") 99 | devd.owner = "" 100 | if onRelease != nil { 101 | onRelease() 102 | onRelease = nil 103 | } 104 | } 105 | if strings.Contains( line, "responding with identity" ) { 106 | plog.WithFields( log.Fields{ 107 | "type": "device_ios_ident", 108 | } ).Debug("Device IOS Unit Registered Identity") 109 | } 110 | if strings.Contains( line, "Sent ready message" ) { 111 | plog.WithFields( log.Fields{ 112 | "type": "device_ios_ready", 113 | } ).Debug("Device IOS Unit Ready") 114 | } 115 | return true 116 | } 117 | o.procName = "stf_device_ios" 118 | o.binary = "/usr/local/opt/node@12/bin/node" 119 | o.startDir = "./repos/stf-ios-provider" 120 | proc_generic( o ) 121 | } 122 | 123 | -------------------------------------------------------------------------------- /coordinator/proc_generic.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | log "github.com/sirupsen/logrus" 5 | gocmd "github.com/go-cmd/cmd" 6 | "time" 7 | ) 8 | 9 | type OutputHandler func( string, *log.Entry ) (bool) 10 | 11 | type ProcOptions struct { 12 | config *Config 13 | baseProgs *BaseProgs 14 | devd *RunningDev 15 | lineLog *log.Entry 16 | procName string 17 | binary string 18 | args []string 19 | stderrHandler OutputHandler 20 | stdoutHandler OutputHandler 21 | startFields log.Fields 22 | startDir string 23 | env map[string]string 24 | curIP string 25 | noRestart bool 26 | noWait bool 27 | onStop func( *RunningDev ) 28 | } 29 | 30 | type GPMsg struct { 31 | msgType int 32 | } 33 | 34 | type GenericProc struct { 35 | controlCh chan GPMsg 36 | backoff *Backoff 37 | pid int 38 | cmd *gocmd.Cmd 39 | } 40 | 41 | func (self *GenericProc) Kill() { 42 | if self.cmd == nil { return } 43 | self.controlCh <- GPMsg{ msgType: 1 } 44 | } 45 | 46 | func (self *GenericProc) Restart() { 47 | if self.cmd == nil { return } 48 | self.controlCh <- GPMsg{ msgType: 2 } 49 | } 50 | 51 | func restart_proc_generic( devd *RunningDev, name string ) { 52 | genProc := devd.process[ name ] 53 | genProc.Restart() 54 | } 55 | 56 | func proc_generic( opt ProcOptions ) ( *GenericProc ) { 57 | controlCh := make( chan GPMsg ) 58 | proc := GenericProc { 59 | controlCh: controlCh, 60 | } 61 | 62 | devd := opt.devd 63 | 64 | var plog *log.Entry 65 | var lineLog *log.Entry 66 | if devd != nil { 67 | plog = log.WithFields( log.Fields{ 68 | "proc": opt.procName, 69 | "uuid": censor_uuid( devd.uuid ), 70 | } ) 71 | lineLog = opt.lineLog.WithFields( log.Fields{ 72 | "proc": opt.procName, 73 | "uuid": censor_uuid( devd.uuid ), 74 | } ) 75 | devd.lock.Lock() 76 | devd.process[ opt.procName ] = &proc 77 | devd.lock.Unlock() 78 | } else { 79 | plog = log.WithFields( log.Fields{ "proc": opt.procName } ) 80 | lineLog = opt.lineLog.WithFields( log.Fields{ "proc": opt.procName } ) 81 | opt.baseProgs.lock.Lock() 82 | opt.baseProgs.process[ opt.procName ] = &proc 83 | opt.baseProgs.lock.Unlock() 84 | } 85 | 86 | backoff := Backoff{} 87 | proc.backoff = &backoff 88 | 89 | stop := false 90 | 91 | go func() { for { 92 | startFields := log.Fields{ 93 | "type": "proc_start", 94 | "binary": opt.binary, 95 | } 96 | if opt.startFields != nil { 97 | for k, v := range opt.startFields { 98 | startFields[k] = v 99 | } 100 | } 101 | 102 | plog.WithFields( startFields ).Info("Process start - " + opt.procName) 103 | 104 | cmd := gocmd.NewCmdOptions( gocmd.Options{ Streaming: true }, opt.binary, opt.args... ) 105 | proc.cmd = cmd 106 | 107 | if opt.startDir != "" { 108 | cmd.Dir = opt.startDir 109 | } 110 | 111 | if opt.env != nil { 112 | var envArr []string 113 | for k,v := range( opt.env ) { 114 | envArr = append( envArr, k, v ) 115 | } 116 | cmd.Env = envArr 117 | } 118 | 119 | backoff.markStart() 120 | 121 | statCh := cmd.Start() 122 | 123 | i := 0 124 | for { 125 | status := cmd.Status() 126 | 127 | if status.Error != nil { 128 | plog.WithFields( log.Fields{ 129 | "type": "proc_err", 130 | "error": status.Error, 131 | } ).Error("Error starting - " + opt.procName) 132 | 133 | return 134 | } 135 | 136 | if status.Exit != -1 { 137 | plog.WithFields( log.Fields{ 138 | "type": "proc_exit", 139 | "exit": status.Exit, 140 | } ).Error("Error starting - " + opt.procName) 141 | 142 | return 143 | } 144 | 145 | proc.pid = status.PID 146 | if proc.pid != 0 { 147 | break 148 | } 149 | time.Sleep(50 * time.Millisecond) 150 | if i > 4 { 151 | break 152 | } 153 | } 154 | 155 | plog.WithFields( log.Fields{ 156 | "type": "proc_pid", 157 | "pid": proc.pid, 158 | } ).Debug("Process pid") 159 | 160 | outStream := cmd.Stdout 161 | errStream := cmd.Stderr 162 | 163 | runDone := false 164 | for { 165 | select { 166 | case <- statCh: 167 | runDone = true 168 | case msg := <- controlCh: 169 | plog.Debug("Got stop request on control channel") 170 | if msg.msgType == 1 { // stop 171 | stop = true 172 | proc.cmd.Stop() 173 | } else if msg.msgType == 2 { // restart 174 | proc.cmd.Stop() 175 | } 176 | case line := <- outStream: 177 | doLog := true 178 | if opt.stdoutHandler != nil { doLog = opt.stdoutHandler( line, plog ) } 179 | if doLog { lineLog.WithFields( log.Fields{ "line": line } ).Info(""); } 180 | case line := <- errStream: 181 | doLog := true 182 | if opt.stderrHandler != nil { doLog = opt.stderrHandler( line, plog ) } 183 | if doLog { lineLog.WithFields( log.Fields{ "line": line, "iserr": true } ).Info("") } 184 | } 185 | if runDone { break } 186 | } 187 | 188 | proc.cmd = nil 189 | 190 | backoff.markEnd() 191 | 192 | plog.WithFields( log.Fields{ "type": "proc_end" } ).Warn("Process end - "+ opt.procName) 193 | 194 | if opt.onStop != nil { 195 | opt.onStop( devd ) 196 | } 197 | 198 | if opt.noRestart { 199 | plog.Debug( "No restart requested" ) 200 | break 201 | } 202 | 203 | if stop { break } 204 | 205 | if !opt.noWait { 206 | backoff.wait() 207 | } else { 208 | plog.Debug("No wait requested") 209 | } 210 | } }() 211 | 212 | return &proc 213 | } -------------------------------------------------------------------------------- /coordinator/proc_h264_to_jpeg.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | func proc_h264_to_jpeg( o ProcOptions ) { 11 | devd := o.devd.dup() 12 | udid := devd.uuid 13 | 14 | nanoIn := o.config.DecodeInPort 15 | nanoOut := o.config.DecodeOutPort 16 | 17 | inSpec := fmt.Sprintf("tcp://127.0.0.1:%d", nanoIn) 18 | outSpec := fmt.Sprintf("tcp://127.0.0.1:%d", nanoOut) 19 | 20 | o.binary = o.config.BinPaths.H264ToJpeg 21 | o.startFields = log.Fields { 22 | "h264SrcSpec": outSpec, 23 | "jpegDestSpec": inSpec, 24 | } 25 | o.procName = "h264_to_jpeg" 26 | o.args = []string { 27 | "nano", 28 | "--in", outSpec, 29 | "--out", inSpec, 30 | "--frameSkip", "2", 31 | "--cacheid", udid, 32 | } 33 | 34 | width := o.config.FrameServer.Width 35 | height := o.config.FrameServer.Height 36 | 37 | if width != 0 { 38 | o.args = append( o.args, "--dw", strconv.Itoa( width ) ) 39 | } 40 | if height != 0 { 41 | o.args = append( o.args, "--dh", strconv.Itoa( height ) ) 42 | } 43 | 44 | o.stdoutHandler = func( line string, plog *log.Entry ) (bool) { 45 | if strings.Contains( line, "Iframe - size:" ) { 46 | return false 47 | } 48 | return true 49 | } 50 | 51 | proc_generic( o ) 52 | } -------------------------------------------------------------------------------- /coordinator/proc_ios_video_pull.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | //"strconv" 6 | "strings" 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | func proc_ios_video_pull( o ProcOptions ) { 11 | devd := o.devd.dup() 12 | udid := devd.uuid 13 | 14 | nanoOut := o.config.DecodeOutPort 15 | 16 | outSpec := fmt.Sprintf("tcp://127.0.0.1:%d", nanoOut) 17 | 18 | o.binary = o.config.BinPaths.IosVideoPull 19 | o.startFields = log.Fields { 20 | "pushSpec": outSpec, 21 | } 22 | o.procName = "ios_video_pull" 23 | o.args = []string { 24 | "-pull", 25 | "-udid", udid, 26 | "-pushSpec", outSpec, 27 | } 28 | o.stdoutHandler = func( line string, plog *log.Entry ) (bool) { 29 | if strings.Contains( line, "error: libusb: interrupted" ) { 30 | return false 31 | } 32 | return true 33 | } 34 | proc_generic( o ) 35 | } -------------------------------------------------------------------------------- /coordinator/proc_ios_video_stream.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | log "github.com/sirupsen/logrus" 7 | ) 8 | 9 | func restart_ios_video_stream( devd *RunningDev ) { 10 | restart_proc_generic( devd, "ios_video_stream" ) 11 | } 12 | 13 | func proc_ios_video_stream( o ProcOptions, tunName string, frameInIp string ) { 14 | devd := o.devd.dup() 15 | udid := devd.uuid 16 | port := o.config.MirrorFeedPort 17 | 18 | nanoIn := o.config.DecodeInPort 19 | 20 | inSpec := fmt.Sprintf("tcp://%s:%d", frameInIp, nanoIn) 21 | 22 | coordinator := fmt.Sprintf( "127.0.0.1:%d", o.config.Network.Coordinator ) 23 | 24 | o.binary = o.config.BinPaths.IosVideoStream 25 | o.startFields = log.Fields { 26 | "tunName": tunName, 27 | "pullSpec": inSpec, 28 | "port": port, 29 | } 30 | o.procName = "ios_video_stream" 31 | o.args = []string { 32 | "-stream", 33 | "-port", strconv.Itoa( port ), 34 | "-udid", udid, 35 | "-interface", tunName, 36 | "-pullSpec", inSpec, 37 | "-coordinator", coordinator, 38 | } 39 | secure := o.config.FrameServer.Secure 40 | if secure { 41 | cert := o.config.FrameServer.Cert 42 | key := o.config.FrameServer.Key 43 | o.args = append( o.args, 44 | "--secure", 45 | "--cert", cert, 46 | "--key", key, 47 | ) 48 | } 49 | proc_generic( o ) 50 | } -------------------------------------------------------------------------------- /coordinator/proc_ivf.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | //"strconv" 6 | //"strings" 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | func restart_ivf( devd *RunningDev ) { 11 | restart_proc_generic( devd, "ivf" ) 12 | } 13 | 14 | func proc_ivf( o ProcOptions ) { 15 | devd := o.devd.dup() 16 | udid := devd.uuid 17 | 18 | nanoIn := o.config.DecodeInPort 19 | toStreamSpec := fmt.Sprintf("tcp://127.0.0.1:%d", nanoIn) 20 | 21 | o.binary = o.config.BinPaths.IVF 22 | o.startFields = log.Fields { 23 | "outSpec": toStreamSpec, 24 | } 25 | o.procName = "ivf" 26 | o.args = []string { 27 | "nano", 28 | "--udid", udid, 29 | "--out", toStreamSpec, 30 | "--frameSkip", "2", 31 | } 32 | proc_generic( o ) 33 | } -------------------------------------------------------------------------------- /coordinator/proc_stf_provider.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | func proc_stf_provider( o ProcOptions, curIP string ) { 11 | o.binary = o.config.BinPaths.IosVideoStream 12 | 13 | serverHostname := o.config.Stf.HostName 14 | clientHostname, _ := os.Hostname() 15 | serverIP := o.config.Stf.Ip 16 | 17 | location := fmt.Sprintf("macmini/%s", clientHostname) 18 | if o.config.Stf.Location != "" { 19 | location = o.config.Stf.Location 20 | } 21 | 22 | o.startFields = log.Fields { 23 | "client_ip": curIP, 24 | "server_ip": serverIP, 25 | "client_hostname": clientHostname, 26 | "server_hostname": serverHostname, 27 | "location": location, 28 | } 29 | o.binary = "/usr/local/opt/node@12/bin/node" 30 | o.args = []string { 31 | "--inspect=127.0.0.1:9230", 32 | "runmod.js" , "provider", 33 | "--name" , location, 34 | "--connect-sub" , fmt.Sprintf("tcp://%s:7250", serverIP), 35 | "--connect-push" , fmt.Sprintf("tcp://%s:7270", serverIP), 36 | "--storage-url" , fmt.Sprintf("https://%s", serverHostname), 37 | "--public-ip" , curIP, 38 | "--min-port=7400", 39 | "--max-port=7700", 40 | "--heartbeat-interval=10000", 41 | "--server-ip" , serverIP, 42 | "--no-cleanup", 43 | } 44 | o.procName = "stf_ios_provider" 45 | o.startDir = "./repos/stf-ios-provider" 46 | o.stdoutHandler = func( line string, plog *log.Entry ) (bool) { 47 | if strings.Contains( line, " IOS Heartbeat:" ) { 48 | return false 49 | } 50 | return true 51 | } 52 | proc_generic( o ) 53 | } -------------------------------------------------------------------------------- /coordinator/proc_video_enabler.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | ) 5 | 6 | func proc_video_enabler( o ProcOptions ) { 7 | o.procName = "video_enabler" 8 | o.binary = o.config.BinPaths.VideoEnabler 9 | proc_generic( o ) 10 | } -------------------------------------------------------------------------------- /coordinator/proc_vnc_proxy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strconv" 5 | log "github.com/sirupsen/logrus" 6 | ) 7 | 8 | func proc_vnc_proxy( o ProcOptions ) { 9 | o.procName = "vnc_proxy" 10 | 11 | vncPort := o.config.VncPort 12 | o.binary = o.config.BinPaths.Iproxy 13 | o.startFields = log.Fields { 14 | "vncPort": vncPort, 15 | } 16 | o.args = []string { 17 | strconv.Itoa( vncPort ), "5900", 18 | } 19 | proc_generic( o ) 20 | } -------------------------------------------------------------------------------- /coordinator/proc_wdaproxy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | log "github.com/sirupsen/logrus" 6 | "strconv" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | func restart_wdaproxy( devd *RunningDev, onRelease bool ) { 12 | if onRelease { 13 | test_restart_on_release( devd ) 14 | return 15 | } 16 | restart_proc_generic( devd, "wdaproxy" ) 17 | } 18 | func wait_wdaup( devd *RunningDev ) { 19 | for { 20 | if devd.wda == true { break } 21 | time.Sleep( time.Second * 10 ) 22 | } 23 | } 24 | 25 | func proc_wdaproxy( o ProcOptions, devEventCh chan<- DevEvent, temp bool ) { 26 | uuid := o.devd.uuid 27 | config := o.config 28 | 29 | if temp { 30 | o.procName = "wdaproxytemp" 31 | o.noRestart = true 32 | o.noWait = false 33 | } else { 34 | o.procName = "wdaproxy" 35 | o.noWait = true 36 | o.noRestart = false 37 | } 38 | 39 | o.binary = "../wdaproxy" //o.config.BinPaths.WdaProxy 40 | o.startFields = log.Fields { 41 | "wdaPort": o.config.WDAProxyPort, 42 | "iosVersion": o.devd.iosVersion, 43 | "--iosDeploy": config.BinPaths.IosDeploy, 44 | } 45 | o.args = []string { 46 | "-p", strconv.Itoa(o.config.WDAProxyPort), 47 | "-q", strconv.Itoa(8100),//o.config.WDAProxyPort), 48 | "-d", 49 | fmt.Sprintf("--iosDeploy=%s", config.BinPaths.IosDeploy), 50 | fmt.Sprintf("--mobileDevice=%s", "/usr/local/bin/mobiledevice"), 51 | "-W", ".", 52 | "-u", uuid, 53 | fmt.Sprintf("--iosversion=%s", o.devd.iosVersion), 54 | } 55 | o.startDir = o.config.WdaFolder 56 | 57 | o.stdoutHandler = func( line string, plog *log.Entry ) (bool) { 58 | if strings.Contains( line, "TEST EXECUTE FAILED" ) { 59 | plog.WithFields( log.Fields{ 60 | "type": "wda_failed", 61 | } ).Error("WDA Failed") 62 | 63 | devEventCh <- DevEvent{ 64 | action: 5, 65 | uuid: uuid, 66 | } 67 | } 68 | return true 69 | } 70 | 71 | devd := o.devd 72 | o.stderrHandler = func( line string, plog *log.Entry ) (bool) { 73 | if strings.Contains( line, "[WDA] successfully started" ) { 74 | plog.WithFields( log.Fields{ 75 | "type": "wda_started", 76 | } ).Info("WDA Running") 77 | devd.lock.Lock() 78 | devd.wda = true 79 | devd.lock.Unlock() 80 | 81 | devEventCh <- DevEvent{ 82 | action: 4, 83 | uuid: uuid, 84 | } 85 | } 86 | return true 87 | } 88 | o.onStop = func( devd *RunningDev ) { 89 | devd.lock.Lock() 90 | devd.wda = false 91 | devd.lock.Unlock() 92 | } 93 | 94 | proc_generic( o ) 95 | } -------------------------------------------------------------------------------- /coordinator/shutdown.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/signal" 7 | //"strings" 8 | "syscall" 9 | "time" 10 | log "github.com/sirupsen/logrus" 11 | //ps "github.com/jviney/go-proc" 12 | si "github.com/elastic/go-sysinfo" 13 | ) 14 | 15 | func cleanup_procs(config *Config) { 16 | plog := log.WithFields( log.Fields{ 17 | "type": "proc_cleanup", 18 | } ) 19 | 20 | procMap := map[string]string { 21 | "ios_video_stream": config.BinPaths.IosVideoStream, 22 | "device_trigger": config.BinPaths.DeviceTrigger, 23 | "h264_to_jpeg": config.BinPaths.H264ToJpeg, 24 | "wdaproxy": "../wdaproxy", 25 | "ivf": config.BinPaths.IVF, 26 | "ios-deploy": config.BinPaths.IosDeploy, 27 | } 28 | 29 | // Cleanup hanging processes if any 30 | procs, listErr := si.Processes() 31 | if listErr != nil { 32 | fmt.Printf( "listErr:%s\n", listErr ) 33 | os.Exit(1) 34 | } 35 | 36 | var hangingPids []int 37 | 38 | for _, proc := range procs { 39 | info, infoErr := proc.Info() 40 | if infoErr != nil { 41 | //fmt.Printf( "infoErr:%s\n", infoErr ) 42 | continue 43 | } 44 | 45 | cmd := info.Args 46 | //cmdFlat := strings.Join( cmd, " " ) 47 | 48 | for k,v := range procMap { 49 | if cmd[0] == v { 50 | pid := proc.PID() 51 | plog.WithFields( log.Fields{ 52 | "proc": k, 53 | "pid": pid, 54 | } ).Warn("Leftover " + k + " - Sending SIGTERM") 55 | 56 | syscall.Kill( pid, syscall.SIGTERM ) 57 | hangingPids = append( hangingPids, pid ) 58 | } 59 | } 60 | 61 | /*if strings.Contains( cmdFlat, "node" ) { 62 | log.WithFields( log.Fields{ 63 | "cmdLine": cmdFlat, 64 | } ).Info("Leftover Node proc") 65 | }*/ 66 | 67 | // node --inspect=[ip]:[port] runmod.js device-ios 68 | if cmd[0] == "/usr/local/opt/node@12/bin/node" && cmd[3] == "device-ios" { 69 | pid := proc.PID() 70 | 71 | plog.WithFields( log.Fields{ 72 | "proc": "device-ios", 73 | "pid": pid, 74 | } ).Warn("Leftover Proc - Sending SIGTERM") 75 | 76 | syscall.Kill( pid, syscall.SIGTERM ) 77 | hangingPids = append( hangingPids, pid ) 78 | } 79 | 80 | // node --inspect=[ip]:[port] runmod.js provider 81 | if cmd[0] == "/usr/local/opt/node@12/bin/node" && cmd[3] == "provider" { 82 | pid := proc.PID() 83 | 84 | plog.WithFields( log.Fields{ 85 | "proc": "stf_provider", 86 | "pid": pid, 87 | } ).Warn("Leftover Proc - Sending SIGTERM") 88 | 89 | syscall.Kill( pid, syscall.SIGTERM ) 90 | hangingPids = append( hangingPids, pid ) 91 | } 92 | } 93 | 94 | if len( hangingPids ) > 0 { 95 | // Give the processes half a second to shudown cleanly 96 | time.Sleep( time.Millisecond * 500 ) 97 | 98 | // Send kill to processes still around 99 | for _, pid := range( hangingPids ) { 100 | proc, _ := si.Process( pid ) 101 | if proc != nil { 102 | info, infoErr := proc.Info() 103 | arg0 := "unknown" 104 | if infoErr == nil { 105 | args := info.Args 106 | arg0 = args[0] 107 | } else { 108 | // If the process vanished before here; it errors out fetching info 109 | continue 110 | } 111 | 112 | plog.WithFields( log.Fields{ 113 | "arg0": arg0, 114 | } ).Warn("Leftover Proc - Sending SIGKILL") 115 | syscall.Kill( pid, syscall.SIGKILL ) 116 | } 117 | } 118 | 119 | // Spend up to 500 ms waiting for killed processes to vanish 120 | i := 0 121 | for { 122 | i = i + 1 123 | time.Sleep( time.Millisecond * 100 ) 124 | allGone := 1 125 | for _, pid := range( hangingPids ) { 126 | proc, _ := si.Process( pid ) 127 | if proc != nil { 128 | _, infoErr := proc.Info() 129 | if infoErr != nil { 130 | continue 131 | } 132 | allGone = 0 133 | } 134 | } 135 | if allGone == 1 && i > 5 { 136 | break 137 | } 138 | } 139 | 140 | // Write out error messages for processes that could not be killed 141 | for _, pid := range( hangingPids ) { 142 | proc, _ := si.Process( pid ) 143 | if proc != nil { 144 | info, infoErr := proc.Info() 145 | arg0 := "unknown" 146 | if infoErr != nil { 147 | continue 148 | } 149 | args := info.Args 150 | arg0 = args[0] 151 | 152 | plog.WithFields( log.Fields{ 153 | "arg0": arg0, 154 | } ).Error("Kill attempted and failed") 155 | } 156 | } 157 | } 158 | } 159 | 160 | func closeAllRunningDevs( runningDevs map [string] *RunningDev ) { 161 | for _, devd := range runningDevs { 162 | closeRunningDev( devd, nil ) 163 | } 164 | } 165 | 166 | func closeRunningDev( devd *RunningDev, portMap *PortMap ) { 167 | devd.lock.Lock() 168 | devd.shuttingDown = true 169 | devd.lock.Unlock() 170 | 171 | if portMap != nil { 172 | free_ports( devd.wdaPort, devd.vidPort, devd.devIosPort, devd.vncPort, devd.usbmuxdPort, portMap ) 173 | } 174 | 175 | plog := log.WithFields( log.Fields{ 176 | "type": "proc_cleanup_kill", 177 | "uuid": censor_uuid( devd.uuid ), 178 | } ) 179 | 180 | plog.Info("Closing running dev") 181 | 182 | for k,v := range( devd.process ) { 183 | plog.WithFields( log.Fields{ "proc": k } ).Debug("Killing "+k) 184 | if v != nil { v.Kill() } 185 | } 186 | } 187 | 188 | func closeBaseProgs( baseProgs *BaseProgs ) { 189 | baseProgs.shuttingDown = true 190 | vpn_shutdown( baseProgs ) 191 | 192 | plog := log.WithFields( log.Fields{ "type": "proc_cleanup_kill" } ) 193 | 194 | for k,v := range( baseProgs.process ) { 195 | plog.WithFields( log.Fields{ "proc": k } ).Debug("Killing "+k) 196 | v.Kill() 197 | } 198 | } 199 | 200 | func coro_sigterm( runningDevs map [string] *RunningDev, baseProgs *BaseProgs, config *Config ) { 201 | c := make(chan os.Signal, 2) 202 | signal.Notify(c, os.Interrupt, syscall.SIGTERM) 203 | go func() { 204 | <- c 205 | log.WithFields( log.Fields{ 206 | "type": "sigterm", 207 | "state": "begun", 208 | } ).Info("Shutdown started") 209 | 210 | // This triggers zmq to stop receiving 211 | // We don't actually wait after this to ensure it has finished cleanly... oh well :) 212 | gStop = true 213 | 214 | closeAllRunningDevs( runningDevs ) 215 | closeBaseProgs( baseProgs ) 216 | 217 | time.Sleep( time.Millisecond * 1000 ) 218 | cleanup_procs( config ) 219 | 220 | log.WithFields( log.Fields{ 221 | "type": "sigterm", 222 | "state": "done", 223 | } ).Info("Shutdown finished") 224 | 225 | os.Exit(0) 226 | }() 227 | } -------------------------------------------------------------------------------- /coordinator/stf_control.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "strings" 10 | ) 11 | 12 | func NewStfClient() (http.Client) { 13 | tr := &http.Transport{ 14 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 15 | } 16 | return http.Client{ Transport: tr } 17 | } 18 | 19 | func stf_set_auth( config *Config, req *http.Request ) { 20 | token := config.Stf.AdminToken 21 | req.Header.Set( "Authorization", "Bearer " + token ) 22 | } 23 | 24 | func stf_do_request( config *Config, req *http.Request ) (bool, *http.Response) { 25 | client := NewStfClient() 26 | stf_set_auth( config, req ) 27 | resp, err := client.Do( req ) 28 | if err != nil || !strings.HasPrefix( resp.Status, "200" ) { 29 | fmt.Println("Error:", err ) 30 | fmt.Println("Response Status:", resp.Status) 31 | body, _ := ioutil.ReadAll(resp.Body) 32 | fmt.Println("Response Body:", string(body)) 33 | 34 | return false,nil 35 | } 36 | return true, resp 37 | } 38 | 39 | func stf_reserve( config *Config, udid string ) (bool) { 40 | json := fmt.Sprintf(`{"serial":"%s"}`,udid) 41 | fmt.Println("Sending:",json) 42 | url := fmt.Sprintf("https://%s/api/v1/user/devices", config.Stf.HostName ) 43 | req, _ := http.NewRequest("POST", url, bytes.NewReader( []byte( json ) ) ) 44 | req.Header.Set( "Content-Type", "application/json" ) 45 | 46 | success, _ := stf_do_request( config, req ) 47 | return success 48 | } 49 | 50 | func stf_release( config *Config, udid string ) (bool) { 51 | url := fmt.Sprintf("https://%s/api/v1/user/devices/%s", config.Stf.HostName, udid) 52 | req, _ := http.NewRequest("DELETE", url, nil ) 53 | 54 | success, _ := stf_do_request( config, req ) 55 | return success 56 | } -------------------------------------------------------------------------------- /coordinator/video_app.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "os/exec" 8 | ) 9 | 10 | func va_write_config( config *Config, uuid string, vidport string, ip string ) { 11 | // create a temp file containing config 12 | // use ios-deploy to write the file to Documents dir of app 13 | fmt.Printf("Writing video app config port=%s ip=%s\n", vidport, ip ) 14 | 15 | conf := fmt.Sprintf(`{ 16 | "port": "%s", 17 | "ip": "%s" 18 | }`, vidport, ip ) 19 | fh, err := ioutil.TempFile("", "config") 20 | if err != nil { 21 | os.Exit(1) 22 | } 23 | fh.WriteString( conf ) 24 | //defer os.Remove( fh.Name() ) 25 | 26 | fmt.Printf( "%s -i %s -o %s -1 %s -2 %s\n", config.BinPaths.IosDeploy, uuid, fh.Name(), "com.dryark.vidtest2", "Documents/config.json" ); 27 | 28 | exec.Command( 29 | config.BinPaths.IosDeploy, 30 | "-i", uuid, 31 | "-o", fh.Name(), 32 | "-1", "com.dryark.vidtest2", 33 | "-2", "Documents/config.json", 34 | ).Output() 35 | 36 | } 37 | 38 | func va_start_stream() { 39 | } 40 | 41 | func va_stop_stream() { 42 | } 43 | 44 | func va_check_status() { 45 | } -------------------------------------------------------------------------------- /coordinator/wda.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net/http" 7 | "strconv" 8 | "strings" 9 | "sync" 10 | "time" 11 | uj "github.com/nanoscopic/ujsonin/mod" 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | type WDAType struct { 16 | base string 17 | channel chan DevEvent 18 | devd *RunningDev 19 | } 20 | 21 | func NewWDACaller( base string ) ( *WDAType ) { 22 | self := WDAType { base: base } 23 | return &self 24 | } 25 | 26 | func NewTempWDA( o ProcOptions ) ( *WDAType ) { 27 | tempCh := make( chan DevEvent ) 28 | wda := WDAType { 29 | channel: tempCh, 30 | base: ( "http://" + o.curIP + ":" + strconv.Itoa( o.devd.wdaPort ) ), 31 | devd: o.devd, 32 | } 33 | 34 | proc_wdaproxy( o, tempCh, true ) 35 | 36 | // Wait for WDA to actually start up 37 | for { 38 | done := 0 39 | select { 40 | case devEvent := <- tempCh: 41 | if devEvent.action == 4 { 42 | log.Info("TempWDA Started") 43 | done = 1 44 | break 45 | } 46 | } 47 | if done == 1 { break } 48 | } 49 | 50 | return &wda 51 | } 52 | 53 | func aio_reset_media_services( o ProcOptions ) { 54 | baseCopy := *(o.baseProgs) 55 | o.baseProgs = &baseCopy 56 | devCopy := *(o.devd) 57 | o.devd = &devCopy 58 | devCopy.lock = &sync.Mutex{} 59 | 60 | wda := NewTempWDA( o ) 61 | time.Sleep( time.Second * 2 ) 62 | wda.reset_media_services() 63 | o.baseProgs.shuttingDown = true 64 | wda.end() 65 | time.Sleep( time.Second * 2 ) 66 | } 67 | 68 | func ( self *WDAType ) end() { 69 | devd := self.devd 70 | wdaProc := devd.process["wdaproxytemp"] 71 | log.WithFields( log.Fields{ 72 | "type": "proc_kill", 73 | "pid": wdaProc.pid, 74 | } ).Debug("Attempting to kill") 75 | wdaProc.Kill() 76 | } 77 | 78 | func ( self *WDAType ) reset_media_services() { 79 | sid := self.create_session( "com.apple.Preferences" ) 80 | devEl := self.el_by_name( sid, "Developer" ) 81 | log.Debug("Got ID " + devEl + " for Developer item" ) 82 | self.scroll_to( sid, devEl ) 83 | self.click( sid, devEl ) 84 | resetEl := self.el_by_name( sid, "Reset Media Services" ) 85 | log.Debug("Got ID " + resetEl + " for Reset Media Services item" ) 86 | self.scroll_to( sid, resetEl ) 87 | self.click( sid, resetEl ) 88 | self.home( sid ) 89 | } 90 | 91 | func ( self *WDAType ) el_by_name( sid string, name string ) ( string ) { 92 | json := fmt.Sprintf(`{ 93 | "using": "name", 94 | "value": "%s" 95 | }`, name ) 96 | url := self.base + "/session/" + sid + "/element" 97 | log.Info( "visiting " + url ) 98 | resp, _ := http.Post( url, "application/json", strings.NewReader( json ) ) 99 | res := resp_to_val( resp ) 100 | //log.Info( "response " + resp_to_str( resp ) ) 101 | el := res.Get("ELEMENT") 102 | if el != nil { 103 | return el.String() 104 | } 105 | log.Error( "could not find element with name %s", name ) 106 | return "" 107 | } 108 | 109 | func ( self *WDAType ) click( sid string, eid string ) { 110 | url := self.base + "/session/" + sid + "/element/" + eid + "/click" 111 | log.Info( "visiting " + url ) 112 | resp, _ := http.Post( url, "application/json", strings.NewReader( "{}" ) ) 113 | if resp.StatusCode != 200 { 114 | log.Error( "got resp" + strconv.Itoa( resp.StatusCode ) + "from " + url ) 115 | } 116 | //res := resp_to_val( resp ) 117 | } 118 | 119 | func ( self *WDAType ) force_touch( sid string, eid string ) { 120 | url := self.base + "/session/" + sid + "/wda/element/" + eid + "/forceTouch" 121 | log.Info( "visiting " + url ) 122 | 123 | json := `{ 124 | "duration": 1, 125 | "pressure": 1000 126 | }` 127 | 128 | resp, _ := http.Post( url, "application/json", strings.NewReader( json ) ) 129 | if resp.StatusCode != 200 { 130 | log.Error( "got resp" + strconv.Itoa( resp.StatusCode ) + "from " + url ) 131 | } 132 | } 133 | 134 | func ( self *WDAType ) scroll_to( sid string, eid string ) { 135 | url := self.base + "/session/" + sid + "/wda/element/" + eid + "/scroll" 136 | log.Info( "visiting " + url ) 137 | resp, _ := http.Post( url, "application/json", strings.NewReader( "{\"toVisible\":1}" ) ) 138 | if resp.StatusCode != 200 { 139 | log.Error( "got resp" + strconv.Itoa( resp.StatusCode ) + "from " + url ) 140 | } 141 | } 142 | 143 | func ( self *WDAType ) home( sid string ) { 144 | url := self.base + "/wda/homescreen" 145 | log.Info( "visiting " + url ) 146 | resp, _ := http.Post( url, "application/json", strings.NewReader( "{}" ) ) 147 | if resp.StatusCode != 200 { 148 | log.Error( "got resp" + strconv.Itoa( resp.StatusCode ) + "from " + url ) 149 | } 150 | } 151 | 152 | func ( self *WDAType ) create_session( bundle string ) ( string ) { 153 | ops := fmt.Sprintf( `{ 154 | "capabilities": { 155 | "alwaysMatch": {}, 156 | "firstMatch": [ 157 | { 158 | "arguments": [], 159 | "bundleId": "%s", 160 | "environment": {}, 161 | "shouldUseSingletonTestManager": true, 162 | "shouldUseTestManagerForVisibilityDetection": false, 163 | "shouldWaitForQuiescence": true 164 | } 165 | ] 166 | } 167 | }`, bundle ); 168 | resp, _ := http.Post( self.base + "/session", "application/json", strings.NewReader( ops ) ) 169 | res := resp_to_val( resp ) 170 | return res.Get("sessionId").String() 171 | } 172 | 173 | func ( self *WDAType ) swipe( sid string, x1 int, y1 int, x2 int, y2 int ) ( string ) { 174 | log.Info( "Swiping:", x1, y1, x2, y2 ) 175 | json := fmt.Sprintf( `{ 176 | "actions": [ 177 | { 178 | "action": "press", 179 | "options": { 180 | "x":%d, 181 | "y":%d 182 | } 183 | }, 184 | { 185 | "action":"wait", 186 | "options": { 187 | "ms": 500 188 | } 189 | }, 190 | { 191 | "action": "moveTo", 192 | "options": { 193 | "x":%d, 194 | "y":%d 195 | } 196 | }, 197 | { 198 | "action":"release", 199 | "options":{} 200 | } 201 | ] 202 | }`, x1, y1, x2, y2 ) 203 | resp, _ := http.Post( self.base + "/session/" + sid + "/wda/touch/perform", "application/json", strings.NewReader( json ) ) 204 | res := resp_to_str( resp ) 205 | log.Info( "response " + res ) 206 | return res 207 | } 208 | 209 | func ( self *WDAType ) launch_app( sid string, app string ) ( string ) { 210 | log.Info( "Launching:", app ) 211 | json := fmt.Sprintf( `{ 212 | "bundleId": "%s", 213 | "shouldWaitForQuiescence": false, 214 | "arguments": [], 215 | "environment": [] 216 | }`, app ) 217 | resp, _ := http.Post( self.base + "/session/" + sid + "/wda/apps/launch", "application/json", strings.NewReader( json ) ) 218 | res := resp_to_str( resp ) 219 | log.Info( "response " + res ) 220 | return res 221 | } 222 | 223 | func wda_session( base string ) ( string ) { 224 | resp, _ := http.Get( base + "/status" ) 225 | content, _ := uj.Parse( []byte( resp_to_str( resp ) ) ) 226 | sid := content.Get("sessionId").String() 227 | return sid 228 | } 229 | 230 | func ( self *WDAType ) is_locked() ( bool ) { 231 | resp, _ := http.Get( self.base + "/wda/locked" ) 232 | respStr := resp_to_str( resp ) 233 | fmt.Printf("response str:%s\n", respStr) 234 | content, _ := uj.Parse( []byte( respStr ) ) 235 | //fmt.Printf("output:%s\n", content ) 236 | return content.Get("value").Bool() 237 | } 238 | 239 | func ( self *WDAType ) start_broadcast( devd *RunningDev, sid string, app_name string ) { 240 | self.control_center( devd, sid ) 241 | 242 | devEl := self.el_by_name( sid, "Screen Recording" ) 243 | self.force_touch( sid, devEl ) 244 | 245 | devEl = self.el_by_name( sid, app_name ) 246 | self.click( sid, devEl ) 247 | 248 | devEl = self.el_by_name( sid, "Start Broadcast" ) 249 | self.click( sid, devEl ) 250 | } 251 | 252 | func ( self *WDAType ) control_center( devd *RunningDev, sid string ) { 253 | prod := devd.productNum 254 | 255 | // ProductTypes that use the new method of bringing up the control center 256 | // See https://gist.github.com/adamawolf/3048717 257 | var newProds = map[string]bool{ 258 | "iPhone11": true, 259 | "iPhone12": true, 260 | "iPhone13": true, 261 | "iPad11": true, 262 | } 263 | var newProdFull = map[string]bool{ 264 | "iPhone10,3": true, 265 | "iPhone10,6": true, 266 | } 267 | 268 | width, height := self.window_size( sid ) 269 | if newProds[ prod ] || newProdFull[ devd.productType ] { 270 | maxx := width -1 271 | self.swipe( sid, maxx, 0, maxx, 100 ) 272 | } else { 273 | midx := width / 2 274 | maxy := height - 1 275 | self.swipe( sid, midx, maxy, midx, maxy - 100 ) 276 | } 277 | } 278 | 279 | func ( self *WDAType ) window_size( sid string ) ( int, int ) { 280 | resp, _ := http.Get( self.base + "/session/" + sid + "/window/size" ) 281 | val := resp_to_val( resp ) 282 | width := val.Get("width").Int() 283 | height := val.Get("height").Int() 284 | return width, height 285 | } 286 | 287 | func ( self *WDAType ) unlock() { 288 | http.Post( self.base + "/wda/unlock", "application/json", strings.NewReader( "{}" ) ) 289 | } 290 | 291 | func source( base string ) ( string ) { 292 | resp, _ := http.Get( base + "/source" ) 293 | res := resp_to_str( resp ) 294 | //print Dumper( res ) 295 | return res 296 | } 297 | 298 | func wda_apps_list( base string ) ( string ) { 299 | sid := wda_session( base ) 300 | resp, _ := http.Get( base + "/session/" + sid + "/wda/apps/list" ) 301 | res := resp_to_str( resp ) 302 | //print Dumper( res ) 303 | return res 304 | } 305 | 306 | func wda_battery_info( base string ) ( string ) { 307 | sid := wda_session( base ) 308 | resp, _ := http.Get( base + "/session/" + sid + "/wda/batteryInfo" ) 309 | res := resp_to_str( resp ) 310 | //print Dumper( $res ) 311 | return res 312 | } 313 | 314 | func resp_to_str( resp *http.Response ) ( string ) { 315 | body := resp.Body 316 | buf := new( bytes.Buffer ) 317 | buf.ReadFrom( body ) 318 | return buf.String() 319 | } 320 | 321 | func resp_to_json( resp *http.Response ) ( *uj.JNode ) { 322 | rawContent := resp_to_str( resp ) 323 | if !strings.HasPrefix( rawContent, "{" ) { 324 | return nil // &JNode{ nodeType: 1, hash: NewNodeHash() } 325 | } 326 | content, _ := uj.Parse( []byte( rawContent ) ) 327 | return content 328 | } 329 | 330 | func resp_to_val( resp *http.Response ) ( *uj.JNode ) { 331 | rawContent := resp_to_str( resp ) 332 | if !strings.HasPrefix( rawContent, "{" ) { 333 | return nil // &JNode{ nodeType: 1, hash: NewNodeHash() } 334 | } 335 | content, _ := uj.Parse( []byte( rawContent ) ) 336 | val := content.Get("value") 337 | if val == nil { return content } 338 | return val 339 | } -------------------------------------------------------------------------------- /get-version-info.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import json 4 | import os 5 | import subprocess 6 | import sys 7 | import argparse 8 | 9 | parser = argparse.ArgumentParser( description = "Collect git version info" ) 10 | parser.add_argument('--repo',default="") 11 | parser.add_argument('--unix',action="count") 12 | parser.add_argument('--wdasource',action="count") 13 | args = parser.parse_args() 14 | 15 | def git_info( dir ): 16 | if args.unix==1: 17 | cmd = ["/usr/bin/git","-C","./"+dir,"log","-1","--date=unix", "--no-merges"] 18 | else: 19 | cmd = ["/usr/bin/git","-C","./"+dir,"log","-1", "--no-merges"] 20 | 21 | try: 22 | res = subprocess.check_output( cmd, stderr=subprocess.STDOUT ) 23 | except subprocess.CalledProcessError as e: 24 | #sys.stderr.write( e.output ) 25 | return { 26 | "error": "missing"#e.output 27 | } 28 | 29 | remote = subprocess.check_output( ["/usr/bin/git", "-C", "./" + dir, "remote","-v"] ) 30 | 31 | res = res[:-1] # remove trailing "\n" 32 | remote = remote[:-1] 33 | remote = remote.split("\n")[0].split("\t") # just first line 34 | 35 | parts = res.split("\n") 36 | 37 | return { 38 | "commit": parts[0][7:], # remove 'commit ' 39 | "author": parts[1][7:].lstrip(), # remove 'Author:' and spaces 40 | "date": parts[2][5:].lstrip(), # remove 'Date:' and spaces 41 | "remote": remote[1].replace(" (fetch)",""), # just url to fetch 42 | } 43 | 44 | def xcode_version(): 45 | res = subprocess.check_output( ["/usr/bin/xcodebuild", "-version"] ) 46 | res = res[:-1] 47 | return res.split("\n") 48 | 49 | if args.repo != "": 50 | if args.repo == 'wda': 51 | data = { 52 | "wda": git_info( 'repos/WebDriverAgent' ), 53 | } 54 | data['wda']['xcode'] = xcode_version() 55 | if args.repo == 'ios_support': 56 | data = { 57 | "ios_support": git_info( '.' ), 58 | } 59 | else: 60 | data = { 61 | "wda": git_info( 'repos/WebDriverAgent' ), 62 | "h264_to_jpeg": git_info( 'repos/h264_to_jpeg' ), 63 | "device_trigger": git_info( 'repos/osx_ios_device_trigger' ), 64 | "stf": git_info( 'repos/stf-ios-provider' ), 65 | "ios_video_stream": git_info( 'repos/ios_video_stream' ), 66 | "wdaproxy": git_info( 'repos/wdaproxy' ), 67 | "ios_support": git_info( '.' ), 68 | "ios_avf_pull": git_info( 'repos/ios_avf_pull' ) 69 | } 70 | if os.path.exists( 'bin/wda/build_info.json' ): 71 | fh = open( 'bin/wda/build_info.json', 'r' ) 72 | wda_root = json.load( fh ) 73 | if args.wdasource != 1: 74 | data["wda"] = wda_root["wda"] 75 | 76 | print json.dumps( data, indent = 2 ) 77 | 78 | -------------------------------------------------------------------------------- /get-wda-build-path.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # From https://stackoverflow.com/questions/3915040/bash-fish-command-to-print-absolute-path-to-a-file 3 | abspath() { 4 | if [ -d "$1" ]; then (cd "$1"; pwd) 5 | elif [ -f "$1" ]; then 6 | if [[ $1 = /* ]]; then echo "$1" 7 | elif [[ $1 == */* ]]; then echo "$(cd "${1%/*}"; pwd)/${1##*/}" 8 | else echo "$(pwd)/$1"; fi 9 | fi 10 | } 11 | 12 | BPATH=$(xcodebuild -project repos/WebDriverAgent/WebDriverAgent.xcodeproj -showBuildSettings -configuration Debug | grep TARGET_BUILD | awk '{print $3}' | tr -d "\n") 13 | #echo BUILD_PATH="$BPATH" 14 | echo "$(abspath $BPATH/..)" 15 | -------------------------------------------------------------------------------- /init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | mkdir -p repos 3 | 4 | GR="\033[32m" 5 | RED="\033[91m" 6 | RST="\033[0m" 7 | 8 | function install_brew_if_needed() { 9 | if ! command -v brew > /dev/null; then 10 | echo "Brew not installed; installing" 11 | /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" 12 | fi 13 | } 14 | 15 | function assert_has_xcodebuild() { 16 | XCODE_VERSION="none" 17 | XCODE_MAJOR_VERSION="0" 18 | XCODE_MINOR_VERSION="0" 19 | if command -v xcodebuild > /dev/null; then 20 | XCODE_VERSION=`xcodebuild -version | grep Xcode | tr -d "\n" | perl -pe 's/Xcode //'` 21 | XCODE_MAJOR_VERSION=`echo $XCODE_VERSION | perl -pe 's/([0-9]+)\.[0-9]+/$1/'` 22 | XCODE_MINOR_VERSION=`echo $XCODE_VERSION | perl -pe 's/[0-9]+\.([0-9]+)/$1/'` 23 | fi 24 | 25 | #echo "XCODE Version: $XCODE_VERSION" 26 | #echo "XCODE Version: Major = $XCODE_MAJOR_VERSION, Minor = $XCODE_MINOR_VERSION" 27 | 28 | if [ $XCODE_MAJOR_VERSION > 10 ]; then 29 | echo -e "${GR}Xcode $XCODE_VERSION installed$RST" 30 | elif [ "$XCODE_VERSION" == "10.3" ]; then 31 | echo -e "${GR}Xcode 10.3 installed$RST" 32 | else 33 | echo -e "${RED}Xcode 10.3+ not installed$RST" 34 | echo -e "${RED}You need to install it and then rerun init.sh$RST" 35 | exit 1 36 | fi 37 | } 38 | 39 | install_brew_if_needed 40 | assert_has_xcodebuild 41 | ./util/brewser.pl installdeps stf_ios_support.rb 42 | ./util/brewser.pl ensurehead libplist 2.2.1 43 | ./util/brewser.pl fixpc libplist 2.0 44 | ./util/brewser.pl ensurehead libusbmuxd 2.0.3 45 | ./util/brewser.pl fixpc libusbmuxd 2.0 46 | #make libimd -------------------------------------------------------------------------------- /logs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dryark/stf_ios_support/2f32cbbf1506a740eb21a496c1cdc11d875558f7/logs/.gitkeep -------------------------------------------------------------------------------- /makefile_preflight.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | use strict; 3 | my $brew_check = `./util/brewser.pl checkdeps stf_ios_support.rb`; 4 | if( $brew_check =~ m/Missing/ ) { 5 | print STDERR $brew_check, "\nRun init.sh to correct\n"; 6 | print "x"; 7 | exit(1); 8 | } 9 | if( $brew_check =~ m/Brew must be installed/ ) { 10 | print STDERR "Brew must be installed", "\nRun init.sh to correct\n"; 11 | print "x"; 12 | exit(1); 13 | } 14 | `./check-versions.pl`; 15 | -------------------------------------------------------------------------------- /offline/Makefile: -------------------------------------------------------------------------------- 1 | all: /usr/local/bin/ideviceinfo 2 | 3 | # --- Basic Directories --- 4 | 5 | repos: 6 | mkdir repos 7 | 8 | # --- LibIMobileDevice --- 9 | 10 | /usr/local/bin/ideviceinfo: repos/libimobiledevice repos/libimobiledevice/tools/ideviceinfo | repos/libimobiledevice 11 | $(MAKE) -C repos/libimobiledevice install 12 | 13 | repos/libimobiledevice/tools/ideviceinfo: repos/libimobiledevice repos/libimobiledevice/Makefile | repos/libimobiledevice 14 | $(MAKE) -C repos/libimobiledevice 15 | 16 | repos/libimobiledevice/Makefile: | repos/libimobiledevice 17 | cd repos/libimobiledevice && NOCONFIGURE=1 ./autogen.sh 18 | cd repos/libimobiledevice && ./configure --disable-openssl 19 | 20 | # --- Clones --- 21 | 22 | repos/libimobiledevice: | repos 23 | git clone https://github.com/libimobiledevice/libimobiledevice.git repos/libimobiledevice -------------------------------------------------------------------------------- /run: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | bin/coordinator $* 3 | -------------------------------------------------------------------------------- /runner/Makefile: -------------------------------------------------------------------------------- 1 | TARGET = runner 2 | 3 | all: $(TARGET) 4 | 5 | runner_sources := $(wildcard *.go) 6 | 7 | $(TARGET): $(runner_sources) go.sum 8 | go build -o $(TARGET) -ldflags "-X main.GitCommit=$(GIT_COMMIT) -X main.GitDate=$(GIT_DATE) -X main.GitRemote=$(GIT_REMOTE) -X main.EasyVersion=$(EASY_VERSION)" . 9 | 10 | go.sum: 11 | go get 12 | go get . 13 | 14 | clean: 15 | $(RM) $(TARGET) go.sum -------------------------------------------------------------------------------- /runner/gencert.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | use strict; 3 | 4 | my $hostname = "localhost"; 5 | my $mainip = "127.0.0.1"; 6 | 7 | my $template = "runnercert.tmpl"; 8 | my $template_data = slurp( $template ); 9 | 10 | $template_data =~ s/HOSTNAME/$hostname/g; 11 | $template_data =~ s/IPADDR/$mainip/g; 12 | 13 | open( my $outfh, ">runnercert.conf" ); 14 | print $outfh $template_data; 15 | my $ips = get_ips(); 16 | my $index = 2; 17 | for my $ip ( @$ips ) { 18 | print $outfh "DNS.$index = $ip\n"; 19 | $index++; 20 | } 21 | close( $outfh ); 22 | 23 | `openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout server.key -out server.crt -config runnercert.conf -subj "/C=US/ST=Washington/L=Seattle/O=Dis/CN=$hostname"`; 24 | 25 | sub slurp { 26 | my $file = shift; 27 | open( my $fh, "<$file" ); 28 | my $data; 29 | { 30 | local $/ = undef; 31 | $data = <$fh>; 32 | } 33 | close( $fh ); 34 | return $data; 35 | } 36 | 37 | sub get_ips { 38 | my @lines = `ifconfig`; 39 | my @ips; 40 | for my $line ( @lines ) { 41 | next if( $line !~ m/inet / ); 42 | if( $line =~ m/inet ([0-9.]+) / ) { 43 | push( @ips, $1 ); 44 | } 45 | } 46 | return \@ips; 47 | } -------------------------------------------------------------------------------- /runner/go.mod: -------------------------------------------------------------------------------- 1 | module runner.go 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/go-cmd/cmd v1.2.1 7 | github.com/gorilla/template v0.0.0-20130106121210-ad2f1c41567b 8 | github.com/jviney/go-proc v0.2.0 9 | github.com/nanoscopic/ujsonin v1.9.0 10 | github.com/sirupsen/logrus v1.6.0 11 | ) 12 | -------------------------------------------------------------------------------- /runner/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/go-cmd/cmd v1.2.1 h1:fV4o2i9JX8+TeI1xB1x7Ji/3ndEfJNdaa+7uYGVhkuM= 3 | github.com/go-cmd/cmd v1.2.1/go.mod h1:F2yJeMVdy5ymftSgCR0zMN7XLhKFJpG5/1brXju8EXU= 4 | github.com/go-test/deep v1.0.6/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8= 5 | github.com/gorilla/template v0.0.0-20130106121210-ad2f1c41567b h1:jMvt85GaJEKEZdIQYsS72I/aVFvIIA4c+0/+P+wDVag= 6 | github.com/gorilla/template v0.0.0-20130106121210-ad2f1c41567b/go.mod h1:xfqvveesQKRN2vVwJ1nSfDdBGGkj+C+uxQpUqxIU76s= 7 | github.com/jviney/go-proc v0.2.0 h1:HzS0OfhmrhHrDIW7R1ajsCYu0HN/gB6ayl6BW76Zw5U= 8 | github.com/jviney/go-proc v0.2.0/go.mod h1:lb9gbAFTP34lAyqc4DEvOo21RDaAT6xO3cEuxrzI+mU= 9 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 10 | github.com/nanoscopic/ujsonin v1.9.0 h1:lblaLCr1H6g5Iy5IObSZJcvtzBIHK04GVYtuxATPtpk= 11 | github.com/nanoscopic/ujsonin v1.9.0/go.mod h1:FyHvuWes/DhijYGBTtQB74enYOLiHodd3M3waNV3gWU= 12 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 13 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 14 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 15 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 16 | -------------------------------------------------------------------------------- /runner/http_server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net/http" 7 | "text/template" 8 | "strconv" 9 | "strings" 10 | "time" 11 | uj "github.com/nanoscopic/ujsonin/mod" 12 | ) 13 | 14 | type Info struct { 15 | proc *GenericProc 16 | passmap map[string] string 17 | } 18 | 19 | func coro_http_server( port int, proc *GenericProc, passmap map[string] string, secure bool, crt string, key string, runnerVersion VersionInfo, config *uj.JNode ) { 20 | var listen_addr = fmt.Sprintf( "0.0.0.0:%d", port ) 21 | startServer( listen_addr, proc, passmap, secure, crt, key, runnerVersion, config ) 22 | } 23 | 24 | func BasicAuth(handler http.HandlerFunc, passmap map[string] string ) http.HandlerFunc { 25 | realm := "Enter auth for coordinator runner admin" 26 | 27 | return func(w http.ResponseWriter, r *http.Request) { 28 | 29 | user, pass, ok := r.BasicAuth() 30 | 31 | if !ok || !check_pass( user, pass, passmap ) { 32 | w.Header().Set("WWW-Authenticate", `Basic realm="`+realm+`"`) 33 | w.WriteHeader(401) 34 | w.Write([]byte("Unauthorised.\n")) 35 | return 36 | } 37 | 38 | handler(w, r) 39 | } 40 | } 41 | 42 | func startServer( listen_addr string, proc *GenericProc, passmap map[string] string, secure bool, crt string, key string, runnerVersion VersionInfo, config *uj.JNode ) { 43 | info := Info{ 44 | proc: proc, 45 | passmap: passmap, 46 | } 47 | 48 | fmt.Printf("HTTP server started") 49 | 50 | rootClosure := BasicAuth( func( w http.ResponseWriter, r *http.Request ) { 51 | handleRoot( w, r, info, runnerVersion ) 52 | }, passmap ); 53 | startClosure := BasicAuth( func( w http.ResponseWriter, r *http.Request ) { 54 | handleStart( w, r, info ) 55 | }, passmap ); 56 | stopClosure := BasicAuth( func( w http.ResponseWriter, r *http.Request ) { 57 | handleStop( w, r, info ) 58 | }, passmap ); 59 | restartClosure := BasicAuth( func( w http.ResponseWriter, r *http.Request ) { 60 | handleRestart( w, r, info ) 61 | }, passmap ); 62 | updateClosure := BasicAuth( func( w http.ResponseWriter, r *http.Request ) { 63 | handleUpdate( w, r, info, config ) 64 | }, passmap ); 65 | 66 | http.HandleFunc( "/", rootClosure ) 67 | http.HandleFunc( "/start", startClosure ) 68 | http.HandleFunc( "/stop", stopClosure ) 69 | http.HandleFunc( "/restart", restartClosure ) 70 | http.HandleFunc( "/update", updateClosure ) 71 | 72 | var err error 73 | if secure { 74 | err = http.ListenAndServeTLS( listen_addr, crt, key, nil ) 75 | } else { 76 | err = http.ListenAndServe( listen_addr, nil ) 77 | } 78 | fmt.Printf("HTTP ListenAndServe Error %s\n", err) 79 | } 80 | 81 | func handleRoot( w http.ResponseWriter, r *http.Request, info Info, rv VersionInfo ) { 82 | var allVersions map[string] VersionInfo = make( map[string] VersionInfo ) 83 | allVersions["Runner"] = rv 84 | loadVersionInfo( allVersions ) 85 | 86 | versionText := "" 87 | for itemName,item := range allVersions { 88 | var str bytes.Buffer 89 | 90 | remote := item.GitRemote 91 | remote = strings.Replace( remote, "git@github.com:", "", 1 ) 92 | remote = strings.Replace( remote, ".git", "", 1 ) 93 | rawRemote := remote 94 | remote = "" + remote + "" 95 | 96 | commit := item.GitCommit 97 | commit = "" + commit + "" 98 | 99 | time := unixToTimeObject( item.GitDate ) 100 | timeStr := time.Format( "Mon, Jan 2 2006 3:04 PM MST" ) 101 | 102 | versionTpl.Execute( &str, map[string] string { 103 | "GitCommit": commit, 104 | "GitDate": timeStr, 105 | "GitRemote": remote, 106 | "EasyVersion": item.EasyVersion, 107 | "Name": itemName, 108 | } ) 109 | versionText += str.String() + "
" 110 | } 111 | 112 | rootTpl.Execute( w, map[string] string{ 113 | "pid": strconv.Itoa( info.proc.pid ), 114 | "timeUp": info.proc.backoff.timeUpText(), 115 | "Versions": versionText, 116 | } ) 117 | } 118 | 119 | func unixToTimeObject( unix string ) ( time.Time ) { 120 | i, _ := strconv.ParseInt( unix, 10, 64) 121 | return time.Unix( i, 0 ) 122 | } 123 | 124 | func handleStart( w http.ResponseWriter, r *http.Request, info Info ) { 125 | w.Header().Set("Content-Type", "text/html; charset=utf-8") 126 | info.proc.Start() 127 | fmt.Fprintf( w, "ok" ) 128 | } 129 | 130 | func handleStop( w http.ResponseWriter, r *http.Request, info Info ) { 131 | w.Header().Set("Content-Type", "text/html; charset=utf-8") 132 | info.proc.Stop() 133 | fmt.Fprintf( w, "ok" ) 134 | } 135 | 136 | func handleRestart( w http.ResponseWriter, r *http.Request, info Info ) { 137 | w.Header().Set("Content-Type", "text/html; charset=utf-8") 138 | info.proc.Restart() 139 | fmt.Fprintf( w, "ok" ) 140 | } 141 | 142 | func handleUpdate( w http.ResponseWriter, r *http.Request, info Info, config *uj.JNode ) { 143 | w.Header().Set("Content-Type", "text/html; charset=utf-8") 144 | runUpdate( info, w, config ) 145 | } 146 | 147 | var versionTpl = template.Must(template.New("version").Parse(` 148 | {{.Name}} Version info:
149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 |
Git Commit{{.GitCommit}}
Git Date{{.GitDate}}
Git Remote{{.GitRemote}}
Easy Version{{.EasyVersion}}
167 | `)) 168 | 169 | var rootTpl = template.Must(template.New("root").Parse(` 170 | 171 | 172 | 173 | 235 | 236 | 237 |

DeviceFarmer IOS Coordinator Runner

238 | 246 | PID: {{.pid}}
247 | Time up: {{.timeUp}}
248 |
249 |
250 |
251 |
252 |
253 | {{.Versions}} 254 | 255 | 256 | `)) 257 | -------------------------------------------------------------------------------- /runner/pass_check.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "crypto/sha256" 6 | "crypto/subtle" 7 | uj "github.com/nanoscopic/ujsonin/mod" 8 | ) 9 | 10 | func check_pass( user string, pass string, hashes map[string] string ) bool { 11 | hash := hash_pass( pass ) 12 | check, ok := hashes[ user ] 13 | if !ok { return false } 14 | res := subtle.ConstantTimeCompare([]byte(check),[]byte(hash)) 15 | if res == 1 { return true } 16 | return false 17 | } 18 | 19 | func hash_pass( pass string ) string { 20 | h := sha256.New() 21 | h.Write([]byte(pass)) 22 | hash := fmt.Sprintf("%x", h.Sum(nil)) 23 | return hash 24 | } 25 | 26 | func json_users_to_passmap( users *uj.JNode ) ( map[string] string ) { 27 | passmap := map[string] string{} 28 | 29 | users.ForEach( func( cur *uj.JNode ) { 30 | //cur.Dump() 31 | user := cur.Get("user").String() 32 | pass := cur.Get("pass").String() 33 | passmap[ user ] = pass 34 | } ) 35 | return passmap 36 | } -------------------------------------------------------------------------------- /runner/proc_backoff.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | "strconv" 6 | ) 7 | 8 | type Backoff struct { 9 | fails int 10 | start time.Time 11 | elapsedSeconds float64 12 | } 13 | 14 | func ( self *Backoff ) markStart() { 15 | self.start = time.Now() 16 | } 17 | 18 | func ( self *Backoff ) timeUp() ( float64 ) { 19 | elapsed := time.Since( self.start ) 20 | seconds := elapsed.Seconds() 21 | return seconds 22 | } 23 | 24 | func ( self *Backoff ) timeUpText() ( string ) { 25 | seconds := uint16( self.timeUp() ) 26 | minutes := uint16(0) 27 | hours := uint16(0) 28 | days := uint16(0) 29 | if seconds > 60 { 30 | mod := seconds % 60 31 | minutes = seconds / 60 32 | seconds = mod 33 | } 34 | if minutes > 60 { 35 | mod := minutes % 60 36 | hours = minutes / 60 37 | minutes = mod 38 | } 39 | if hours > 24 { 40 | mod := hours % 24 41 | days = hours / 24 42 | hours = mod 43 | } 44 | text := strconv.Itoa(int(seconds)) + " sec" 45 | if minutes > 0 { 46 | text = strconv.Itoa(int(minutes)) + " mins " + text 47 | } 48 | if hours > 0 { 49 | text = strconv.Itoa(int(hours)) + " hrs " + text 50 | } 51 | if days > 0 { 52 | text = strconv.Itoa(int(days)) + " days " + text 53 | } 54 | return text 55 | } 56 | 57 | func ( self *Backoff ) markEnd() ( float64 ) { 58 | elapsed := time.Since( self.start ) 59 | seconds := elapsed.Seconds() 60 | self.elapsedSeconds = seconds 61 | return seconds 62 | } 63 | 64 | func ( self *Backoff ) wait() { 65 | sleeps := []int{ 0, 0, 2, 5, 10 } 66 | numSleeps := len( sleeps ) 67 | if self.elapsedSeconds < 20 { 68 | self.fails = self.fails + 1 69 | index := self.fails 70 | if index >= numSleeps { 71 | index = numSleeps - 1 72 | } 73 | sleepLen := sleeps[ index ] 74 | if sleepLen != 0 { 75 | time.Sleep( time.Second * time.Duration( sleepLen ) ) 76 | } 77 | } else { 78 | self.fails = 0 79 | } 80 | } -------------------------------------------------------------------------------- /runner/runner.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | "flag" 7 | gocmd "github.com/go-cmd/cmd" 8 | uj "github.com/nanoscopic/ujsonin/mod" 9 | "io/ioutil" 10 | "os" 11 | "os/exec" 12 | ) 13 | 14 | type GPMsg struct { 15 | msgType int 16 | } 17 | 18 | type GenericProc struct { 19 | controlCh chan GPMsg 20 | backoff *Backoff 21 | pid int 22 | cmd *gocmd.Cmd 23 | hold bool 24 | } 25 | 26 | func (self *GenericProc) Kill() { 27 | if self.cmd == nil { return } 28 | self.controlCh <- GPMsg{ msgType: 1 } 29 | } 30 | 31 | func (self *GenericProc) Restart() { 32 | if self.cmd == nil { return } 33 | self.controlCh <- GPMsg{ msgType: 2 } 34 | } 35 | 36 | func (self *GenericProc) Start() { 37 | self.controlCh <- GPMsg{ msgType: 3 } 38 | } 39 | 40 | func (self *GenericProc) Stop() { 41 | self.controlCh <- GPMsg{ msgType: 4 } 42 | } 43 | 44 | func proc_generic( binary string, args []string, startDir string ) ( *GenericProc ) { 45 | controlCh := make( chan GPMsg ) 46 | backoff := Backoff{} 47 | 48 | proc := GenericProc { 49 | controlCh: controlCh, 50 | backoff: &backoff, 51 | hold: false, 52 | } 53 | 54 | stop := false 55 | hold := false 56 | 57 | go func() { for { 58 | if hold == true { 59 | fmt.Println("Waiting for signal to start again") 60 | } 61 | 62 | for { 63 | if hold == false { 64 | break 65 | } 66 | select { 67 | case msg := <- controlCh: 68 | fmt.Printf("Got message on control channel") 69 | if msg.msgType == 3 { // start 70 | hold = false 71 | break 72 | } 73 | } 74 | } 75 | 76 | fmt.Printf("Coordinator start\n") 77 | 78 | if !fileExists( binary ) { 79 | fmt.Printf("Coordinator binary does not exist. Waiting for creation\n") 80 | hold = true 81 | continue 82 | } 83 | cmd := gocmd.NewCmdOptions( gocmd.Options{ Streaming: true }, binary, args... ) 84 | proc.cmd = cmd 85 | 86 | if startDir != "" { 87 | cmd.Dir = startDir 88 | } 89 | 90 | backoff.markStart() 91 | 92 | statCh := cmd.Start() 93 | 94 | i := 0 95 | for { 96 | proc.pid = cmd.Status().PID 97 | if proc.pid != 0 { 98 | break 99 | } 100 | time.Sleep(50 * time.Millisecond) 101 | if i > 4 { 102 | break 103 | } 104 | } 105 | 106 | fmt.Printf("PID %d\n", proc.pid) 107 | 108 | outStream := cmd.Stdout 109 | errStream := cmd.Stderr 110 | 111 | runDone := false 112 | for { 113 | select { 114 | case <- statCh: 115 | runDone = true 116 | case msg := <- controlCh: 117 | fmt.Printf("Got stop request on control channel\n") 118 | typ := msg.msgType 119 | 120 | if typ == 1 { // stop 121 | stop = true 122 | } else if typ == 4 { // stop 123 | hold = true 124 | } 125 | 126 | if typ == 1 || typ == 2 || typ == 4 { 127 | proc.cmd.Stop() 128 | cleanup_subprocs( binary ) 129 | } 130 | 131 | case line := <- outStream: 132 | fmt.Println( line ) 133 | case line := <- errStream: 134 | fmt.Println( line ) 135 | } 136 | if runDone { break } 137 | } 138 | 139 | proc.cmd = nil 140 | proc.pid = 0 141 | 142 | backoff.markEnd() 143 | 144 | fmt.Printf("Coordinator end\n") 145 | 146 | if stop { break } 147 | backoff.wait() 148 | } }() 149 | 150 | return &proc 151 | } 152 | 153 | func cleanup_subprocs( binary string ) { 154 | // Make absolutely sure all coordinator subprocesses have been stopped 155 | out, _ := exec.Command( binary, "-killProcs" ).Output() 156 | fmt.Println( out ) 157 | } 158 | 159 | func gen_cert() { 160 | out, err := exec.Command( "/usr/bin/perl", "gencert.pl" ).Output() 161 | if err != nil { 162 | fmt.Printf("Error from cert gen: %s\n", err ) 163 | return 164 | } 165 | fmt.Println( out ) 166 | } 167 | 168 | var GitCommit string 169 | var GitDate string 170 | var GitRemote string 171 | var EasyVersion string 172 | 173 | type VersionInfo struct { 174 | GitCommit string 175 | GitDate string 176 | GitRemote string 177 | EasyVersion string 178 | } 179 | 180 | func main() { 181 | runnerVersion := VersionInfo{ 182 | GitCommit: GitCommit, 183 | GitDate: GitDate, 184 | GitRemote: GitRemote, 185 | EasyVersion: EasyVersion, 186 | } 187 | 188 | var passToHash = flag.String( "pass", "", "Password to show hash of" ) 189 | var doVersion = flag.Bool( "version" , false , "Show coordinator version info" ) 190 | 191 | flag.Parse() 192 | 193 | if *passToHash != "" { 194 | hash := hash_pass( *passToHash ) 195 | fmt.Printf("hash:%s\n", hash ) 196 | return 197 | } 198 | if *doVersion { 199 | fmt.Printf("Commit:%s\nDate:%s\nRemote:%s\nVersion:%s\n", GitCommit, GitDate, GitRemote, EasyVersion ) 200 | os.Exit(0) 201 | } 202 | 203 | if !fileExists("server.crt") { 204 | gen_cert() 205 | } 206 | 207 | content, _ := ioutil.ReadFile("runner.json") 208 | root, _ := uj.Parse( content ) 209 | users := root.Get("users") 210 | passmap := json_users_to_passmap( users ) 211 | secure := root.Get("https").Bool() 212 | installDir := root.Get("install_dir").String() 213 | 214 | coordPath := installDir + "/bin/coordinator" 215 | cleanup_subprocs( coordPath ) 216 | cleanup_procs() 217 | 218 | proc := proc_generic( coordPath, []string{}, installDir ) 219 | 220 | coro_sigterm( proc, coordPath ) 221 | 222 | if !fileExists( "runner.json" ) { 223 | fmt.Println("runner.json config file not present. exiting\n") 224 | os.Exit( 1 ) 225 | return 226 | } 227 | 228 | crt := "" 229 | key := "" 230 | if secure { 231 | crt = root.Get("crt").String() 232 | key = root.Get("key").String() 233 | } 234 | 235 | coro_http_server( 8021, proc, passmap, secure, crt, key, runnerVersion, root ) 236 | } -------------------------------------------------------------------------------- /runner/runner.json: -------------------------------------------------------------------------------- 1 | { 2 | "users": [ 3 | { 4 | "user": "replaceme", 5 | "pass": "e9cfcc4980e32ec6f3e8fca563f4de3c5f53766a8f0cc37accffc8ce1d99ff98" 6 | } 7 | ], 8 | "https": true, 9 | "crt": "server.crt", 10 | "key": "server.key", 11 | "updates": "/Users/user/stf_updates", 12 | "install_dir": "/Users/user/stf", 13 | "config": "/Users/user/stf/config.json", 14 | "update_host": "localhost", 15 | "update_port": 8022 16 | } 17 | -------------------------------------------------------------------------------- /runner/runnercert.tmpl: -------------------------------------------------------------------------------- 1 | [req] 2 | default_bits = 2048 3 | default_keyfile = server.key 4 | distinguished_name = req_distinguished_name 5 | req_extensions = req_ext 6 | x509_extensions = v3_ca 7 | 8 | [req_distinguished_name] 9 | countryName = Country Name (2 letter code) 10 | countryName_default = US 11 | stateOrProvinceName = State or Province Name (full name) 12 | stateOrProvinceName_default = Washington 13 | localityName = Locality Name (eg, city) 14 | localityName_default = Seattle 15 | organizationName = Organization Name (eg, company) 16 | organizationName_default = HOSTNAME 17 | organizationalUnitName = organizationalunit 18 | organizationalUnitName_default = Development 19 | commonName = Common Name (e.g. server FQDN or YOUR name) 20 | commonName_default = HOSTNAME 21 | commonName_max = 64 22 | 23 | [req_ext] 24 | subjectAltName = @alt_names 25 | 26 | [v3_ca] 27 | subjectAltName = @alt_names 28 | 29 | [alt_names] 30 | DNS.1 = HOSTNAME 31 | -------------------------------------------------------------------------------- /runner/shutdown.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/signal" 7 | "syscall" 8 | "strings" 9 | ps "github.com/jviney/go-proc" 10 | ) 11 | 12 | func coro_sigterm( proc *GenericProc, binary string ) { 13 | c := make(chan os.Signal, 2) 14 | signal.Notify(c, os.Interrupt, syscall.SIGTERM) 15 | go func() { 16 | <- c 17 | fmt.Println("\nShutdown started") 18 | 19 | // shutdown proc 20 | proc.Kill() 21 | cleanup_subprocs( binary ) 22 | 23 | fmt.Println("Shutdown finished") 24 | 25 | os.Exit(0) 26 | }() 27 | } 28 | 29 | func cleanup_procs() { 30 | // Cleanup hanging processes if any 31 | procs := ps.GetAllProcessesInfo() 32 | for _, proc := range procs { 33 | cmd := proc.CommandLine 34 | if strings.HasSuffix( cmd[0], "/bin/coordinator" ) { 35 | fmt.Printf("Leftover coordinator with PID %d. Killing\n", proc.Pid ) 36 | syscall.Kill( proc.Pid, syscall.SIGTERM ) 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /runner/update.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | //"time" 6 | "fmt" 7 | "io" 8 | "os" 9 | uj "github.com/nanoscopic/ujsonin/mod" 10 | "io/ioutil" 11 | "strings" 12 | gocmd "github.com/go-cmd/cmd" 13 | escape "github.com/gorilla/template/v0/escape" 14 | ) 15 | 16 | func writeLine( w http.ResponseWriter, f http.Flusher, str string, args ...interface{} ) { 17 | fmt.Fprintf( w, "" ) 21 | f.Flush() 22 | } 23 | 24 | func writeText( w http.ResponseWriter, f http.Flusher, str string, args ...interface{} ) { 25 | fmt.Fprintf( w, "" ) 29 | f.Flush() 30 | } 31 | 32 | func runUpdate( info Info, w http.ResponseWriter, config *uj.JNode ) { 33 | fw, ok := w.(http.Flusher) 34 | if !ok { 35 | fmt.Fprintf( w, "sadness. broken. :(" ) 36 | return 37 | } 38 | 39 | //"updates": "/Users/user/stf_updates", 40 | //"install_dir": "/Users/user/stf", 41 | //"config": "/Users/user/stf/config.json" 42 | 43 | fmt.Fprintf( w, "" ) 44 | 45 | writeLine( w, fw, "Stopping coordinator" ) 46 | info.proc.Stop() 47 | 48 | installDir := config.Get("install_dir").String() 49 | configFile := config.Get("config").String() 50 | updateHost := config.Get("update_host").String() 51 | updatePort := config.Get("update_port").String() 52 | updateUrl := "http://" + updateHost + ":" + updatePort + "/" 53 | 54 | configWithin := false 55 | if strings.HasPrefix( configFile, installDir ) { 56 | configWithin = true 57 | } 58 | 59 | configSource := configFile 60 | 61 | lineSpace := "
 
" 62 | 63 | if dirExists( installDir ) { 64 | writeLine( w, fw, "Install directory exists; erasing %s", installDir ) 65 | if configWithin { 66 | writeLine( w, fw, lineSpace + "Config file within install dir; backing up %s", configFile ) 67 | tempFile, err := ioutil.TempFile( "/tmp", "config_json" ) 68 | if err != nil { 69 | writeLine( w, fw, err.Error() ) 70 | } 71 | err = copyFileContents( configFile, tempFile.Name() ) 72 | if err != nil { 73 | writeLine( w, fw, err.Error() ) 74 | } 75 | configSource = tempFile.Name() 76 | } 77 | os.RemoveAll( installDir ) 78 | } 79 | 80 | writeLine( w, fw, "Creating install directory %s", installDir ) 81 | os.MkdirAll( installDir, 0755 ) 82 | 83 | //writeLine( w, fw, "Downloading update information" ) 84 | 85 | updateFolder := config.Get("updates").String() 86 | if !dirExists( updateFolder ) { 87 | writeLine( w, fw, "Update folder %s did not exist. Created", updateFolder ) 88 | os.MkdirAll( updateFolder, 0755 ) 89 | } 90 | 91 | updatesDest := updateFolder + "/updates.json" 92 | updatesSrc := updateUrl + "updates.json" 93 | err := download( updatesDest, updatesSrc ) 94 | if err != nil { 95 | writeLine( w, fw, "Error downloading update metadata from %s: %s\n", updatesSrc, err ) 96 | return 97 | } 98 | updateContent, _ := ioutil.ReadFile( updatesDest ) 99 | uRoot, _ := uj.Parse( updateContent ) 100 | latest := uRoot.Get("latest").String() 101 | writeLine( w, fw, "Latest update:%s", latest ) 102 | 103 | latestDest := updateFolder + "/" + latest 104 | download( latestDest, updateUrl + latest ) 105 | latestContent, _ := ioutil.ReadFile( latestDest ) 106 | lRoot, _ := uj.Parse( latestContent ) 107 | files := lRoot.Get("files") 108 | //writeLine( w, fw, "Files in update:" ) 109 | fileArr := []string{} 110 | files.ForEach( func( fileNode *uj.JNode ) { 111 | file := fileNode.String() 112 | //writeLine( w, fw, lineSpace + file ) 113 | fileArr = append( fileArr, file ) 114 | } ) 115 | 116 | writeLine( w, fw, "Downloading files:" ) 117 | for _,file := range fileArr { 118 | dest := updateFolder + "/" + file 119 | writeText( w, fw, lineSpace + file + "..." ) 120 | src := updateUrl + file 121 | download( dest, src ) 122 | writeLine( w, fw, " Done" ) 123 | } 124 | 125 | action := lRoot.Get("action").String() 126 | writeLine( w, fw, "Running install ( %s )", action ) 127 | parts := strings.Split( action, " " ) 128 | parts[0] = updateFolder + "/" + parts[0] 129 | 130 | os.Chmod( parts[0], 0770 ) 131 | cmd := gocmd.NewCmdOptions( gocmd.Options{ Streaming: true }, parts[0], parts[1:]... ) 132 | 133 | env := map[string] string { 134 | "CONFIG_SRC": configSource, 135 | "INSTALL_DIR": installDir, 136 | "UPDATE_DIR": updateFolder, 137 | } 138 | 139 | var envArr []string 140 | for k,v := range( env ) { 141 | envArr = append( envArr, k + "=" + v ) 142 | } 143 | cmd.Env = envArr 144 | 145 | statCh := cmd.Start() 146 | 147 | outStream := cmd.Stdout 148 | errStream := cmd.Stderr 149 | runDone := false 150 | for { 151 | select { 152 | case <- statCh: 153 | runDone = true 154 | case line := <- outStream: 155 | line = strings.Replace( line, "[32m", "", 1 ) 156 | line = strings.Replace( line, "[91m", "", 1 ) 157 | line = strings.Replace( line, "[0m", "", 1 ) 158 | writeLine( w, fw, line ) 159 | case line := <- errStream: 160 | writeLine( w, fw, "err:%s", line ) 161 | } 162 | if runDone { break } 163 | } 164 | 165 | writeLine( w, fw, "Install complete" ) 166 | 167 | if configWithin { 168 | os.Remove( configSource ) 169 | } 170 | 171 | writeLine( w, fw, "Starting coordinator" ) 172 | info.proc.Start() 173 | } 174 | 175 | // copyFileContents copies the contents of the file named src to the file named 176 | // by dst. The file will be created if it does not already exist. If the 177 | // destination file exists, all it's contents will be replaced by the contents 178 | // of the source file. 179 | func copyFileContents(src, dst string) (err error) { 180 | in, err := os.Open(src) 181 | if err != nil { 182 | return 183 | } 184 | defer in.Close() 185 | out, err := os.Create(dst) 186 | if err != nil { 187 | return 188 | } 189 | defer func() { 190 | cerr := out.Close() 191 | if err == nil { 192 | err = cerr 193 | } 194 | }() 195 | if _, err = io.Copy(out, in); err != nil { 196 | return 197 | } 198 | err = out.Sync() 199 | return err 200 | } 201 | 202 | func download( dest string, url string) error { 203 | resp, err := http.Get( url ) 204 | if err != nil { return err } 205 | defer resp.Body.Close() 206 | 207 | out, err := os.Create( dest ) 208 | if err != nil { return err } 209 | defer out.Close() 210 | 211 | _, err = io.Copy(out, resp.Body) 212 | return err 213 | } -------------------------------------------------------------------------------- /runner/versions.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | uj "github.com/nanoscopic/ujsonin/mod" 5 | "io/ioutil" 6 | "os/exec" 7 | "strings" 8 | "os" 9 | "fmt" 10 | ) 11 | 12 | func loadVersionInfo( vmap map[string] VersionInfo ) { 13 | if !fileExists( "bin/bins.json" ) { 14 | fmt.Printf("bin/bins.json file does not exist; cannot do extended version check") 15 | return 16 | } 17 | 18 | content, _ := ioutil.ReadFile("bin/bins.json") 19 | root, _ := uj.Parse( content ) 20 | 21 | bins := root.Get("bins") 22 | 23 | bins.ForEach( func( item *uj.JNode ) { 24 | //short := item.Get("short").String() 25 | name := item.Get("name").String() 26 | cmd := item.Get("cmd").String() 27 | 28 | arr := strings.Split( "./bin/" + cmd, " " ) 29 | out, err := exec.Command(arr[0], arr[1:]...).Output() 30 | 31 | if err != nil { 32 | fmt.Printf("err running %s : %s\n", "./bin/" + cmd, err ) 33 | return 34 | } 35 | 36 | vmap[ name ] = processVersionText( string(out) ) 37 | } ) 38 | } 39 | 40 | func processVersionText( text string ) ( VersionInfo ) { 41 | lines := strings.Split( text, "\n" ) 42 | var res VersionInfo = VersionInfo{} 43 | for _,line := range lines { 44 | if strings.HasPrefix( line, "Commit:" ) { res.GitCommit = line[7:] } 45 | if strings.HasPrefix( line, "Date:" ) { res.GitDate = line[5:] } 46 | if strings.HasPrefix( line, "Remote:" ) { res.GitRemote = line[7:] } 47 | if strings.HasPrefix( line, "Version:" ) { res.EasyVersion = line[8:] } 48 | } 49 | return res 50 | } 51 | 52 | func fileExists(filename string) bool { 53 | info, err := os.Stat(filename) 54 | if os.IsNotExist(err) { 55 | return false 56 | } 57 | return !info.IsDir() 58 | } 59 | 60 | func dirExists(filename string) bool { 61 | info, err := os.Stat(filename) 62 | if os.IsNotExist(err) { 63 | return false 64 | } 65 | return info.IsDir() 66 | } -------------------------------------------------------------------------------- /server/.env: -------------------------------------------------------------------------------- 1 | PUBLIC_IP=192.168.56.108 2 | SECRET=secret 3 | RETHINKDB_PORT_28015_TCP=tcp://rethinkdb:28015 4 | HOSTNAME=stf.test 5 | STF_IMAGE=livxtrm/devicefarmer:latest 6 | -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | ## STF Server Setup 2 | ### Set environment variables 3 | 1. `docker pull openstf/stf:v3.4.1` 4 | 1. Look through docker-compose.yml 5 | 1. Update `.env` with your environment settings 6 | 7 | 1. STF_IMAGE ( custom image if desired otherwise openstf/stf ) 8 | 1. HOSTNAME ( hostname of your server ) 9 | 1. PUBLIC_IP ( IP address of your server ) 10 | 1. Setup certificates for Nginx on your local system 11 | 12 | 1. For testing you can generate a self signed certificate using `cert/gencert.sh` 13 | 1. Pass the paths for those cert in by tweaking the mounted files in docker-compose.yml 14 | 15 | 1. eg: Change the `cert/...` parts 16 | 1. `docker-compose up` 17 | 1. If testing using self signed cert; trust the cert in your browser ( or in keychain on mac ) 18 | -------------------------------------------------------------------------------- /server/cert/gencert.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout server.key -out server.crt -config server.conf -subj "/C=US/ST=Washington/L=Seattle/O=Dis/CN=stf.test" 3 | -------------------------------------------------------------------------------- /server/cert/server.conf: -------------------------------------------------------------------------------- 1 | [req] 2 | default_bits = 2048 3 | default_keyfile = server.key 4 | distinguished_name = req_distinguished_name 5 | req_extensions = req_ext 6 | x509_extensions = v3_ca 7 | 8 | [req_distinguished_name] 9 | countryName = Country Name (2 letter code) 10 | countryName_default = US 11 | stateOrProvinceName = State or Province Name (full name) 12 | stateOrProvinceName_default = Washington 13 | localityName = Locality Name (eg, city) 14 | localityName_default = Seattle 15 | organizationName = Organization Name (eg, company) 16 | organizationName_default = stf.test 17 | organizationalUnitName = organizationalunit 18 | organizationalUnitName_default = Development 19 | commonName = Common Name (e.g. server FQDN or YOUR name) 20 | commonName_default = stf.test 21 | commonName_max = 64 22 | 23 | [req_ext] 24 | subjectAltName = @alt_names 25 | 26 | [v3_ca] 27 | subjectAltName = @alt_names 28 | 29 | [alt_names] 30 | DNS.1 = stf.test 31 | DNS.2 = 192.168.56.108 32 | -------------------------------------------------------------------------------- /server/cert/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDVDCCAjygAwIBAgIJAIGe4VrJjSC/MA0GCSqGSIb3DQEBCwUAMFUxCzAJBgNV 3 | BAYTAlVTMRMwEQYDVQQIDApXYXNoaW5ndG9uMRAwDgYDVQQHDAdTZWF0dGxlMQww 4 | CgYDVQQKDANEaXMxETAPBgNVBAMMCHN0Zi50ZXN0MB4XDTIwMDgxNDE2MjYyMVoX 5 | DTIxMDgxNDE2MjYyMVowVTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCldhc2hpbmd0 6 | b24xEDAOBgNVBAcMB1NlYXR0bGUxDDAKBgNVBAoMA0RpczERMA8GA1UEAwwIc3Rm 7 | LnRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDLaPlh0AiVEpWM 8 | Y5oErTs5Qw3UXsfJWsuEK18b8lDs+nWrHClCAb0Rp9y/oVbNVwuobZGl36oNwUmT 9 | vV4bIBjDpNUDhaJu5rN/rsFV2phIBe/qgJPqz3ty1kFkJ5aFEnTClcuPwYkjU4E+ 10 | Ed3jcOAuaCtdkSfWab8/gc+Gn9qL1nAFz3qG5Bbej4SExKI7yp3WRNM4hUjppH5j 11 | EiO8Ti5Jp2d+1Ahzxjc7DQjIkYcCA3KNNXQrzB3GrEq9j5D4sYW6x8NVeqLr7olB 12 | a4ylfEpxDafzakIdtxezFKyElmYenai/DHOAciH7X7XMBHk9IHUa0eCzpjYr5GHa 13 | nujFUpVvAgMBAAGjJzAlMCMGA1UdEQQcMBqCCHN0Zi50ZXN0gg4xOTIuMTY4LjU2 14 | LjEwODANBgkqhkiG9w0BAQsFAAOCAQEAJTAclMsxxD3H/AEo2NregQSQ1p5ET+w6 15 | Mc34GlGa/jycrBL23iXPJQZj0TE/XEiJmjm1BbwTDXJFNTSBAp7+ufnmtnaYPL+o 16 | BEY8VvjCoTek7xmipVO7z2NoeDb+UHVn8PSCXWiDdCR2yY+c2qdTHzZtLUr2w/80 17 | imP1+bl9QS6vnXvc5g0BadtBcxZshjK0OtsnNfsQXmxeY0Y3X0YHpt1D8ZvyD4gf 18 | IjulHgrEWPmDG/dYA/k8ZJfgbRchkzkilmG36lHRpKQ8aIAafJaQp/Z2KaOvyFbv 19 | CGXSfkeMmEQU+hRq1ccbglARkWxRNGkg1aOQmdNEnK49QBREq5FLcA== 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /server/cert/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDLaPlh0AiVEpWM 3 | Y5oErTs5Qw3UXsfJWsuEK18b8lDs+nWrHClCAb0Rp9y/oVbNVwuobZGl36oNwUmT 4 | vV4bIBjDpNUDhaJu5rN/rsFV2phIBe/qgJPqz3ty1kFkJ5aFEnTClcuPwYkjU4E+ 5 | Ed3jcOAuaCtdkSfWab8/gc+Gn9qL1nAFz3qG5Bbej4SExKI7yp3WRNM4hUjppH5j 6 | EiO8Ti5Jp2d+1Ahzxjc7DQjIkYcCA3KNNXQrzB3GrEq9j5D4sYW6x8NVeqLr7olB 7 | a4ylfEpxDafzakIdtxezFKyElmYenai/DHOAciH7X7XMBHk9IHUa0eCzpjYr5GHa 8 | nujFUpVvAgMBAAECggEAPu9CwY2xKhZu6NnkTHAgs83YWI3euKD7+O/GZIormbbA 9 | c2mqJj8NdYn/VdcgWTYGaF1GRBEYt1rHXguoMzJSFy5HrehJ4pBEl0vFi7+vgBE+ 10 | MssHeQ4q/tPltYw+GPwl3hKkwdy6hpCOm1rB0V4aLqGSUUfZEJD1WDvcrqWE4+Cu 11 | 2Kl8SqsDmUyvWgpQJjVQ+X9mQEsrSebNK/s7vBl1VX5K3RHWW5u2oSg1lgdxYTmM 12 | wUBO/maV+68bZWl3VUfJmwdlenoVahzyUOtxNy36kapqVe1pmq7QMtAkBhemtrw6 13 | rrdCWyjl8mH96rtO8O+mXvkCnuiBurfcmpIpyLwzeQKBgQDpJid/tqZvmzlVNmQi 14 | F4WMmsnj7QwfhkCI2AdnUAy5LtWumcVeQVutq3jHidLkJxTEFDK85xcF/++oftMp 15 | NFdEErt71fA9GQTLg7a7OeyY9eR/gelZNMqEnjjUGKzfj4DyM4WsqTgOZsfb4yPz 16 | GTtj2B7Xf50LjFdPTDXHgptI2wKBgQDfWKa+OTPQpPAj4kWoOEzN4Sr4Y4gQioO/ 17 | ksnlmhUgBkhoHVGV1u742mXSpVYRzyQv6H1kLGJRTkEFHl3ozJUneWXmFY7eUNTn 18 | fnuPss1yLuE+wqLJ+iB9mSQGgBdetY5ynB3cYvVup5hDmSV8lHP+Jp3BTdpEx5pp 19 | xyancSBP/QKBgD68oJZSLNkNWNEgMLOnxqz+HeNyLvfwpT7tepiHRtUx0BgKkrx5 20 | M9U4tehjotb32TOmB70jJePcab3aWrHUvsK3k7GP8PRP3iVxTON2g77pM9JHv+Xc 21 | Ob6T4NDZzvLdZ6JE0OyUIFxntdHqfgr1ODD2v93XHgg0fG3/IN2NvIFPAoGBAJBz 22 | 0uyHLLcOZm6fAzRorWwe7N7X6QHhxJJcCw7gGDetOJl2FPVXnRoAjwitfLxp/9qo 23 | gKkQd8pkVXNND6no37M3NiuY19175CeRS7NGDtCB95bS5dzCVM9HA+DcacEMpgQE 24 | at/GdTzLUpSt8WvgzCCdszx58Oi5PGqbrqlvZlm1AoGAWvUHqRIVN5sy6IJPCBoh 25 | SSlSZxPK9hnVnBvWUHxGN3IX9YiAfAx2fpAMC8glR2aKATtKVOWxMgQ2YJ/ZBk7p 26 | SM3D3v5D2RlmLXy7ulvslF+KxGHJWmDxQ4Zuaqiq7+MAEaAwq1A80MYjqqwWnlQH 27 | ZBWwH6vFWgJn7gps7/gW1aE= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /server/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | volumes: 4 | rethinkdb: 5 | storage-temp: 6 | 7 | services: 8 | nginx: 9 | build: nginx/ 10 | volumes: 11 | - ./nginx/nginx.conf:/etc/nginx/nginx.conf 12 | # You'll need to set the paths below to where your certs actually are 13 | - ./cert/server.crt:/etc/nginx/ssl/cert.crt 14 | - ./cert/server.key:/etc/nginx/ssl/cert.key 15 | restart: unless-stopped 16 | ports: 17 | - 80:80 18 | - 443:443 19 | depends_on: 20 | - app 21 | - auth 22 | - storage-plugin-apk 23 | - storage-plugin-image 24 | - storage-temp 25 | - websocket 26 | - api 27 | rethinkdb: 28 | image: rethinkdb:2.3 29 | restart: unless-stopped 30 | ports: 31 | - 8080:8080 32 | volumes: 33 | - rethinkdb:/data 34 | app: 35 | image: ${STF_IMAGE} 36 | restart: unless-stopped 37 | environment: 38 | - RETHINKDB_PORT_28015_TCP 39 | - SECRET 40 | command: > 41 | node runcli.js app 42 | --auth-url https://${HOSTNAME}/auth/mock/ 43 | --websocket-url wss://${HOSTNAME}/ --port 3000 44 | volumes: 45 | - ./runcli.js:/app/runcli.js 46 | ports: 47 | - 10006:9229 48 | depends_on: 49 | - rethinkdb 50 | - auth 51 | - websocket 52 | auth: 53 | image: ${STF_IMAGE} 54 | restart: unless-stopped 55 | volumes: 56 | - ./runcli.js:/app/runcli.js 57 | environment: 58 | - SECRET 59 | - RETHINKDB_PORT_28015_TCP 60 | command: node runcli.js auth-mock --app-url http://${HOSTNAME}/ --port 3000 61 | processor: 62 | image: ${STF_IMAGE} 63 | restart: unless-stopped 64 | environment: 65 | - RETHINKDB_PORT_28015_TCP 66 | command: > 67 | node runcli.js processor 68 | --connect-app-dealer tcp://triproxy:7160 69 | --connect-dev-dealer tcp://dev-triproxy:7260 70 | volumes: 71 | - ./runcli.js:/app/runcli.js 72 | ports: 73 | - 10002:9229 74 | depends_on: 75 | - rethinkdb 76 | - triproxy 77 | - dev-triproxy 78 | triproxy: 79 | image: ${STF_IMAGE} 80 | restart: unless-stopped 81 | command: > 82 | node runcli.js triproxy app 83 | --bind-pub "tcp://*:7150" 84 | --bind-dealer "tcp://*:7160" 85 | --bind-pull "tcp://*:7170" 86 | volumes: 87 | - ./runcli.js:/app/runcli.js 88 | ports: 89 | - 10005:9229 90 | dev-triproxy: 91 | image: ${STF_IMAGE} 92 | restart: unless-stopped 93 | command: > 94 | node runcli.js triproxy dev 95 | --bind-pub "tcp://*:7250" 96 | --bind-dealer "tcp://*:7260" 97 | --bind-pull "tcp://*:7270" 98 | volumes: 99 | - ./runcli.js:/app/runcli.js 100 | ports: 101 | - 7250:7250 102 | - 7270:7270 103 | - 10003:9229 104 | migrate: 105 | image: ${STF_IMAGE} 106 | environment: 107 | - RETHINKDB_PORT_28015_TCP 108 | volumes: 109 | - ./runcli.js:/app/runcli.js 110 | command: node runcli.js migrate 111 | depends_on: 112 | - rethinkdb 113 | reaper: 114 | image: ${STF_IMAGE} 115 | restart: unless-stopped 116 | environment: 117 | - RETHINKDB_PORT_28015_TCP 118 | depends_on: 119 | - migrate 120 | - rethinkdb 121 | - dev-triproxy 122 | - triproxy 123 | volumes: 124 | - ./runcli.js:/app/runcli.js 125 | command: > 126 | node runcli.js reaper dev 127 | --connect-push tcp://dev-triproxy:7270 128 | --connect-sub tcp://triproxy:7150 129 | --heartbeat-timeout 30000 130 | storage-plugin-apk: 131 | image: ${STF_IMAGE} 132 | restart: unless-stopped 133 | volumes: 134 | - ./runcli.js:/app/runcli.js 135 | command: node runcli.js storage-plugin-apk --port 3000 --storage-url http://${PUBLIC_IP}/ 136 | depends_on: 137 | - storage-temp 138 | storage-plugin-image: 139 | image: ${STF_IMAGE} 140 | restart: unless-stopped 141 | volumes: 142 | - ./runcli.js:/app/runcli.js 143 | command: node runcli.js storage-plugin-image --port 3000 --storage-url http://${PUBLIC_IP}/ 144 | depends_on: 145 | - storage-temp 146 | storage-temp: 147 | build: storage-temp/ 148 | restart: unless-stopped 149 | volumes: 150 | - storage-temp:/app/data 151 | - ./runcli.js:/app/runcli.js 152 | command: node runcli.js storage-temp --port 3000 --save-dir /app/data 153 | websocket: 154 | image: ${STF_IMAGE} 155 | restart: unless-stopped 156 | environment: 157 | - SECRET 158 | - RETHINKDB_PORT_28015_TCP 159 | command: > 160 | node runcli.js 161 | websocket 162 | --port 3000 163 | --storage-url "http://${PUBLIC_IP}/" 164 | --connect-sub "tcp://triproxy:7150" 165 | --connect-push "tcp://triproxy:7170" 166 | volumes: 167 | - ./runcli.js:/app/runcli.js 168 | ports: 169 | - 10004:9229 170 | depends_on: 171 | - migrate 172 | - rethinkdb 173 | - storage-temp 174 | - triproxy 175 | - dev-triproxy 176 | api: 177 | image: ${STF_IMAGE} 178 | restart: unless-stopped 179 | environment: 180 | - SECRET 181 | - RETHINKDB_PORT_28015_TCP 182 | command: > 183 | node runcli.js 184 | api 185 | --port 3000 186 | --connect-sub tcp://triproxy:7150 187 | --connect-push tcp://triproxy:7170 188 | --connect-sub-dev tcp://dev-triproxy:7250 189 | --connect-push-dev tcp://dev-triproxy:7270 190 | ports: 191 | - 10001:9229 192 | volumes: 193 | - ./runcli.js:/app/runcli.js 194 | ports: 195 | - 9229:9229 196 | depends_on: 197 | - migrate 198 | - rethinkdb 199 | - triproxy 200 | - dev-triproxy 201 | 202 | -------------------------------------------------------------------------------- /server/nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:mainline 2 | 3 | COPY ./entrypoint.sh / 4 | RUN chmod +x /entrypoint.sh 5 | 6 | CMD ["/entrypoint.sh"] 7 | -------------------------------------------------------------------------------- /server/nginx/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | NAMESERVER=$(awk '/nameserver/{print $2}' /etc/resolv.conf | tr '\\n' ' ') 4 | RESOLVER_CONFIG="/etc/nginx/conf.d/resolver.conf" 5 | echo Got nameserver $NAMESERVER from resolv.conf 6 | echo Writing include file at $RESOLVER_CONFIG 7 | echo "resolver $NAMESERVER;" > $RESOLVER_CONFIG 8 | nginx -g 'daemon off;' 9 | -------------------------------------------------------------------------------- /server/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | worker_processes auto; 2 | 3 | events { 4 | worker_connections 1024; 5 | } 6 | 7 | http { 8 | include /etc/nginx/conf.d/resolver.conf; 9 | keepalive_timeout 65; 10 | types_hash_max_size 2048; 11 | 12 | default_type application/octet-stream; 13 | 14 | upstream stf_app { 15 | server app:3000 max_fails=0; 16 | } 17 | 18 | upstream stf_auth { 19 | server auth:3000 max_fails=0; 20 | } 21 | 22 | upstream stf_storage_apk { 23 | server storage-plugin-apk:3000 max_fails=0; 24 | } 25 | 26 | upstream stf_storage_image { 27 | server storage-plugin-image:3000 max_fails=0; 28 | } 29 | 30 | upstream stf_storage { 31 | server storage-temp:3000 max_fails=0; 32 | } 33 | 34 | upstream stf_websocket { 35 | server websocket:3000 max_fails=0; 36 | } 37 | 38 | upstream stf_api { 39 | server api:3000 max_fails=0; 40 | } 41 | 42 | types { 43 | application/javascript js; 44 | image/gif gif; 45 | image/jpeg jpg; 46 | text/css css; 47 | text/html html; 48 | } 49 | 50 | map $http_upgrade $connection_upgrade { 51 | default upgrade; 52 | '' close; 53 | } 54 | 55 | server { 56 | listen 80; 57 | server_name _; 58 | return 301 https://$host$request_uri; 59 | } 60 | 61 | 62 | server { 63 | listen 443 ssl; 64 | server_name _; 65 | 66 | ssl_certificate /etc/nginx/ssl/cert.crt; 67 | ssl_certificate_key /etc/nginx/ssl/cert.key; 68 | ssl_session_timeout 5m; 69 | ssl_session_cache shared:SSL:10m; 70 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2; 71 | 72 | server_tokens off; 73 | root /dev/null; 74 | 75 | # Client IP should be changed from [^/] to some more specific range such as: 76 | # (?192.168.255.[0-9]+) to restrict it to a reasonable IP range 77 | # If left alone this example config will let clients arbitrarily tunnel to any IP on ports 8000-8009 78 | location ~ "^/frames/(?[^/]+)/(?800[0-9])/x$" { 79 | proxy_pass http://$client_ip:$client_port/echo/; 80 | proxy_http_version 1.1; 81 | proxy_set_header Upgrade $http_upgrade; 82 | proxy_set_header Connection $connection_upgrade; 83 | proxy_set_header X-Forwarded-For $remote_addr; 84 | proxy_set_header X-Real-IP $remote_addr; 85 | } 86 | 87 | location /auth/ { 88 | proxy_pass http://stf_auth/auth/; 89 | } 90 | 91 | location /api/ { 92 | proxy_pass http://stf_api/api/; 93 | } 94 | 95 | location /s/image/ { 96 | proxy_pass http://stf_storage_image; 97 | } 98 | 99 | location /s/apk/ { 100 | proxy_pass http://stf_storage_apk; 101 | } 102 | 103 | location /s/ { 104 | client_max_body_size 1024m; 105 | client_body_buffer_size 128k; 106 | proxy_pass http://stf_storage; 107 | } 108 | 109 | location /socket.io/ { 110 | proxy_pass http://stf_websocket; 111 | proxy_http_version 1.1; 112 | proxy_set_header Upgrade $http_upgrade; 113 | proxy_set_header Connection $connection_upgrade; 114 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 115 | proxy_set_header X-Real-IP $http_x_real_ip; 116 | } 117 | 118 | # This .well-known path is mapped to make it easier to use letsencrypt for certs 119 | location /.well-known/ { 120 | } 121 | 122 | location / { 123 | proxy_pass http://stf_app; 124 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 125 | proxy_set_header X-Real-IP $http_x_real_ip; 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /server/runcli.js: -------------------------------------------------------------------------------- 1 | require('./lib/cli') 2 | -------------------------------------------------------------------------------- /server/storage-temp/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM livxtrm/devicefarmer:latest 2 | 3 | USER root 4 | RUN mkdir data && chown stf:stf data 5 | USER stf 6 | VOLUME ["data"] 7 | -------------------------------------------------------------------------------- /stf_ios_support.rb: -------------------------------------------------------------------------------- 1 | class StfIosSupport < Formula 2 | desc "OpenSTF IOS Device Provider" 3 | homepage "" 4 | url "https://github.com/nanoscopic/empty/archive/empty.tar.gz" 5 | version "1.0.0" 6 | sha256 "324c7d7662fd392fa2b7e0c9ce2bb9fd2ff677403c31311b13ac64bd1a15cbf7" 7 | 8 | def install 9 | system "touch #{prefix}/intentionally_empty_install" 10 | end 11 | 12 | # depends_on "cmake" => :build 13 | depends_on "jq" 14 | # depends_on "rethinkdb" 15 | depends_on "graphicsmagick" 16 | depends_on "zeromq" 17 | depends_on "protobuf" 18 | depends_on "yasm" 19 | depends_on "pkg-config" 20 | depends_on "carthage" 21 | depends_on "automake" 22 | depends_on "autoconf" 23 | depends_on "libtool" 24 | depends_on "wget" 25 | # depends_on "libimobiledevice" # need to install with --HEAD 26 | depends_on "go" => :build 27 | depends_on :xcode => "10.3" 28 | depends_on "node@12" 29 | depends_on "libsodium" 30 | depends_on "czmq" 31 | depends_on "jpeg-turbo" 32 | depends_on "nanomsg" 33 | depends_on "libgcrypt" 34 | depends_on "gnutls" 35 | depends_on "mobiledevice" 36 | # depends_on "libplist" # need to install with --HEAD 37 | # depends_on "libusbmuxd" # need to install with --HEAD 38 | end 39 | -------------------------------------------------------------------------------- /tblick-info.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | CONFNAME="$1" 4 | VPERR="" 5 | 6 | # Junk below mostly copied from Tunnelblick client.2.up.tunnelblick.sh 7 | TBCONFIG="/Users/$USER/Library/Application Support/Tunnelblick/Configurations/$1.tblk/Contents/Resources/config.ovpn" 8 | if [ ! -f "$TBCONFIG" ]; then 9 | TBCONFIG="/Library/Application Support/Tunnelblick/Shared/$1.tblk/Contents/Resources/config.ovpn" 10 | fi 11 | 12 | if [ -f "$TBCONFIG" ]; then 13 | TBALTPREFIX="/Library/Application Support/Tunnelblick/Users/" 14 | TBALTPREFIXLEN="${#TBALTPREFIX}" 15 | TBCONFIGSTART="${TBCONFIG:0:$TBALTPREFIXLEN}" 16 | if [ "$TBCONFIGSTART" = "$TBALTPREFIX" ] ; then 17 | TBBASE="${TBCONFIG:$TBALTPREFIXLEN}" 18 | TBSUFFIX="${TBBASE#*/}" 19 | TBUSERNAME="${TBBASE%%/*}" 20 | TBCONFIG="/Users/$TBUSERNAME/Library/Application Support/Tunnelblick/Configurations/$TBSUFFIX" 21 | fi 22 | 23 | CONFIG_PATH_DASHES_SLASHES="$(echo "${TBCONFIG}" | sed -e 's/-/--/g' | sed -e 's/\//-S/g')" 24 | 25 | # Determine IP adddress and Tunnel name of most recent connection 26 | LF=$(find "/Library/Application Support/Tunnelblick/Logs/" -type f -iname "${CONFIG_PATH_DASHES_SLASHES}*.openvpn.log") 27 | if [ -f "$LF" ]; then 28 | LINE=$(cat "$LF" | grep -E "ifconfig utun[0-9]+ [0-9]+" | tail -1) 29 | if [ "$LINE" == "" ]; then 30 | VPERR="Config '$CONFNAME' does not appear to have ever connected" 31 | else 32 | IPADDR=$(echo $LINE| cut -d \ -f 5) 33 | TUN=$(echo $LINE| cut -d \ -f 4) 34 | fi 35 | else 36 | VPERR="Config '$CONFNAME' does not have any log" 37 | fi 38 | else 39 | VPERR="Config '$CONFNAME' does not appear to exist in Tunnelblick" 40 | fi 41 | 42 | echo "{" 43 | echo " \"err\": \"$VPERR\"," 44 | echo " \"ipAddr\": \"$IPADDR\"," 45 | echo " \"tunName\": \"$TUN\"" 46 | echo "}" 47 | -------------------------------------------------------------------------------- /temp/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dryark/stf_ios_support/2f32cbbf1506a740eb21a496c1cdc11d875558f7/temp/.gitkeep -------------------------------------------------------------------------------- /update_server/Makefile: -------------------------------------------------------------------------------- 1 | TARGET = server 2 | 3 | all: $(TARGET) 4 | 5 | runner_sources := $(wildcard *.go) 6 | 7 | $(TARGET): $(runner_sources) go.sum 8 | go build -o $(TARGET) -ldflags "-X main.GitCommit=$(GIT_COMMIT) -X main.GitDate=$(GIT_DATE) -X main.GitRemote=$(GIT_REMOTE) -X main.EasyVersion=$(EASY_VERSION)" . 9 | 10 | go.sum: 11 | go get 12 | go get . 13 | 14 | clean: 15 | $(RM) $(TARGET) go.sum -------------------------------------------------------------------------------- /update_server/go.mod: -------------------------------------------------------------------------------- 1 | module main.go 2 | 3 | go 1.12 4 | 5 | require ( 6 | ) 7 | -------------------------------------------------------------------------------- /update_server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "fmt" 6 | ) 7 | 8 | func main() { 9 | fs := http.FileServer( http.Dir( "./updates" ) ) 10 | fmt.Println( http.ListenAndServe( ":8022", fs ) ) 11 | } -------------------------------------------------------------------------------- /update_server/updates/index.html: -------------------------------------------------------------------------------- 1 | updates
2 | -------------------------------------------------------------------------------- /update_server/updates/remote.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | use strict; 3 | use File::Copy; 4 | 5 | my $configSrc = $ENV{CONFIG_SRC} or die "ENV CONFIG_SRC not set"; 6 | my $installDir = $ENV{INSTALL_DIR} or die "ENV INSTALL_DIR not set"; 7 | my $updateDir = $ENV{UPDATE_DIR} or die "ENV UPDATE_DIR not set"; 8 | 9 | my $dist = $ARGV[0]; 10 | 11 | print "Extracting $dist to $installDir\n"; 12 | 13 | if( ! -e "/usr/local/bin/pv" ) { 14 | print "pv not installed. installing...\n"; 15 | system("/usr/local/bin/brew install pv"); 16 | } 17 | 18 | print STDERR "PROGSTART\n"; 19 | system("/usr/local/bin/pv -n $updateDir/$dist | /usr/bin/tar -xf - -C $installDir"); 20 | print STDERR "PROGEND\n"; 21 | 22 | my $configDest = "$installDir/config.json"; 23 | print "Copying $configSrc to $configDest\n"; 24 | copy( $configSrc, $configDest ); 25 | 26 | chdir $installDir; 27 | $ENV{'PATH'} = "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"; 28 | system('./init.sh'); -------------------------------------------------------------------------------- /update_server/updates/updates.json: -------------------------------------------------------------------------------- 1 | { 2 | "latest": "v1.json" 3 | } 4 | -------------------------------------------------------------------------------- /update_server/updates/v1.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "remote.pl", 4 | "v1.tgz" 5 | ], 6 | "action": "remote.pl v1.tgz", 7 | "provides": [ 8 | [ "coordinator", "1" ] 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /util/alf2.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | use strict; 3 | use Data::Dumper; 4 | use MIME::Base64; 5 | use JSON::PP; 6 | 7 | my $checkapp = ''; 8 | my $action = $ARGV[0] || ''; 9 | if( $action eq 'permok' || $action eq 'ensureperm' ) { 10 | $checkapp = $ARGV[1] || "/Applications/STF Coordinator.app"; 11 | } 12 | elsif( $action eq 'dump' ) {} 13 | else { 14 | print "ALF Firewall / Network Permissions Tool\n 15 | Usage: 16 | ./alf2.pl [action] [args] 17 | Actions: 18 | dump - dump contents of permissions 19 | permok [path to app or binary] - check if a app or binary has permission 20 | ensureperm [path to app or binary] - ensure an app or binary has permission\n"; 21 | } 22 | 23 | my $i = 1; 24 | my $app; 25 | my %apps; 26 | for my $line ( `/usr/libexec/ApplicationFirewall/socketfilterfw --listapps` ) { 27 | if( $line =~ m/^$i\s+:\s+(.+?)\s*$/ ) { 28 | $app = $1; 29 | $i++; 30 | } 31 | 32 | if( $line =~ m/Allow incoming connections/ ) { 33 | if( !$checkapp || $checkapp eq $app ) { 34 | my $csreq = get_cs_req( $i - 2 ); 35 | chomp $csreq; 36 | my $info = { 37 | csreq => $csreq, 38 | valid => 1 39 | }; 40 | if( $csreq =~ m/cdhash H"(.+?)"/ ) { 41 | my $sha1 = uc($1); 42 | my $cdhash = get_cdhash( $app ); 43 | $info->{ valid } = ( $sha1 eq $cdhash ) ? 1 : 0; 44 | } 45 | $apps{ $app } = $info; 46 | } 47 | else { 48 | $apps{ $app } = {}; 49 | } 50 | } 51 | } 52 | 53 | if( $action eq 'permok' ) { 54 | my $info = $apps{ $checkapp }; 55 | print $info->{valid} ? 'yes' : 'no'; 56 | } 57 | elsif( $action eq 'ensureperm' ) { 58 | # TODO 59 | } 60 | elsif( $action eq 'dump' ) { 61 | print JSON::PP->new->ascii->pretty->encode( \%apps ); 62 | } 63 | 64 | sub get_cs_req { 65 | my $i = shift; 66 | my $grab = 0; 67 | my $b64; 68 | for my $line ( `plutil -extract applications.$i.reqdata xml1 -o - /Library/Preferences/com.apple.alf.plist` ) { 69 | if( $line =~ m// ) { 70 | $grab = 1; 71 | next; 72 | } 73 | if( $grab ) { 74 | $b64 = $line; 75 | last; 76 | } 77 | } 78 | my $raw = decode_base64( $b64 ); 79 | open( my $fh, ">csreq.bin" ); 80 | binmode( $fh ); 81 | print $fh $raw; 82 | my $csreq = `csreq -r csreq.bin -t`; 83 | close( $fh ); 84 | unlink( "csreq.bin" ); 85 | return $csreq; 86 | } 87 | 88 | sub get_cdhash { 89 | my $path = shift; 90 | for my $line ( `codesign -dvvv "$path" 2>&1` ) { 91 | if( $line =~ m/^CDHash=(.+?)\s*$/ ) { 92 | return uc($1); 93 | } 94 | } 95 | return ''; 96 | } -------------------------------------------------------------------------------- /util/brewser.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | use strict; 3 | use JSON::PP qw/decode_json/; 4 | use Data::Dumper; 5 | use Carp qw/confess/; 6 | 7 | my $GR="\033[32m"; 8 | my $RED="\033[91m"; 9 | my $RST="\033[0m"; 10 | my $action = $ARGV[0] || 'help'; 11 | 12 | if( !`which brew` ) { 13 | print "Brew must be installed\n"; 14 | help(); 15 | exit(1); 16 | } 17 | 18 | if( $action eq 'list' ) { 19 | my $pkgs = get_pkg_versions(); 20 | for my $pkg ( keys %$pkgs ) { 21 | my $ver = $pkgs->{$pkg}; 22 | print "$pkg,$ver\n"; 23 | } 24 | } 25 | elsif( $action eq 'installdeps' ) { 26 | my $rbspec = $ARGV[1] or die "Ruby spec file must be given"; 27 | my $spec = read_file( $rbspec ); 28 | my $pkgs = get_pkg_versions(); 29 | my @need; 30 | for my $line ( split( "\n", $spec ) ) { 31 | if( $line =~ m/^\s*depends_on "(.+?)"/ ) { 32 | my $dep = $1; 33 | if( my $ver = $pkgs->{ $dep } ) { 34 | print "$GR$dep\t\t=> version $ver$RST\n"; 35 | } 36 | else { 37 | push( @need, $dep ); 38 | } 39 | } 40 | } 41 | if( @need ) { 42 | my $allneed = join(' ',@need); 43 | print "Installing missing packages:\n"; 44 | print " ".join("\n ",@need); 45 | `brew install $allneed 1>&2`; 46 | } 47 | } 48 | elsif( $action eq 'checkdeps' ) { 49 | my $rbspec = $ARGV[1] or die "Ruby spec file must be given"; 50 | my $spec = read_file( $rbspec ); 51 | my $pkgs = get_pkg_versions(); 52 | my @need; 53 | for my $line ( split( "\n", $spec ) ) { 54 | if( $line =~ m/^\s*depends_on "(.+?)"/ ) { 55 | my $dep = $1; 56 | if( my $ver = $pkgs->{ $dep } ) { 57 | print "$GR$dep\t\t=> version $ver$RST\n"; 58 | } 59 | else { 60 | push( @need, $dep ); 61 | } 62 | } 63 | } 64 | if( @need ) { 65 | my $allneed = join(' ',@need); 66 | print "Missing brew package(s):\n"; 67 | print " ".join("\n ",@need); 68 | } 69 | } 70 | elsif( $action eq 'info' ) { 71 | my $pkg = $ARGV[1]; 72 | my ( $info, $ver ) = install_info( $pkg ); 73 | if( !$info ) { 74 | print "$pkg is not installed\n"; 75 | exit 1; 76 | } 77 | print JSON::PP->new->ascii->pretty->encode( $info ); 78 | if( $ver =~ m/HEAD/ ) { 79 | my $headVersion = head_version( $pkg ); 80 | print "HEAD version = $headVersion\n"; 81 | } 82 | } 83 | elsif( $action eq 'ensurehead' ) { 84 | ensure_head( $ARGV[1], $ARGV[2] || '' ); 85 | } 86 | elsif( $action eq 'fixpc' ) { 87 | fix_pc( $ARGV[1], $ARGV[2] ); 88 | } 89 | else { 90 | help(); 91 | } 92 | 93 | sub help { 94 | print "Brewser 95 | Usage: 96 | ./brewser.pl [action] [args] 97 | Actions: 98 | list - list packages and versions installed 99 | info [package name] - pretty print json install receipt of named package 100 | ensurehead [package name] - ensure HEAD version of a package is installed 101 | If a non-HEAD version is installed, it will be removed and the current HEAD installed. 102 | If a HEAD version is installed, even if old, nothing will happen. 103 | installdeps [ruby spec file] - install dependencies for a specified brew package spec file 104 | fixpc [package name] [version] - Ensure both [pkg].pc and [pkg]-[ver].pc exist\n"; 105 | } 106 | sub get_pkg_versions { 107 | my %pkgs; 108 | my @dirs = sort `find /usr/local/Cellar -name .brew -maxdepth 3 -type d`; 109 | for my $dir ( @dirs ) { 110 | $pkgs{ $1 } = $2 if( $dir =~ m|^/usr/local/Cellar/([^/]+)/([^/]+)/\.brew$| ); 111 | } 112 | return \%pkgs; 113 | } 114 | 115 | sub read_file { 116 | my $file = shift; 117 | open( my $fh, "<$file" ) or confess("Could not open $file"); 118 | my $data; 119 | { local $/ = undef; $data = <$fh>; } 120 | close( $fh ); 121 | return $data; 122 | } 123 | 124 | sub install_info { 125 | my ( $pkg, $ver ) = @_; 126 | if( !$ver ) { 127 | my $path = `find /usr/local/Cellar/$pkg -maxdepth 1 2>/dev/null | tail -1`; 128 | chomp $path; 129 | return 0 if( !$path ); 130 | my @parts = split( "/", $path ); 131 | $ver = pop @parts; 132 | } 133 | my $receiptFile = "/usr/local/Cellar/$pkg/$ver/INSTALL_RECEIPT.json"; 134 | #print "Checking $receiptFile\n"; 135 | return 0 if( ! -e $receiptFile ); 136 | return decode_json( read_file( $receiptFile ) ), $ver; 137 | } 138 | 139 | sub files { 140 | my $path = shift; 141 | opendir( my $DIR, $path ); 142 | my @files = readdir( $DIR ); 143 | my @outfiles; 144 | for my $file ( @files ) { 145 | next if( $file =~ m/^\.+$/ ); 146 | push( @outfiles, $file ); 147 | } 148 | closedir( $DIR ); 149 | return @outfiles; 150 | } 151 | 152 | sub pkg_pc_file { 153 | my ( $pkg ) = @_; 154 | #my $pc = "/usr/local/lib/pkgconfig/$pkg.pc"; 155 | my $path = `find /usr/local/Cellar/$pkg -maxdepth 1 2>/dev/null | tail -1`; 156 | chomp $path; 157 | return 0 if( !$path ); 158 | my $pcPath = "$path/lib/pkgconfig/"; 159 | my @pcFiles = files( $pcPath ); 160 | my $pc = ""; 161 | for my $pcFile ( @pcFiles ) { 162 | if( $pcFile =~ m/$pkg(\-|\.)/ ) { 163 | $pc = "$pcPath/$pcFile"; 164 | last; 165 | } 166 | } 167 | return 0 if( !$pc ); 168 | return $pc; 169 | } 170 | 171 | sub head_version { 172 | my $pkg = shift; 173 | my $pc = pkg_pc_file( $pkg ); 174 | return 0 if( !$pc ); 175 | my $version = `cat $pc | grep Version | cut -d\\ -f2`; 176 | chomp $version; 177 | return $version; 178 | } 179 | 180 | sub ensure_head { 181 | my ( $pkg, $ver ) = @_; 182 | my ( $info, $iv ) = install_info( $pkg ); 183 | my $spec = $info ? $info->{source}{spec} : ''; 184 | if( !$spec || $spec ne 'head' ) { 185 | print "$pkg - Installing HEAD\n"; 186 | `brew uninstall $pkg --ignore-dependencies` if( $spec ); 187 | `brew install --HEAD $pkg`; 188 | } 189 | else { 190 | print "$GR$pkg - HEAD already installed$RST\n"; 191 | if( $ver ) { 192 | my $installedVer = head_version( $pkg ); 193 | my $greater = version_gte( $ver, $installedVer ); 194 | if( !$greater ) { 195 | print "Installed HEAD version is $installedVer; need $ver\n"; 196 | `brew uninstall $pkg --ignore-dependencies`; 197 | `brew install --HEAD $pkg`; 198 | } 199 | else { 200 | if( $greater == 1 ) { print "$GR$pkg - installed HEAD is version ${installedVer} ( ==$ver )$RST\n"; } 201 | elsif( $greater == 2 ) { print "$GR$pkg - installed HEAD is version ${installedVer} ( >$ver )$RST\n"; } 202 | } 203 | } 204 | } 205 | } 206 | 207 | sub fix_pc { 208 | my ( $pkg, $ver ) = @_; 209 | my $f1 = "/usr/local/lib/pkgconfig/$pkg.pc"; 210 | my $f2 = "/usr/local/lib/pkgconfig/$pkg-$ver.pc"; 211 | 212 | my $pc = pkg_pc_file( $pkg ); 213 | if( !$pc ) { 214 | print "Could not fix pkgconfig for $pkg; could not locate installed pc file in Cellar\n"; 215 | return 216 | } 217 | if( ! -e $f2 ) { 218 | print "$f2 was missing; creating symlink to $pc\n"; 219 | `ln -s $pc $f2`; 220 | } 221 | if( ! -e $f1 ) { 222 | print "$f1 was missing; creating symlink to $pc\n"; 223 | `ln -s $pc $f1`; 224 | } 225 | } 226 | 227 | sub version_gte { 228 | my ( $v1, $v2 ) = @_; 229 | my @p1 = split(/\./,$v1); 230 | my @p2 = split(/\./,$v2); 231 | for( my $i=0; $i<3; $i++ ) { 232 | my $n1 = $p1[ $i ]; 233 | my $n2 = $p2[ $i ]; 234 | #print "Comparing $n1 $n2\n"; 235 | return 2 if( $n2 > $n1 ); 236 | return 0 if( $n2 < $n1 ); 237 | } 238 | return 1; 239 | } -------------------------------------------------------------------------------- /util/signers.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | use strict; 3 | use File::Temp qw/tempfile/; 4 | use Data::Dumper; 5 | 6 | my $action = $ARGV[0] || ''; 7 | 8 | my $goalou = ""; 9 | my $tosign = ""; 10 | if( $action eq "sign" ) { 11 | $goalou = $ARGV[1]; 12 | $tosign = $ARGV[2]; 13 | } 14 | my $found = 0; 15 | 16 | my $rawcerts = `security find-certificate -a -p -Z`; 17 | $rawcerts .= "SHA-1 hash: 000"; 18 | 19 | my %certs; 20 | my $cert = ""; 21 | my $hash = ""; 22 | for my $line ( split( '\n', $rawcerts ) ) { 23 | if( $line =~ m/^SHA-1 hash: ([0-9A-F]+)$/ ) { 24 | my $linehash = $1; 25 | if( $hash ) { 26 | $certs{ $hash } = $cert; 27 | } 28 | $cert = ""; 29 | $hash = $linehash; 30 | next; 31 | } 32 | $cert .= "$line\n"; 33 | } 34 | 35 | my $signers = `security find-identity -v -p codesigning`; 36 | my $type = "Mac Developer"; 37 | for my $line ( split( '\n', $signers ) ) { 38 | if( $line =~ m/[0-9]+\) ([A-Z0-9]+) "$type: (.+)"$/ ) { 39 | my $linehash = $1; 40 | my $linename = $2; 41 | my $cert = $certs{ $linehash }; 42 | decode_cert( $cert ); 43 | } 44 | } 45 | 46 | if( $action eq 'sign' ) { 47 | if( !$found ) { 48 | print STDERR "Could not find Mac Developer cert for developer OU $goalou\n"; 49 | exit 1; 50 | } 51 | `codesign -fs "$found" "$tosign"`; 52 | print `codesign -d -r- "$tosign"`; 53 | } 54 | exit 0; 55 | 56 | sub decode_cert { 57 | my $cert = shift; 58 | 59 | my ( $fh, $fname ) = tempfile( UNLINK => 1 ); 60 | print $fh $cert; 61 | close( $fh ); 62 | 63 | my $text = `openssl x509 -text -in $fname -noout`; 64 | for my $line ( split( '\n', $text ) ) { 65 | if( $line =~ m/Subject:.+CN=$type: (.+), OU=([A-Z0-9]+),/ ) { 66 | my $name = $1; 67 | my $ou = $2; 68 | if( !$action ) { 69 | print "Name: $name, OU: $ou\n"; 70 | } 71 | else { 72 | if( $goalou eq $ou ) { 73 | $found = "$type: $name"; 74 | } 75 | } 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /util/tcc.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | use strict; 3 | use Data::Dumper; 4 | use IPC::Open2; 5 | use File::Temp qw/tempfile/; 6 | 7 | my $user = getlogin(); 8 | my $db = "/Users/$user/Library/Application Support/com.apple.TCC/TCC.db"; 9 | my @cols = qw/ 10 | service 11 | client 12 | client_type 13 | allowed 14 | prompt_count 15 | csreq 16 | policy_id 17 | indirect_object_identifier 18 | indirect_object_code_identity 19 | flags 20 | last_modified 21 | /; 22 | 23 | my %usehex = ( 24 | csreq => 1, 25 | indirect_object_code_identity => 1 26 | ); 27 | 28 | my $action = $ARGV[0] || 'usage'; 29 | if ( $action eq 'getcamera' ) { print_all_camera_access(); } 30 | elsif( $action eq 'getcontrol' ) { print_all_control(); } 31 | elsif( $action eq 'getall' ) { print_all_access(); } 32 | elsif( $action eq 'addcamera' ) { add_camera_access( $ARGV[1] || '/Applications/STF Coordinator.app' ); } 33 | elsif( $action eq 'delcamera' ) { del_camera_access( $ARGV[1] || '/Applications/STF Coordinator.app' ); } 34 | elsif( $action eq 'hascamera' ) { has_camera_access( $ARGV[1] || '/Applications/STF Coordinator.app' ); } 35 | elsif( $action eq 'addcontrol' ) { add_control( $ARGV[1], $ARGV[2] ); } 36 | elsif( $action eq 'delcontrol' ) { del_control( $ARGV[1], $ARGV[2] ); } 37 | elsif( $action eq 'usage' ) { 38 | my $w = "\033[97m"; 39 | my $o = "\033[0m"; 40 | print < [parameter(s) of action] 43 | 44 | Actions: 45 | ${w}getcamera$o 46 | Show all of the apps with access to the camera 47 | 48 | ${w}getcontrol$o 49 | Show which apps can control which other apps 50 | 51 | ${w}getall$o 52 | Show all entries in the access DB 53 | 54 | ${w}addcamera$o 55 | Give camera permission to the specified app 56 | Example: addcamera /Applications/Utilities/Terminal.app 57 | 58 | ${w}addcontrol$o 59 | Give app1 permission to control app2 using OSA / Applescript 60 | 61 | ${w}delcamera$o 62 | Remove camera permissions for app 63 | 64 | ${w}delcontrol$o 65 | Remove permissions of app1 to control app2 66 | DONE 67 | } 68 | 69 | sub get_access { 70 | my $where = shift; 71 | 72 | my $selectCols = ""; 73 | for my $col ( @cols ) { 74 | if( $usehex{ $col } ) { 75 | $selectCols .= "hex($col) as $col,"; 76 | } 77 | else { 78 | $selectCols .= "quote($col) as $col,"; 79 | } 80 | } 81 | chop $selectCols; 82 | 83 | my $wheretext = ''; 84 | 85 | if( $where ) { 86 | my @conds; 87 | for my $key ( keys %$where ) { 88 | my $val = $where->{ $key }; 89 | push( @conds, "$key=$val" ); 90 | } 91 | $wheretext = "where " . join( ',', @conds ); 92 | } 93 | 94 | my $lines = `sqlite3 "$db" -line "select $selectCols from access $wheretext;"`; 95 | $lines .= "\n \n"; 96 | my $row = {}; 97 | my @rows; 98 | for my $line ( split( '\n', $lines ) ) { 99 | if( $line eq "" ) { 100 | push( @rows, $row ); 101 | $row = {}; 102 | next; 103 | } 104 | if( $line =~ m/([a-z()_]+) = (.+)$/ ) { 105 | my $name = $1; 106 | my $val = $2; 107 | if( $usehex{ $name } ) { 108 | my $raw = pack("H*", $val); 109 | $row->{ $name } = pipe_in_out( $raw, "csreq -r- -t" ); 110 | } 111 | else { 112 | $row->{ $name } = $val; 113 | } 114 | } 115 | } 116 | return \@rows; 117 | } 118 | 119 | sub has_camera_access { 120 | my $app = shift; 121 | my $rows = get_access( { service => "'kTCCServiceCamera'" } ); 122 | my $idents = get_app_idents(); 123 | for my $row ( @$rows ) { 124 | if( $row->{allowed} ) { 125 | my $clientNoQuote = substr( $row->{client}, 1, -1 ); 126 | if( $idents->{ $clientNoQuote } ) { 127 | my $full = $idents->{ $clientNoQuote }; 128 | if( $full eq $app ) { 129 | print "yes\n"; 130 | exit( 0 ); 131 | } 132 | } 133 | } 134 | } 135 | print "no\n"; 136 | exit( 1 ); 137 | } 138 | 139 | sub print_all_camera_access { 140 | my $rows = get_access( { service => "'kTCCServiceCamera'" } ); 141 | #print Dumper( $rows ); 142 | my $idents = get_app_idents(); 143 | for my $row ( @$rows ) { 144 | print_camera_access( $idents, $row ); 145 | print "\n"; 146 | } 147 | } 148 | 149 | sub print_all_control { 150 | my $rows = get_access( { service => "'kTCCServiceAppleEvents\'" } ); 151 | print Dumper( $rows ); 152 | my $idents = get_app_idents(); 153 | 154 | for my $row ( @$rows ) { 155 | print_control( $idents, $row ); 156 | print "\n"; 157 | } 158 | } 159 | 160 | sub print_control { 161 | my ( $idents, $row ) = @_; 162 | 163 | my $allowed = $row->{allowed}; 164 | my $csreq = $row->{csreq}; 165 | my $client = $row->{client}; 166 | my $indir = $row->{indirect_object_identifier}; 167 | my $indir_ident = $row->{indirect_object_code_identity}; 168 | 169 | if( $allowed ) { 170 | print "Controlling App = $client\nControlling App Identity = $csreq\n"; 171 | my $clientNoQuote = substr( $client, 1, -1 ); 172 | if( $idents->{ $clientNoQuote } ) { 173 | my $full = $idents->{ $clientNoQuote }; 174 | print " Possible Match = $full\n"; 175 | my $pIdent = app_to_ident( $full ); 176 | if( $pIdent ) { 177 | print " Possible Match Identity = $pIdent\n"; 178 | } 179 | } 180 | 181 | print "Controlled App = $indir\nControlled App Identity = $indir_ident\n"; 182 | my $indirNoQuote = substr( $indir, 1, -1 ); 183 | if( $idents->{ $indirNoQuote } ) { 184 | my $full = $idents->{ $indirNoQuote }; 185 | print " Possible Match = $full\n"; 186 | my $pIdent = app_to_ident( $full ); 187 | if( $pIdent ) { 188 | print " Possible Match Identity = $pIdent\n"; 189 | } 190 | } 191 | } 192 | } 193 | 194 | sub app_to_ident { 195 | my $app = shift; 196 | my @matchLines = `codesign -d -r- "$app" 2>/dev/null`; 197 | for my $line ( @matchLines ) { 198 | if( $line =~ m/designated => (.+)/ ) { 199 | return $1; 200 | } 201 | } 202 | return ""; 203 | } 204 | 205 | sub print_all_access { 206 | my $rows = get_access( 0 ); 207 | print Dumper( $rows ); 208 | 209 | } 210 | 211 | sub print_camera_access { 212 | my ( $idents, $row ) = @_; 213 | my $allowed = $row->{allowed}; 214 | my $csreq = $row->{csreq}; 215 | my $client = $row->{client}; 216 | 217 | if( $allowed ) { 218 | print "CS Req = $csreq\nClient = $client\n"; 219 | my $clientNoQuote = substr( $client, 1, -1 ); 220 | if( $idents->{ $clientNoQuote } ) { 221 | my $full = $idents->{ $clientNoQuote }; 222 | print " Possible Match = $full\n"; 223 | my $pIdent = app_to_ident( $full ); 224 | if( $pIdent ) { 225 | print " Possible Match Identity = $pIdent\n"; 226 | } 227 | } 228 | } 229 | } 230 | 231 | sub pipe_in_out { 232 | my ( $in, $cmd ) = @_; 233 | 234 | my $out = ''; 235 | my $pid = open2( \*SUB_OUT, \*SUB_IN, $cmd ); 236 | 237 | print SUB_IN "$in\cD"; 238 | 239 | while( ) { 240 | $out .= $_; 241 | } 242 | chomp $out; 243 | 244 | waitpid( $pid, 0 ); 245 | 246 | return $out; 247 | } 248 | 249 | sub get_app_idents { 250 | my %idents; 251 | 252 | opendir( my $dh, "/Applications" ); 253 | my @files = readdir( $dh ); 254 | closedir( $dh ); 255 | for my $file ( @files ) { 256 | next if( $file =~ m/^\.+$/ ); 257 | next if( $file !~ m/\.app$/ ); 258 | my $full = "/Applications/$file"; 259 | my $ident = get_app_bundle( $full ); 260 | if( $ident ) { 261 | $idents{ $ident } = $full; 262 | } 263 | } 264 | return \%idents; 265 | } 266 | 267 | sub get_app_bundle { 268 | my $full = shift; 269 | my @lines = `plutil -extract CFBundleIdentifier xml1 "$full/Contents/Info.plist" -o -`; 270 | for my $line ( @lines ) { 271 | if( $line =~ m|(.+)| ) { 272 | return $1; 273 | } 274 | } 275 | return ""; 276 | } 277 | 278 | sub add_camera_access { 279 | my $app = shift; 280 | my $appIdent = app_to_ident( $app ); 281 | #print "Ident: $appIdent\n"; 282 | my $hex = ident_to_csreq( $appIdent ); 283 | #print "$hex\n"; 284 | 285 | my $bundle = get_app_bundle( $app ); 286 | #print "Bundle: $bundle\n"; 287 | 288 | `sqlite3 "$db" "delete from access where service='kTCCServiceCamera' and client='$bundle'"`; 289 | sql_insert( 'access', { 290 | service => "'kTCCServiceCamera'", 291 | client => "'$bundle'", 292 | client_type => 0, 293 | allowed => 1, 294 | prompt_count => 1, 295 | csreq => "x'$hex'", 296 | policy_id => "'NULL'", 297 | indirect_object_identifier => "'UNUSED'", 298 | flags => 0 299 | } ); 300 | } 301 | 302 | sub del_camera_access { 303 | my $app = shift; 304 | my $bundle = get_app_bundle( $app ); 305 | `sqlite3 "$db" "delete from access where service='kTCCServiceCamera' and client='$bundle'"`; 306 | } 307 | 308 | sub ident_to_csreq { 309 | my $ident = shift; 310 | 311 | open( my $fh, "| csreq -r- -b ./test" ); 312 | print $fh $ident; 313 | close( $fh ); 314 | open( my $bh, "<./test" ); 315 | binmode( $bh ); 316 | my $data; 317 | { 318 | local $/ = undef; 319 | $data = <$bh>; 320 | } 321 | close( $bh ); 322 | my $hex = uc( unpack("H*", $data) ); 323 | 324 | return $hex; 325 | } 326 | 327 | sub add_control { 328 | my ( $app, $app2 ) = @_; 329 | my $appIdent = app_to_ident( $app ); 330 | my $app2Ident = app_to_ident( $app2 ); 331 | #print "Ident: $appIdent\n"; 332 | my $hex = ident_to_csreq( $appIdent ); 333 | my $hex2 = ident_to_csreq( $app2Ident ); 334 | #print "$hex\n"; 335 | 336 | my $bundle = get_app_bundle( $app ); 337 | my $bundle2 = get_app_bundle( $app2 ); 338 | #print "Bundle: $bundle\n"; 339 | 340 | `sqlite3 "$db" "delete from access where service='kTCCServiceAppleEvents' and client='$bundle' and indirect_object_identifier='$bundle2'"`; 341 | sql_insert( 'access', { 342 | service => "'kTCCServiceAppleEvents'", 343 | client => "'$bundle'", 344 | client_type => 0, 345 | allowed => 1, 346 | prompt_count => 1, 347 | csreq => "x'$hex'", 348 | policy_id => "'NULL'", 349 | indirect_object_identifier => "'$bundle2'", 350 | indirect_object_code_identity => "x'$hex2'", 351 | flags => "NULL" 352 | } ); 353 | } 354 | 355 | sub del_control { 356 | my ( $app, $app2 ) = @_; 357 | my $bundle = get_app_bundle( $app ); 358 | my $bundle2 = get_app_bundle( $app2 ); 359 | 360 | `sqlite3 "$db" "delete from access where service='kTCCServiceAppleEvents' and client='$bundle' and indirect_object_identifier='$bundle2'"`; 361 | } 362 | 363 | sub sql_insert { 364 | my ( $table, $vals ) = @_; 365 | 366 | my @keys = sort keys %$vals; 367 | 368 | my @valset; 369 | for my $key ( @keys ) { 370 | my $val = $vals->{ $key }; 371 | push( @valset, $val ); 372 | } 373 | my $keytext = join(',', @keys ); 374 | my $valtext = join(',', @valset ); 375 | `sqlite3 "$db" "insert into $table ($keytext) values($valtext)"`; 376 | } -------------------------------------------------------------------------------- /version.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0" 3 | } 4 | -------------------------------------------------------------------------------- /video_pipes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dryark/stf_ios_support/2f32cbbf1506a740eb21a496c1cdc11d875558f7/video_pipes/.gitkeep -------------------------------------------------------------------------------- /view_log.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "flag" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "encoding/json" 11 | "os" 12 | "strings" 13 | "github.com/fsnotify/fsnotify" 14 | log "github.com/sirupsen/logrus" 15 | ) 16 | 17 | type Config struct { 18 | Log LogConfig 19 | //LogFile string `json:"log_file"` 20 | //LinesLogFile string `json:"lines_log_file"` 21 | } 22 | 23 | type LogConfig struct { 24 | Main string `json:"main"` 25 | ProcLines string `json:"proc_lines"` 26 | WDAWrapperStdout string `json:"wda_wrapper_stdout"` 27 | WDAWrapperStderr string `json:"wda_wrapper_stderr"` 28 | } 29 | 30 | func main() { 31 | var configFile = flag.String( "config", "config.json", "Config file path" ) 32 | var findProc = flag.String( "proc" , "" , "Process to view log of" ) 33 | flag.Parse() 34 | 35 | config := read_config( *configFile ) 36 | 37 | fileName := config.Log.ProcLines 38 | 39 | if *findProc == "" { 40 | fmt.Println("specify a log to view / tail ( view_log -proc [proc] ):\n wdaproxy\n stf_device_ios\n device_trigger\n video_enabler\n stf_provider\n ffmpeg\n wda\n device_trigger\n") 41 | os.Exit( 0 ) 42 | } 43 | 44 | if *findProc == "wda" { 45 | fileName = "bin/wda/req_log.json" 46 | } 47 | 48 | watcher, err := fsnotify.NewWatcher() 49 | if err != nil { 50 | log.Fatal(err) 51 | } 52 | 53 | fh, err := os.Open( fileName ) 54 | if err != nil { 55 | panic(err) 56 | } 57 | defer fh.Close() 58 | 59 | size := fileSize( fh ) 60 | //fh.Seek( size, io.SeekStart ) 61 | 62 | scanner := bufio.NewScanner( fh ) 63 | for scanner.Scan() { 64 | checkLine( []byte( scanner.Text() ), *findProc ) 65 | } 66 | 67 | err = watcher.Add( fileName ) 68 | if err != nil { 69 | log.Fatal(err) 70 | } 71 | for { 72 | select { 73 | case event := <-watcher.Events: 74 | if event.Op & fsnotify.Write == fsnotify.Write { 75 | //fmt.Println("modify") 76 | newSize := fileSize( fh ) 77 | 78 | newBytes := newSize - size 79 | 80 | if newBytes > 0 { 81 | //fmt.Printf(" dif: %d\n", newBytes ) 82 | 83 | //f.Seek(pos, io.SeekStart) 84 | buf := make( []byte, newBytes ) 85 | fh.Read( buf ) 86 | //fmt.Printf(" \"%s\"\n", string( buf ) ) 87 | 88 | checkLine( buf, *findProc ) 89 | 90 | size = newSize 91 | } 92 | } 93 | } 94 | } 95 | } 96 | 97 | func read_config( configPath string ) *Config { 98 | fh, serr := os.Stat( configPath ) 99 | if serr != nil { 100 | log.WithFields( log.Fields{ 101 | "type": "err_read_config", 102 | "error": serr, 103 | "config_path": configPath, 104 | } ).Fatal("Could not read specified config path") 105 | } 106 | var configFile string 107 | switch mode := fh.Mode(); { 108 | case mode.IsDir(): 109 | configFile = fmt.Sprintf("%s/config.json", configPath) 110 | case mode.IsRegular(): 111 | configFile = configPath 112 | } 113 | 114 | configFh, err := os.Open( configFile ) 115 | if err != nil { 116 | log.WithFields( log.Fields{ 117 | "type": "err_read_config", 118 | "config_file": configFile, 119 | "error": err, 120 | } ).Fatal("failed reading config file") 121 | } 122 | defer configFh.Close() 123 | 124 | jsonBytes, _ := ioutil.ReadAll( configFh ) 125 | config := Config{} 126 | 127 | defaultJson := `{ 128 | "log":{ 129 | "main": "logs/coordinator", 130 | "proc_lines": "logs/procs", 131 | "wda_wrapper_stdout": "./logs/wda_wrapper_stdout", 132 | "wda_wrapper_stderr": "./logs/wda_wrapper_stderr" 133 | } 134 | }` 135 | 136 | err = json.Unmarshal( []byte( defaultJson ), &config ) 137 | if err != nil { 138 | log.Fatal( "1 ", err ) 139 | } 140 | 141 | json.Unmarshal( jsonBytes, &config ) 142 | return &config 143 | } 144 | 145 | func checkLine( data []byte, findProc string ) { 146 | var dat map[string]interface{} 147 | 148 | startJ := strings.Index( string(data), "{" ) 149 | endJ := strings.LastIndex( string(data), "}" ) 150 | 151 | part := string(data)[ startJ : (endJ + 1) ] 152 | 153 | decoder := json.NewDecoder( strings.NewReader( part ) ) 154 | for { 155 | err := decoder.Decode( &dat ) 156 | if err == io.EOF { 157 | break 158 | } 159 | if err != nil { 160 | panic(err) 161 | } 162 | 163 | if findProc == "wda" { 164 | //fmt.Println( part ) 165 | typeif := dat["type"] 166 | if typeif != nil { 167 | typ := typeif.(string) 168 | //fmt.Println( typ ) 169 | if typ == "req.start" { 170 | if dat["body_in"] != nil { 171 | inStr := dat["body_in"].(string) 172 | 173 | fmt.Printf("Req URI: %s\n", dat["uri"].(string) ) 174 | if inStr[:1] == "{" { 175 | var prettyJSON bytes.Buffer 176 | error := json.Indent(&prettyJSON, []byte( inStr ), "", " ") 177 | if error != nil { 178 | fmt.Println( inStr ) 179 | } else { 180 | fmt.Println( prettyJSON.String() ) 181 | } 182 | 183 | //dec2 :=- json.NewDecoder( strings.NewReader( dat["body_in"].(string) ) ) 184 | //err = dec2.Decode( &dat ) 185 | } else { 186 | fmt.Println( inStr ) 187 | } 188 | } 189 | } else if typ == "req.done" { 190 | if dat["body_out"] != nil { 191 | outStr := dat["body_out"].(string) 192 | fmt.Printf("Response to URI: %s\n", dat["uri"].(string) ) 193 | 194 | fmt.Println( outStr ) 195 | } 196 | } 197 | } 198 | } else { 199 | proc := dat["proc"].(string) 200 | if proc == findProc { 201 | //fmt.Println(dat) 202 | line := dat["line"].(string) 203 | fmt.Println( line ) 204 | } 205 | } 206 | } 207 | } 208 | 209 | func fileSize( fh *os.File ) (int64) { 210 | newinfo, err := fh.Stat() 211 | if err != nil { 212 | panic(err) 213 | } 214 | return newinfo.Size() 215 | } -------------------------------------------------------------------------------- /wda_wrapper/Makefile: -------------------------------------------------------------------------------- 1 | TARGET = ../bin/wda_wrapper 2 | 3 | all: $(TARGET) 4 | 5 | $(TARGET): wda_wrapper.go go.sum 6 | go build -o $(TARGET) . 7 | 8 | go.sum: 9 | go get 10 | go get . 11 | 12 | clean: 13 | $(RM) $(TARGET) go.sum -------------------------------------------------------------------------------- /wda_wrapper/go.mod: -------------------------------------------------------------------------------- 1 | module wda_wrapper 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/go-cmd/cmd v1.2.0 7 | github.com/jviney/go-proc v0.2.0 8 | github.com/sirupsen/logrus v1.4.2 9 | github.com/zeromq/goczmq v4.1.0+incompatible 10 | ) 11 | -------------------------------------------------------------------------------- /wda_wrapper/wda_wrapper.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "encoding/json" 7 | "os" 8 | "os/exec" 9 | "os/signal" 10 | "strconv" 11 | "strings" 12 | "syscall" 13 | "time" 14 | zmq "github.com/zeromq/goczmq" 15 | log "github.com/sirupsen/logrus" 16 | gocmd "github.com/go-cmd/cmd" 17 | ) 18 | 19 | var exit bool 20 | var reqSock *zmq.Sock 21 | var reqOb *zmq.ReadWriter 22 | 23 | func main() { 24 | exit = false 25 | 26 | var wdaPort = flag.Int( "port", 8100, "WDA Port" ) 27 | var uuid = flag.String( "uuid", "", "IOS Device UUID" ) 28 | var iosVersion = flag.String( "iosVersion", "", "IOS Version" ) 29 | var wdaRoot = flag.String( "wdaRoot", "", "WDA Folder Path" ) 30 | flag.Parse() 31 | 32 | coro_sigterm() 33 | setup_zmq() 34 | proc_wdaproxy( *wdaPort, *uuid, *iosVersion, *wdaRoot ) 35 | } 36 | 37 | 38 | func proc_wdaproxy( 39 | wdaPort int, 40 | uuid string, 41 | iosVersion string, 42 | wdaRoot string ) { 43 | 44 | log.WithFields( log.Fields{ 45 | "type": "proc_start", 46 | "proc": "wda_wrapper", 47 | "wdaPort": wdaPort, 48 | "uuid": uuid, 49 | "iosVersion": iosVersion, 50 | "wdaRoot": wdaRoot, 51 | } ).Info("wda wrapper started") 52 | 53 | backoff := Backoff{} 54 | 55 | stdoutChan := make(chan string, 100) 56 | stderrChan := make(chan string, 100) 57 | 58 | go func() { 59 | for { 60 | line := <- stdoutChan 61 | 62 | if strings.Contains( line, "is implemented in both" ) { 63 | } else if strings.Contains( line, "Couldn't write value" ) { 64 | } else if strings.Contains( line, "GET /status " ) { 65 | } else if strings.Contains( line, "] Error" ) { 66 | msgCoord( map[string]string{ 67 | "type": "wda_error", 68 | "line": line, 69 | "uuid": uuid, 70 | } ) 71 | } else { 72 | log.WithFields( log.Fields{ 73 | "type": "proc_stdout", 74 | "line": line, 75 | } ).Info("") 76 | msgCoord( map[string]string{ 77 | "type": "wda_stdout", 78 | "line": line, 79 | "uuid": uuid, 80 | } ) 81 | // send line to linelog ( through zmq ) 82 | } 83 | 84 | if exit { break } 85 | } 86 | } () 87 | 88 | go func() { 89 | for { 90 | line := <- stderrChan 91 | 92 | if strings.Contains( line, "[WDA] successfully started" ) { 93 | // send message that WDA has started to coordinator 94 | msgCoord( map[string]string{ 95 | "type": "wda_started", 96 | "uuid": uuid, 97 | } ) 98 | } 99 | 100 | // send line to coordinator 101 | log.WithFields( log.Fields{ 102 | "type": "proc_stderr", 103 | "line": line, 104 | } ).Error("") 105 | msgCoord( map[string]string{ 106 | "type": "wda_stderr", 107 | "line": line, 108 | "uuid": uuid, 109 | } ) 110 | 111 | if exit { break } 112 | } 113 | } () 114 | 115 | for { 116 | ops := []string{ 117 | "-p", strconv.Itoa( wdaPort ), 118 | "-q", strconv.Itoa( wdaPort ), 119 | "-d", 120 | "-W", ".", 121 | "-u", uuid, 122 | fmt.Sprintf("--iosversion=%s", iosVersion), 123 | } 124 | 125 | log.WithFields( log.Fields{ 126 | "type": "proc_cmdline", 127 | "cmd": "../wdaproxy", 128 | "args": ops, 129 | } ).Info("") 130 | 131 | cmd := exec.Command( "../wdaproxy", ops... ) 132 | 133 | cmd.Dir = wdaRoot 134 | 135 | stdStream := gocmd.NewOutputStream(stdoutChan) 136 | cmd.Stdout = stdStream 137 | 138 | errStream := gocmd.NewOutputStream(stderrChan) 139 | cmd.Stderr = errStream 140 | 141 | backoff.markStart() 142 | err := cmd.Start() 143 | if err != nil { 144 | log.WithFields( log.Fields{ 145 | "type": "proc_err", 146 | "error": err, 147 | } ).Error("Error starting wdaproxy") 148 | backoff.markEnd() 149 | backoff.wait() 150 | continue 151 | } 152 | 153 | // send message that it has started 154 | msgCoord( map[string]string{ 155 | "type": "wdaproxy_started", 156 | "uuid": uuid, 157 | } ) 158 | 159 | cmd.Wait() 160 | 161 | backoff.markEnd() 162 | 163 | // send message that it has ended 164 | log.WithFields( log.Fields{ 165 | "type": "proc_end", 166 | } ).Info("Wdaproxy ended") 167 | msgCoord( map[string]string{ 168 | "type": "wdaproxy_ended", 169 | "uuid": uuid, 170 | } ) 171 | 172 | if exit { break } 173 | 174 | backoff.wait() 175 | } 176 | 177 | close_zmq() 178 | } 179 | 180 | type Backoff struct { 181 | fails int 182 | start time.Time 183 | elapsedSeconds float64 184 | } 185 | 186 | func ( self *Backoff ) markStart() { 187 | self.start = time.Now() 188 | } 189 | 190 | func ( self *Backoff ) markEnd() ( float64 ) { 191 | elapsed := time.Since( self.start ) 192 | seconds := elapsed.Seconds() 193 | self.elapsedSeconds = seconds 194 | return seconds 195 | } 196 | 197 | func ( self *Backoff ) wait() { 198 | sleeps := []int{ 0, 0, 2, 5, 10 } 199 | numSleeps := len( sleeps ) 200 | if self.elapsedSeconds < 20 { 201 | self.fails = self.fails + 1 202 | index := self.fails 203 | if index >= numSleeps { 204 | index = numSleeps - 1 205 | } 206 | sleepLen := sleeps[ index ] 207 | if sleepLen != 0 { 208 | time.Sleep( time.Second * time.Duration( sleepLen ) ) 209 | } 210 | } else { 211 | self.fails = 0 212 | } 213 | } 214 | 215 | func setup_zmq() { 216 | reqSock = zmq.NewSock(zmq.Push) 217 | 218 | spec := "tcp://127.0.0.1:7300" 219 | reqSock.Connect( spec ) 220 | 221 | var err error 222 | reqOb, err = zmq.NewReadWriter(reqSock) 223 | if err != nil { 224 | log.WithFields( log.Fields{ 225 | "type": "zmq_connect_err", 226 | "err": err, 227 | } ).Error("ZMQ Send Error") 228 | } 229 | 230 | reqOb.SetTimeout(1000) 231 | 232 | zmqRequest( []byte("dummy") ) 233 | } 234 | 235 | func close_zmq() { 236 | reqSock.Destroy() 237 | reqOb.Destroy() 238 | } 239 | 240 | func msgCoord( content map[string]string ) { 241 | data, _ := json.Marshal( content ) 242 | zmqRequest( data ) 243 | } 244 | 245 | func zmqRequest( jsonOut []byte ) { 246 | err := reqSock.SendMessage( [][]byte{ jsonOut } ) 247 | if err != nil { 248 | log.WithFields( log.Fields{ 249 | "type": "zmq_send_err", 250 | "err": err, 251 | } ).Error("ZMQ Send Error") 252 | } 253 | } 254 | 255 | func coro_sigterm() { 256 | c := make(chan os.Signal, 2) 257 | signal.Notify(c, os.Interrupt, syscall.SIGTERM) 258 | go func() { 259 | <- c 260 | exit = true 261 | 262 | time.Sleep( time.Millisecond * 1000 ) 263 | 264 | os.Exit(0) 265 | }() 266 | } --------------------------------------------------------------------------------