├── .gitignore ├── .rvmrc ├── README.asciidoc ├── app.rb └── config.example.yml /.gitignore: -------------------------------------------------------------------------------- 1 | config.yml 2 | chromdriver.log 3 | -------------------------------------------------------------------------------- /.rvmrc: -------------------------------------------------------------------------------- 1 | rvm 1.9.2 2 | -------------------------------------------------------------------------------- /README.asciidoc: -------------------------------------------------------------------------------- 1 | Automatically Generate iOS Push Notification Certificates 2 | ========================================================= 3 | 4 | About 5 | ----- 6 | 7 | This hack of a Ruby script logs into your iOS provisioning portal and generates Apple iOS production push notification 8 | certificates. This was needed because I have over 200+ apps in the iOS store. I needed a quick way to generate these 9 | certificates. So, I created a Ruby Gem to interface with the Mac OS X Keychain app. 10 | 11 | This was tested on *Mac OS X 10.7 (Lion)* using *Ruby 1.9.2*. 12 | 13 | You must have Chrome installed and the chromedriver binary http://code.google.com/p/chromium/downloads/list. 14 | 15 | 16 | 17 | Usage 18 | ----- 19 | 20 | First, install the gems. 21 | ---- 22 | gem install keychain_manager 23 | gem install watir-webdriver 24 | ---- 25 | 26 | Next, modify 'config.example.yml' with your appropriate parameters and rename the file to 'config.yml'. 27 | 28 | Now, you'll need to open the script up 'app.rb' and modify the variable 'END_WITH', if you want to configure 29 | all of your iOS apps that aren't already configured for production push notifications, set 'END_WITH' to an 30 | empty string ''. Utlimatetly, right around line 60 with END_WITH is being used should be modified to evaluate 31 | a regular expression. 32 | 33 | Finally, run the script: 34 | ---- 35 | ruby app.rb 36 | ---- 37 | 38 | 39 | 40 | License 41 | ------- 42 | 43 | (The MIT License) 44 | 45 | Copyright (c) 2011 JP Richardson 46 | 47 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files 48 | (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, 49 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 50 | furnished to do so, subject to the following conditions: 51 | 52 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 53 | 54 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 55 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 56 | OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 57 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 58 | 59 | 60 | -------------------------------------------------------------------------------- /app.rb: -------------------------------------------------------------------------------- 1 | # app.rb 2 | # 3 | # Usage: ruby app.rb 4 | # 5 | # Configuring: 6 | # config.yml 7 | 8 | require 'watir-webdriver' 9 | require 'fileutils' 10 | require 'keychain_manager' 11 | require 'yaml' 12 | 13 | config = YAML::load(File.open(Dir.pwd + '/config.yml')) 14 | 15 | USER = config['user'] 16 | PASSWORD = config['password'] 17 | KEYCHAIN = config['keychain'] 18 | DOWNLOAD_DIR = config['download_dir'] 19 | CERT_DIR = config['cert_dir'] 20 | TEAM = config['team'] 21 | REFRESH_CERTS = config['refresh_certs'] 22 | 23 | APP_IDS_URL = "https://developer.apple.com/ios/manage/bundles/index.action" 24 | RSA_FILE = '/tmp/push_notification.key' 25 | CERT_REQUEST_FILE = '/tmp/CertificateSigningRequest.certSigningRequest' 26 | DOWNLOADED_CERT_FILE = "#{DOWNLOAD_DIR}aps_production_identity.cer" 27 | P12_FILE = '/tmp/out.p12' 28 | PEM_FILE = '/tmp/out.pem' 29 | 30 | END_WITH = 'FanFB' #You may want to modify this and this line: if aaid.end_with?(END_WITH) (currently right around line 60) 31 | 32 | WAIT_TO = 180 #3 mins 33 | 34 | # Internal constants 35 | 36 | APP_TABLE_CONFIG_COL = 5 37 | 38 | def main 39 | puts("Let's get this party started.") 40 | 41 | Dir.mkdir(CERT_DIR) unless Dir.exists?(CERT_DIR) 42 | 43 | KeychainManager.generate_rsa_key(RSA_FILE) 44 | KeychainManager.generate_cert_request(USER, 'US', RSA_FILE, CERT_REQUEST_FILE) 45 | 46 | browser = Watir::Browser.new(:chrome) 47 | browser.goto(APP_IDS_URL) 48 | 49 | table = nil 50 | if browser.body.text.include?('Sign in') 51 | puts("Not logged in... logging in...") 52 | browser.text_field(name: 'theAccountName').set(USER) 53 | browser.text_field(id: 'accountpassword').set(PASSWORD) 54 | form = browser.form(name:'appleConnectForm') 55 | form.submit() 56 | puts("Logged in!") 57 | end 58 | 59 | if browser.body.text.include?('Select Your Team') 60 | puts("Now let's select your team...") 61 | browser.select_list(id: 'teams').select(TEAM) 62 | browser.button(id: 'saveTeamSelection_saveTeamSelection_save').click() 63 | puts("Team is selected!") 64 | end 65 | 66 | table = browser.div(:class => 'nt_multi').table #table of Apple App Ids, now called AAID 67 | 68 | count = table.rows.count 69 | 0.upto(count - 1) do |i| 70 | tds = table[i] 71 | if tds[0].strong.exists? 72 | name = tds[0].strong 73 | aaid = '' 74 | if name.text.strip.end_with?('...') #can't see all of the name, must mouse over 75 | aaid = name.attribute_value(:title).strip 76 | else 77 | aaid = name.text.strip 78 | end 79 | 80 | if aaid.end_with?(END_WITH) 81 | if tds[1].text.include?('Enabled for Production') 82 | if REFRESH_CERTS 83 | puts "Rebuilding push ssl cert for #{aaid}..." 84 | tds[APP_TABLE_CONFIG_COL].a.click #'Configure' link 85 | renew_for_prod(browser, aaid) #new configure page 86 | else 87 | puts "#{aaid} already enabled. Skipping..." 88 | end 89 | elsif tds[1].text.include?('Configurable for Production') #too be safe, generate new Keychain everytime 90 | puts "Configuring certificate for #{aaid}..." 91 | tds[APP_TABLE_CONFIG_COL].a.click #'Configure' link 92 | configure_for_prod(browser, aaid) #new configure page 93 | end 94 | end 95 | end 96 | end 97 | 98 | puts('Done.') 99 | #STDIN.gets.strip 100 | 101 | end 102 | 103 | def pem_file(app) 104 | File.join(CERT_DIR, app + '.pem') 105 | end 106 | 107 | def with_keychain(app, &block) 108 | kcm = load_keychain_for_app(app) 109 | yield kcm 110 | kcm.delete 111 | end 112 | 113 | def configure_and_gen_pem(browser, app, &block) 114 | with_keychain(app) do |kcm| 115 | configure_cert(browser, app); #puts "time for some browser fun..." 116 | import_push_ssl_cert(kcm) 117 | @pem_file = pem_file(app) 118 | KeychainManager.convert_p12_to_pem(P12_FILE, @pem_file); puts "exporting #{@pem_file}" 119 | end 120 | end 121 | 122 | def configure_for_prod(browser, app) 123 | browser.checkbox(id: 'enablePush').click() #enable configure buttons 124 | browser.button(id: 'aps-assistant-btn-prod-en').click() #configure button 125 | configure_and_gen_pem(browser, app) 126 | end 127 | 128 | # renew_for_prod 129 | # 130 | # Renews existing ssl prod cert 131 | 132 | def renew_for_prod(browser, app) 133 | # 'Generate a new Production Push SSL Certificate before your current one expires' button 134 | browser.button(id: 'aps-assistant-btn-ov-prod-en').click 135 | configure_and_gen_pem(browser, app) 136 | end 137 | 138 | # configure_cert 139 | # 140 | # Enables push 141 | 142 | def configure_cert(browser, app) 143 | Watir::Wait.until { browser.body.text.include?('Generate a Certificate Signing Request') } 144 | 145 | browser.button(id: 'ext-gen59').click() #on lightbox overlay, click continue 146 | 147 | Watir::Wait.until { browser.body.text.include?('Submit Certificate Signing Request') } 148 | 149 | browser.file_field(name: 'upload').set(CERT_REQUEST_FILE) 150 | browser.execute_script("callFileValidate();") 151 | #browser.file_field(name: 'upload').click() #calls some local javascript to validate the file and enable continue button, unfortunately File Browse dialog shows up 152 | 153 | browser.button(id: 'ext-gen75').click() 154 | 155 | Watir::Wait.until(WAIT_TO) { browser.body.text.include?('Your APNs SSL Certificate has been generated.') } 156 | 157 | browser.button(id: 'ext-gen59').click() #continue 158 | 159 | Watir::Wait.until { browser.body.text.include?('Step 1: Download') } 160 | 161 | File.delete(DOWNLOADED_CERT_FILE) if File.exists?(DOWNLOADED_CERT_FILE) 162 | 163 | browser.button(alt: 'Download').click() #download cert 164 | 165 | puts('Checking for existence of downloaded certificate file...') 166 | while !File.exists?(DOWNLOADED_CERT_FILE) 167 | sleep 1 168 | end 169 | 170 | Watir::Wait.until { browser.body.text.include?("Download & Install Your Apple Push Notification service SSL Certificate") } 171 | 172 | browser.button(id: 'ext-gen91').click() 173 | browser.goto(APP_IDS_URL) 174 | end 175 | 176 | # download_cert 177 | # 178 | # downloads cert 179 | 180 | def download_cert(browser, app) 181 | File.delete(DOWNLOADED_CERT_FILE) if File.exists?(DOWNLOADED_CERT_FILE) 182 | 183 | #browser.button(src: /button_download/).click() #download cert 184 | browser.link(:id, "form_logginMemberCert_").click 185 | 186 | puts('Checking for existence of downloaded certificate file...') 187 | while !File.exists?(DOWNLOADED_CERT_FILE) 188 | sleep 1 189 | end 190 | FileUtils.mv DOWNLOADED_CERT_FILE, File.join(DOWNLOAD_DIR, "#{app}.cer") 191 | 192 | browser.goto(APP_IDS_URL) 193 | end 194 | 195 | def load_keychain_for_app(app) 196 | KeychainManager.new(KEYCHAIN).tap do |kcm| 197 | kcm.delete if kcm.exists? #start fresh 198 | kcm.create; #puts "creating new keychain for #{app}" 199 | kcm.import_rsa_key(RSA_FILE); #puts "importing RSA..." 200 | end 201 | end 202 | 203 | def import_push_ssl_cert(kcm) 204 | kcm.import_apple_cert(DOWNLOADED_CERT_FILE); puts "importing Apple cert" 205 | File.delete(DOWNLOADED_CERT_FILE) 206 | kcm.export_identities(P12_FILE) 207 | end 208 | 209 | main() 210 | -------------------------------------------------------------------------------- /config.example.yml: -------------------------------------------------------------------------------- 1 | --- 2 | user: someuser@somedomain.com 3 | password: secretpassword 4 | keychain: Push_Notifcations 5 | download_dir: /Users/jprichardson/Downloads/ 6 | cert_dir: /Users/jprichardson/Desktop/PushCerts/ 7 | team: yourteamname 8 | --------------------------------------------------------------------------------