pem 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/LICENSE +21 -0
- data/README.md +105 -0
- data/bin/pem +63 -0
- data/lib/pem.rb +13 -0
- data/lib/pem/cert_manager.rb +52 -0
- data/lib/pem/dependency_checker.rb +32 -0
- data/lib/pem/developer_center.rb +312 -0
- data/lib/pem/helper.rb +49 -0
- data/lib/pem/update_checker.rb +44 -0
- data/lib/pem/version.rb +3 -0
- metadata +47 -78
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 71fb127b623bfd0d13e75886377470357f7ee9e4
|
4
|
+
data.tar.gz: 6d647f38ba8ed62457e76705910c141cb48319af
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b2f9937ec278727792b969e1fe17b3851090bd8b2ab1ab7a71d8c63e87112011fee238d034d4d07366f9784513e434884ff463cd97fd60f45963b1b97f231f34
|
7
|
+
data.tar.gz: b62966cc13a6f027a479852952f50f5b2c447584d9b5180a387eea02d8f2f7c5b12092a8a419d55aa9e78b62b215265766f287beb0ca5954d8f29d836ab1a06b
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2014 Felix Krause
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
<p align="center">
|
2
|
+
<a href="https://github.com/KrauseFx/deliver">Deliver</a> •
|
3
|
+
<a href="https://github.com/KrauseFx/snapshot">Snapshot</a> •
|
4
|
+
<a href="https://github.com/KrauseFx/frameit">FrameIt</a> •
|
5
|
+
<b>PEM</b>
|
6
|
+
</p>
|
7
|
+
-------
|
8
|
+
|
9
|
+
<p align="center">
|
10
|
+
<img src="assets/pem.png">
|
11
|
+
</p>
|
12
|
+
|
13
|
+
Pem
|
14
|
+
============
|
15
|
+
|
16
|
+
[![Twitter: @KauseFx](https://img.shields.io/badge/contact-@KrauseFx-blue.svg?style=flat)](https://twitter.com/KrauseFx)
|
17
|
+
[![License](http://img.shields.io/badge/license-MIT-green.svg?style=flat)](https://github.com/KrauseFx/pem/blob/master/LICENSE)
|
18
|
+
[![Gem](https://img.shields.io/gem/v/pem.svg?style=flat)](http://rubygems.org/gems/pem)
|
19
|
+
|
20
|
+
|
21
|
+
Tired of manually creating and maintaining your push notification profiles? Tired of manually generating ```pem``` files for your server?
|
22
|
+
|
23
|
+
```PEM``` does all that for, just by running ```pem```!
|
24
|
+
|
25
|
+
-------
|
26
|
+
[Features](#features) •
|
27
|
+
[Installation](#installation) •
|
28
|
+
[Usage](#usage) •
|
29
|
+
[How does it work?](#how-does-it-work) •
|
30
|
+
[Tips](#tips) •
|
31
|
+
[Need help?](#need-help)
|
32
|
+
|
33
|
+
-------
|
34
|
+
|
35
|
+
# Features
|
36
|
+
Well, it's actually just one: Generate the ```pem``` file for your server.
|
37
|
+
|
38
|
+
|
39
|
+
Check out this gif:
|
40
|
+
|
41
|
+
![assets/PEMRecording.gif](assets/PEMRecording.gif)
|
42
|
+
|
43
|
+
# Installation
|
44
|
+
sudo gem install pem
|
45
|
+
|
46
|
+
Make sure, you have the latest version of the Xcode command line tools installed:
|
47
|
+
|
48
|
+
xcode-select --install
|
49
|
+
|
50
|
+
# Usage
|
51
|
+
|
52
|
+
pem
|
53
|
+
Yes, that's the whole command!
|
54
|
+
|
55
|
+
This does the following:
|
56
|
+
|
57
|
+
- Verifies the production push certificate looks alright
|
58
|
+
- Download the certificate
|
59
|
+
- Generates a new ```.pem``` file in the current working directory, which you can upload to your server
|
60
|
+
|
61
|
+
You can pass parameters like this:
|
62
|
+
|
63
|
+
pem -a at.felixkrause.app -u username
|
64
|
+
|
65
|
+
## Environment Variables
|
66
|
+
In case you prefer environment variables:
|
67
|
+
|
68
|
+
- ```PEM_USERNAME```
|
69
|
+
- ```PEM_APP_IDENTIFIER```
|
70
|
+
- ```PEM_CERT_SIGNING_REQUEST``` which is used, in case a new profile needs to be created
|
71
|
+
|
72
|
+
# How does it work?
|
73
|
+
There are 2 actions involved:
|
74
|
+
|
75
|
+
- Accessing the ```iOS Dev Center``` to download the latest ```aps_production.cer```. See: [developer_center.rb](https://github.com/KrauseFx/PEM/blob/master/lib/pem/developer_center.rb).
|
76
|
+
- Generating all the necessary profiles and files to prepare the finished ```.pem``` file. See: [cert_manager.rb](https://github.com/KrauseFx/PEM/blob/master/lib/pem/cert_manager.rb).
|
77
|
+
```PEM``` will create a temporary keychain called ```PEM.keychain``` and use that to generate the necessary profiles. That means ```PEM``` will not pollute your default keychain.
|
78
|
+
|
79
|
+
|
80
|
+
## How is my password stored?
|
81
|
+
```PEM``` uses the password manager from [```Deliver```](https://github.com/KrauseFx/deliver#can-i-trust-deliver). Take a look the [Deliver README](https://github.com/KrauseFx/deliver#can-i-trust-deliver) for more information.
|
82
|
+
|
83
|
+
# Tips
|
84
|
+
## Use the ```Provisioning Quicklook plugin```
|
85
|
+
Download and install the [Provisioning Plugin](https://github.com/chockenberry/Provisioning).
|
86
|
+
|
87
|
+
It will show you the ```pem``` files like this:
|
88
|
+
![assets/QuickLookScreenshot.png](assets/QuickLookScreenshot.png)
|
89
|
+
|
90
|
+
|
91
|
+
# Need help?
|
92
|
+
- If there is a technical problem with ```PEM```, submit an issue. Run ```pem --trace``` to get the stacktrace.
|
93
|
+
- I'm available for contract work - drop me an email: pem@felixkrause.at
|
94
|
+
|
95
|
+
# License
|
96
|
+
This project is licensed under the terms of the MIT license. See the LICENSE file.
|
97
|
+
|
98
|
+
# Contributing
|
99
|
+
|
100
|
+
1. Create an issue to discuss about your idea
|
101
|
+
2. Fork it (https://github.com/KrauseFx/pem/fork)
|
102
|
+
3. Create your feature branch (`git checkout -b my-new-feature`)
|
103
|
+
4. Commit your changes (`git commit -am 'Add some feature'`)
|
104
|
+
5. Push to the branch (`git push origin my-new-feature`)
|
105
|
+
6. Create a new Pull Request
|
data/bin/pem
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$:.push File.expand_path("../../lib", __FILE__)
|
4
|
+
|
5
|
+
require 'pem'
|
6
|
+
require 'commander/import'
|
7
|
+
require 'pem/update_checker'
|
8
|
+
require 'deliver/password_manager'
|
9
|
+
|
10
|
+
HighLine.track_eof = false
|
11
|
+
|
12
|
+
|
13
|
+
# Commander
|
14
|
+
program :version, PEM::VERSION
|
15
|
+
program :description, 'CLI for \'PEM\' - Automate maintaining of push notification profiles.'
|
16
|
+
program :help, 'Author', 'Felix Krause <krausefx@gmail.com>'
|
17
|
+
program :help, 'Website', 'http://felixkrause.at'
|
18
|
+
program :help, 'GitHub', 'https://github.com/krausefx/PEM'
|
19
|
+
program :help_formatter, :compact
|
20
|
+
|
21
|
+
global_option('--verbose') { $verbose = true }
|
22
|
+
|
23
|
+
default_command :run
|
24
|
+
|
25
|
+
|
26
|
+
command :renew do |c|
|
27
|
+
c.syntax = 'pem renew'
|
28
|
+
c.description = 'Renews the certificate (in case it expired) and returns the path to the generated pem file'
|
29
|
+
|
30
|
+
c.option '-a', '--identifier STRING', String, 'The bundle identifier of your app'
|
31
|
+
c.option '-u', '--username STRING', String, 'Your Apple ID username'
|
32
|
+
|
33
|
+
c.action do |args, options|
|
34
|
+
app = app_identifier(options)
|
35
|
+
username(options)
|
36
|
+
|
37
|
+
PEM::UpdateChecker.verify_latest_version
|
38
|
+
|
39
|
+
path = PEM::CertManager.new.run(app)
|
40
|
+
|
41
|
+
if path
|
42
|
+
output = "./production_#{app}.pem"
|
43
|
+
FileUtils.mv(path, output)
|
44
|
+
puts output
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
default_command :renew
|
50
|
+
|
51
|
+
def username(options)
|
52
|
+
user = nil
|
53
|
+
user = options.username if options.username
|
54
|
+
user = ENV["PEM_USERNAME"] if ENV["PEM_USERNAME"]
|
55
|
+
|
56
|
+
Deliver::PasswordManager.shared_manager(user) if user
|
57
|
+
end
|
58
|
+
|
59
|
+
def app_identifier(options)
|
60
|
+
return options.identifier if options.identifier
|
61
|
+
return ENV["PEM_APP_IDENTIFIER"] if ENV["PEM_APP_IDENTIFIER"]
|
62
|
+
return ask("App Identifier (Bundle ID, e.g. at.felixkrause.app): ")
|
63
|
+
end
|
data/lib/pem.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
module PEM
|
2
|
+
class CertManager
|
3
|
+
# Download the cert, do all kinds of Keychain related things
|
4
|
+
def run(app_identifier)
|
5
|
+
# Keychain (security) documentation: https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man1/security.1.html
|
6
|
+
# Old project, which might be helpful: https://github.com/jprichardson/keychain_manager
|
7
|
+
|
8
|
+
Helper.log.info "Refreshing push notification profiles for app '#{app_identifier}'"
|
9
|
+
|
10
|
+
dev = PEM::DeveloperCenter.new
|
11
|
+
|
12
|
+
keychain = "PEM.keychain"
|
13
|
+
|
14
|
+
cert_file = dev.fetch_cer_file(app_identifier)
|
15
|
+
rsa_file = [TMP_FOLDER, 'myrsa'].join('/')
|
16
|
+
|
17
|
+
previous_keychain = command("security default-keychain")
|
18
|
+
|
19
|
+
|
20
|
+
|
21
|
+
command("security create-keychain -p '' #{keychain}") # create a new keychain for this type
|
22
|
+
|
23
|
+
command("security list-keychains -d user -s #{keychain}") # add it to the list of keychains
|
24
|
+
|
25
|
+
command("openssl genrsa -out '#{rsa_file}' 2048") # generate a new RSA file
|
26
|
+
command("security import '#{rsa_file}' -P '' -k #{keychain}") # import the RSA file into the Keychain
|
27
|
+
command("security import '#{cert_file}' -k #{keychain}") # import the profile from Apple into the Keychain
|
28
|
+
|
29
|
+
p12_file = [TMP_FOLDER, "push_prod.12"].join('/')
|
30
|
+
|
31
|
+
command("security export -k '#{keychain}' -t all -f pkcs12 -P '' -o #{p12_file}") # export code signing identity
|
32
|
+
|
33
|
+
pem_file = [TMP_FOLDER, "production_#{app_identifier}.pem"].join('')
|
34
|
+
command("openssl pkcs12 -passin pass: -nodes -in #{p12_file} -out #{pem_file}")
|
35
|
+
|
36
|
+
command("security delete-keychain #{keychain}")
|
37
|
+
|
38
|
+
command("security list-keychains -d user -s #{previous_keychain}") # switch back to default keychain
|
39
|
+
|
40
|
+
return pem_file
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
# Output the command, execute it, return its result
|
45
|
+
def command(com)
|
46
|
+
puts com.yellow
|
47
|
+
result = `#{com}`
|
48
|
+
puts result if (result || '').length > 0
|
49
|
+
result
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module PEM
|
2
|
+
class DependencyChecker
|
3
|
+
def self.check_dependencies
|
4
|
+
self.check_phantom_js
|
5
|
+
self.check_xcode_select
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.check_phantom_js
|
9
|
+
if `which phantomjs`.length == 0
|
10
|
+
# Missing brew dependency
|
11
|
+
Helper.log.fatal '#############################################################'
|
12
|
+
Helper.log.fatal "# You have to install phantomjs to use PEM"
|
13
|
+
Helper.log.fatal "# phantomjs is used to control the iTunesConnect frontend"
|
14
|
+
Helper.log.fatal "# Install Homebrew using http://brew.sh/" if `which brew`.length == 0
|
15
|
+
Helper.log.fatal "# Run 'brew update && brew install phantomjs' and start PEM again"
|
16
|
+
Helper.log.fatal '#############################################################'
|
17
|
+
raise "Run 'brew update && brew install phantomjs' and start PEM again"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.check_xcode_select
|
22
|
+
unless `xcode-select -v`.include?"xcode-select version "
|
23
|
+
Helper.log.fatal '#############################################################'
|
24
|
+
Helper.log.fatal "# You have to install the Xcode commdand line tools to use PEM"
|
25
|
+
Helper.log.fatal "# Install the latest version of Xcode from the AppStore"
|
26
|
+
Helper.log.fatal "# Run xcode-select --install to install the developer tools"
|
27
|
+
Helper.log.fatal '#############################################################'
|
28
|
+
raise "Run 'xcode-select --install' and start PEM again"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,312 @@
|
|
1
|
+
require 'pry'
|
2
|
+
require 'deliver/password_manager'
|
3
|
+
require 'open-uri'
|
4
|
+
|
5
|
+
require 'capybara'
|
6
|
+
require 'capybara/poltergeist'
|
7
|
+
|
8
|
+
module PEM
|
9
|
+
class DeveloperCenter
|
10
|
+
# This error occurs only if there is something wrong with the given login data
|
11
|
+
class DeveloperCenterLoginError < StandardError
|
12
|
+
end
|
13
|
+
|
14
|
+
# This error can occur for many reaons. It is
|
15
|
+
# usually raised when a UI element could not be found
|
16
|
+
class DeveloperCenterGeneralError < StandardError
|
17
|
+
end
|
18
|
+
|
19
|
+
include Capybara::DSL
|
20
|
+
|
21
|
+
DEVELOPER_CENTER_URL = "https://developer.apple.com/devcenter/ios/index.action"
|
22
|
+
APP_IDS_URL = "https://developer.apple.com/account/ios/identifiers/bundle/bundleList.action"
|
23
|
+
|
24
|
+
# Strings
|
25
|
+
PRODUCTION_SSL_CERTIFICATE_TITLE = "Production SSL Certificate"
|
26
|
+
|
27
|
+
def initialize
|
28
|
+
FileUtils.mkdir_p TMP_FOLDER
|
29
|
+
|
30
|
+
DependencyChecker.check_dependencies
|
31
|
+
|
32
|
+
Capybara.run_server = false
|
33
|
+
Capybara.default_driver = :poltergeist
|
34
|
+
Capybara.javascript_driver = :poltergeist
|
35
|
+
Capybara.current_driver = :poltergeist
|
36
|
+
Capybara.app_host = DEVELOPER_CENTER_URL
|
37
|
+
|
38
|
+
# Since Apple has some SSL errors, we have to configure the client properly:
|
39
|
+
# https://github.com/ariya/phantomjs/issues/11239
|
40
|
+
Capybara.register_driver :poltergeist do |a|
|
41
|
+
conf = ['--debug=no', '--ignore-ssl-errors=yes', '--ssl-protocol=TLSv1']
|
42
|
+
Capybara::Poltergeist::Driver.new(a, {
|
43
|
+
phantomjs_options: conf,
|
44
|
+
phantomjs_logger: File.open("#{TMP_FOLDER}/poltergeist_log.txt", "a"),
|
45
|
+
js_errors: false
|
46
|
+
})
|
47
|
+
end
|
48
|
+
|
49
|
+
self.login
|
50
|
+
end
|
51
|
+
|
52
|
+
# Loggs in a user with the given login data on the Dev Center Frontend.
|
53
|
+
# You don't need to pass a username and password. It will
|
54
|
+
# Automatically be fetched using the {Deliver::PasswordManager}.
|
55
|
+
# This method will also automatically be called when triggering other
|
56
|
+
# actions like {#open_app_page}
|
57
|
+
# @param user (String) (optional) The username/email address
|
58
|
+
# @param password (String) (optional) The password
|
59
|
+
# @return (bool) true if everything worked fine
|
60
|
+
# @raise [DeveloperCenterGeneralError] General error while executing
|
61
|
+
# this action
|
62
|
+
# @raise [DeveloperCenterLoginError] Login data is wrong
|
63
|
+
def login(user = nil, password = nil)
|
64
|
+
begin
|
65
|
+
Helper.log.info "Login into iOS Developer Center"
|
66
|
+
|
67
|
+
user ||= Deliver::PasswordManager.shared_manager.username
|
68
|
+
password ||= Deliver::PasswordManager.shared_manager.password
|
69
|
+
|
70
|
+
result = visit DEVELOPER_CENTER_URL
|
71
|
+
raise "Could not open Developer Center" unless result['status'] == 'success'
|
72
|
+
|
73
|
+
wait_for_elements(".button.blue").first.click
|
74
|
+
|
75
|
+
(wait_for_elements('#accountpassword') rescue nil) # when the user is already logged in, this will raise an exception
|
76
|
+
|
77
|
+
if page.has_content?"My Apps"
|
78
|
+
# Already logged in
|
79
|
+
return true
|
80
|
+
end
|
81
|
+
|
82
|
+
fill_in "accountname", with: user
|
83
|
+
fill_in "accountpassword", with: password
|
84
|
+
|
85
|
+
begin
|
86
|
+
all(".button.large.blue.signin-button").first.click
|
87
|
+
|
88
|
+
wait_for_elements('#aprerelease')
|
89
|
+
rescue Exception => ex
|
90
|
+
Helper.log.debug ex
|
91
|
+
raise DeveloperCenterLoginError.new("Error logging in user #{user} with the given password. Make sure you entered them correctly.")
|
92
|
+
end
|
93
|
+
|
94
|
+
Helper.log.info "Login successful"
|
95
|
+
|
96
|
+
true
|
97
|
+
rescue Exception => ex
|
98
|
+
error_occured(ex)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def app_status(app_identifier)
|
103
|
+
# TODO
|
104
|
+
end
|
105
|
+
|
106
|
+
# This method will enable push for the given app
|
107
|
+
# and download the cer file in any case, no matter if it existed before or not
|
108
|
+
# @return the path to the push file
|
109
|
+
def fetch_cer_file(app_identifier)
|
110
|
+
begin
|
111
|
+
open_app_page(app_identifier)
|
112
|
+
|
113
|
+
click_on "Edit"
|
114
|
+
wait_for_elements(".item-details") # just to finish loading
|
115
|
+
|
116
|
+
push_value = first(:css, '#pushEnabled').value
|
117
|
+
if push_value == "on"
|
118
|
+
Helper.log.info "Push for app '#{app_identifier}' is enabled"
|
119
|
+
else
|
120
|
+
Helper.log.warn "Push for app '#{app_identifier}' is disabled. This has to change."
|
121
|
+
first(:css, '#pushEnabled').click
|
122
|
+
end
|
123
|
+
|
124
|
+
sleep 1
|
125
|
+
|
126
|
+
# Example code
|
127
|
+
# <div class="appCertificates">
|
128
|
+
# <h3>Apple Push Notification service SSL Certificates</h3>
|
129
|
+
# <p>To configure push notifications for this iOS App ID, a Client SSL Certificate that allows your notification server to connect to the Apple Push Notification Service is required. Each iOS App ID requires its own Client SSL Certificate. Manage and generate your certificates below.</p>
|
130
|
+
# <div class="title" data-hires-status="replaced">Development SSL Certificate</div>
|
131
|
+
# <div class="createCertificate">
|
132
|
+
# <p>Create certificate to use for this App ID.</p>
|
133
|
+
# <a class="button small navLink development enabled" href="/account/ios/certificate/certificateRequest.action?appIdId=...&types=..."><span>Create Certificate...</span></a>
|
134
|
+
# </div>
|
135
|
+
# <div class="title" data-hires-status="replaced">Production SSL Certificate</div>
|
136
|
+
# <div class="certificate">
|
137
|
+
# <dl>
|
138
|
+
# <dt>Name:</dt>
|
139
|
+
# <dd>Apple Production iOS Push Services: net.sunapps.151</dd>
|
140
|
+
# <dt>Type:</dt>
|
141
|
+
# <dd>APNs Production iOS</dd>
|
142
|
+
# <dt>Expires:</dt>
|
143
|
+
# <dd>Nov 14, 2015</dd>
|
144
|
+
# </dl>
|
145
|
+
# <a class="button small revoke-button" href="https://developer.apple.com/services-account/QH65B2/account/ios/certificate/revokeCertificate.action?content-type=application/x-www-form-urlencoded&accept=application/json&requestId=....;userLocale=en_US&teamId=...&certificateId=...&type=...."><span>Revoke</span></a>
|
146
|
+
# <a class="button small download-button" href="/account/ios/certificate/certificateContentDownload.action?displayId=....&type=..."><span>Download</span></a>
|
147
|
+
# </div>
|
148
|
+
# <div class="createCertificate">
|
149
|
+
# <p>Create an additional certificate to use for this App ID.</p>
|
150
|
+
# <a class="button small navLink distribution enabled" href="/account/ios/certificate/certificateRequest.action?appIdId=...&types=..."><span>Create Certificate...</span></a>
|
151
|
+
# </div>
|
152
|
+
# </div>
|
153
|
+
|
154
|
+
def find_download_button
|
155
|
+
wait_for_elements(".formContent")
|
156
|
+
|
157
|
+
certificates_block = first('.appCertificates')
|
158
|
+
download_button = nil
|
159
|
+
|
160
|
+
production_section = false
|
161
|
+
certificates_block.all(:xpath, "./div").each do |div|
|
162
|
+
if production_section
|
163
|
+
# We're now in the second part, we only care about production certificates
|
164
|
+
if (download_button = div.first(".download-button"))
|
165
|
+
return download_button
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
production_section = true if div.text == PRODUCTION_SSL_CERTIFICATE_TITLE
|
170
|
+
end
|
171
|
+
nil
|
172
|
+
end
|
173
|
+
|
174
|
+
|
175
|
+
download_button = find_download_button
|
176
|
+
|
177
|
+
if not download_button
|
178
|
+
Helper.log.warn "Push for app '#{app_identifier}' is enabled, but there is no production certificate yet."
|
179
|
+
create_push_for_app(app_identifier)
|
180
|
+
|
181
|
+
download_button = find_download_button
|
182
|
+
raise "Could not find download button for Production SSL Certificate. Check out: '#{current_url}'" unless download_button
|
183
|
+
end
|
184
|
+
|
185
|
+
|
186
|
+
Helper.log.info "Going to download the latest profile"
|
187
|
+
|
188
|
+
# It is enabled, now just download it
|
189
|
+
# Taken from http://stackoverflow.com/a/17111206/445598
|
190
|
+
sleep 2
|
191
|
+
|
192
|
+
host = Capybara.current_session.current_host
|
193
|
+
url = download_button['href']
|
194
|
+
url = [host, url].join('')
|
195
|
+
|
196
|
+
myacinfo = page.driver.cookies['myacinfo'].value # some magic Apple, which is required for the profile download
|
197
|
+
data = open(url, {'Cookie' => "myacinfo=#{myacinfo}"}).read
|
198
|
+
|
199
|
+
raise "Something went wrong when downloading the certificate" unless data
|
200
|
+
|
201
|
+
path = "#{TMP_FOLDER}/aps_production_#{app_identifier}.cer"
|
202
|
+
File.write(path, data)
|
203
|
+
|
204
|
+
Helper.log.info "Successfully downloaded latest .cer file."
|
205
|
+
return path
|
206
|
+
|
207
|
+
rescue Exception => ex
|
208
|
+
error_occured(ex)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
|
213
|
+
private
|
214
|
+
def open_app_page(app_identifier)
|
215
|
+
begin
|
216
|
+
visit APP_IDS_URL
|
217
|
+
|
218
|
+
apps = all(:xpath, "//td[@title='#{app_identifier}']")
|
219
|
+
if apps.count == 1
|
220
|
+
apps.first.click
|
221
|
+
sleep 1
|
222
|
+
|
223
|
+
return true
|
224
|
+
else
|
225
|
+
raise DeveloperCenterGeneralError.new("Could not find app with identifier '#{app_identifier}' on apps page.")
|
226
|
+
end
|
227
|
+
rescue Exception => ex
|
228
|
+
error_occured(ex)
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
def create_push_for_app(app_identifier)
|
233
|
+
wait_for_elements('.button.small.navLink.distribution.enabled').last.click # Create Certificate button
|
234
|
+
|
235
|
+
sleep 2
|
236
|
+
|
237
|
+
click_next # "Continue"
|
238
|
+
|
239
|
+
sleep 1
|
240
|
+
wait_for_elements(".button.small.center.back") # just to wait
|
241
|
+
|
242
|
+
# Upload CSR file
|
243
|
+
first(:xpath, "//input[@type='file']").set signing_request
|
244
|
+
|
245
|
+
click_next # "Generate"
|
246
|
+
|
247
|
+
while all(:css, '.loadingMessage').count > 0
|
248
|
+
Helper.log.debug "Waiting for iTC to generate the profile"
|
249
|
+
sleep 2
|
250
|
+
end
|
251
|
+
|
252
|
+
open_app_page(app_identifier)
|
253
|
+
click_on "Edit"
|
254
|
+
end
|
255
|
+
|
256
|
+
def signing_request
|
257
|
+
return ENV["PEM_CERT_SIGNING_REQUEST"] if (ENV["PEM_CERT_SIGNING_REQUEST"] and File.exists?(ENV["PEM_CERT_SIGNING_REQUEST"]))
|
258
|
+
|
259
|
+
files = Dir["./*.certSigningRequest"]
|
260
|
+
if files.count == 1
|
261
|
+
Helper.log.debug "Found a .certSigningRequest at the current folder. Using that."
|
262
|
+
return files.first
|
263
|
+
end
|
264
|
+
|
265
|
+
path = nil
|
266
|
+
while not path or not File.exists?path
|
267
|
+
puts "There was no profile found. To create a new one, we need a .certSigningRequest file".yellow
|
268
|
+
puts "To not be asked for a .certSigningRequest, PEM will automatically look for one in the current folder".yellow
|
269
|
+
path = ask("Path to your .certSigningRequest file (including extension): ")
|
270
|
+
puts "Could not find certSigningRequest file at path '#{path}'".red unless File.exists?path
|
271
|
+
end
|
272
|
+
|
273
|
+
return path
|
274
|
+
end
|
275
|
+
|
276
|
+
|
277
|
+
private
|
278
|
+
def click_next
|
279
|
+
wait_for_elements('.button.small.blue.right.submit').last.click
|
280
|
+
end
|
281
|
+
|
282
|
+
def error_occured(ex)
|
283
|
+
snap
|
284
|
+
raise ex # re-raise the error after saving the snapshot
|
285
|
+
end
|
286
|
+
|
287
|
+
def snap
|
288
|
+
path = "Error#{Time.now.to_i}.png"
|
289
|
+
save_screenshot(path, :full => true)
|
290
|
+
system("open '#{path}'")
|
291
|
+
end
|
292
|
+
|
293
|
+
def wait_for_elements(name)
|
294
|
+
counter = 0
|
295
|
+
results = all(name)
|
296
|
+
while results.count == 0
|
297
|
+
# Helper.log.debug "Waiting for #{name}"
|
298
|
+
sleep 0.2
|
299
|
+
|
300
|
+
results = all(name)
|
301
|
+
|
302
|
+
counter += 1
|
303
|
+
if counter > 100
|
304
|
+
Helper.log.debug page.html
|
305
|
+
Helper.log.debug caller
|
306
|
+
raise DeveloperCenterGeneralError.new("Couldn't find element '#{name}' after waiting for quite some time")
|
307
|
+
end
|
308
|
+
end
|
309
|
+
return results
|
310
|
+
end
|
311
|
+
end
|
312
|
+
end
|
data/lib/pem/helper.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module PEM
|
4
|
+
module Helper
|
5
|
+
|
6
|
+
# Logging happens using this method
|
7
|
+
def self.log
|
8
|
+
if is_test?
|
9
|
+
@@log ||= Logger.new(STDOUT) #(nil) # don't show any logs when running tests
|
10
|
+
else
|
11
|
+
@@log ||= Logger.new(STDOUT)
|
12
|
+
end
|
13
|
+
|
14
|
+
@@log.formatter = proc do |severity, datetime, progname, msg|
|
15
|
+
string = "#{severity} [#{datetime.strftime('%Y-%m-%d %H:%M:%S.%2N')}]: "
|
16
|
+
second = "#{msg}\n"
|
17
|
+
|
18
|
+
if severity == "DEBUG"
|
19
|
+
string = string.magenta
|
20
|
+
elsif severity == "INFO"
|
21
|
+
string = string.white
|
22
|
+
elsif severity == "WARN"
|
23
|
+
string = string.yellow
|
24
|
+
elsif severity == "ERROR"
|
25
|
+
string = string.red
|
26
|
+
elsif severity == "FATAL"
|
27
|
+
string = string.red.bold
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
[string, second].join("")
|
32
|
+
end
|
33
|
+
|
34
|
+
@@log
|
35
|
+
end
|
36
|
+
|
37
|
+
# @return true if the currently running program is a unit test
|
38
|
+
def self.is_test?
|
39
|
+
defined?SpecHelper
|
40
|
+
end
|
41
|
+
|
42
|
+
# @return the full path to the Xcode developer tools of the currently
|
43
|
+
# running system
|
44
|
+
def self.xcode_path
|
45
|
+
return "" if self.is_test? and not OS.mac?
|
46
|
+
`xcode-select -p`.gsub("\n", '') + "/"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'open-uri'
|
2
|
+
|
3
|
+
module PEM
|
4
|
+
# Verifies, the user runs the latest version of this gem
|
5
|
+
class UpdateChecker
|
6
|
+
# This method will check if the latest version is installed and show a warning if that's not the case
|
7
|
+
def self.verify_latest_version
|
8
|
+
if self.update_available?
|
9
|
+
v = fetch_latest
|
10
|
+
puts '#######################################################################'.green
|
11
|
+
puts "# PEM #{v} is available.".green
|
12
|
+
puts "# It is recommended to use the latest version.".green
|
13
|
+
puts "# Update using '(sudo) gem update PEM'.".green
|
14
|
+
puts "# To see what's new, open https://github.com/KrauseFx/PEM/releases.".green
|
15
|
+
puts '#######################################################################'.green
|
16
|
+
return true
|
17
|
+
end
|
18
|
+
false
|
19
|
+
end
|
20
|
+
|
21
|
+
# Is a new official release available (this does not include pre-releases)
|
22
|
+
def self.update_available?
|
23
|
+
begin
|
24
|
+
latest = fetch_latest
|
25
|
+
if latest and Gem::Version.new(latest) > Gem::Version.new(current_version)
|
26
|
+
return true
|
27
|
+
end
|
28
|
+
rescue Exception => ex
|
29
|
+
Helper.log.error("Could not check if 'PEM' is up to date.")
|
30
|
+
end
|
31
|
+
return false
|
32
|
+
end
|
33
|
+
|
34
|
+
# The currently used version of this gem
|
35
|
+
def self.current_version
|
36
|
+
PEM::VERSION
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
def self.fetch_latest
|
41
|
+
JSON.parse(open("http://rubygems.org/api/v1/gems/pem.json").read)["version"]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/pem/version.rb
ADDED
metadata
CHANGED
@@ -1,274 +1,243 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pem
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Felix Krause
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-11-
|
11
|
+
date: 2014-11-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: json
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- -
|
17
|
+
- - '>='
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '0'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- -
|
24
|
+
- - '>='
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: security
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- -
|
31
|
+
- - ~>
|
32
32
|
- !ruby/object:Gem::Version
|
33
33
|
version: 0.1.3
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- -
|
38
|
+
- - ~>
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: 0.1.3
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: highline
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- -
|
45
|
+
- - ~>
|
46
46
|
- !ruby/object:Gem::Version
|
47
47
|
version: 1.6.21
|
48
48
|
type: :runtime
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- -
|
52
|
+
- - ~>
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: 1.6.21
|
55
|
-
- !ruby/object:Gem::Dependency
|
56
|
-
name: nokogiri
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
58
|
-
requirements:
|
59
|
-
- - "~>"
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
version: 1.6.3.1
|
62
|
-
type: :runtime
|
63
|
-
prerelease: false
|
64
|
-
version_requirements: !ruby/object:Gem::Requirement
|
65
|
-
requirements:
|
66
|
-
- - "~>"
|
67
|
-
- !ruby/object:Gem::Version
|
68
|
-
version: 1.6.3.1
|
69
|
-
- !ruby/object:Gem::Dependency
|
70
|
-
name: rubyzip
|
71
|
-
requirement: !ruby/object:Gem::Requirement
|
72
|
-
requirements:
|
73
|
-
- - "~>"
|
74
|
-
- !ruby/object:Gem::Version
|
75
|
-
version: 1.1.6
|
76
|
-
type: :runtime
|
77
|
-
prerelease: false
|
78
|
-
version_requirements: !ruby/object:Gem::Requirement
|
79
|
-
requirements:
|
80
|
-
- - "~>"
|
81
|
-
- !ruby/object:Gem::Version
|
82
|
-
version: 1.1.6
|
83
55
|
- !ruby/object:Gem::Dependency
|
84
56
|
name: colored
|
85
57
|
requirement: !ruby/object:Gem::Requirement
|
86
58
|
requirements:
|
87
|
-
- -
|
59
|
+
- - '>='
|
88
60
|
- !ruby/object:Gem::Version
|
89
61
|
version: '0'
|
90
62
|
type: :runtime
|
91
63
|
prerelease: false
|
92
64
|
version_requirements: !ruby/object:Gem::Requirement
|
93
65
|
requirements:
|
94
|
-
- -
|
66
|
+
- - '>='
|
95
67
|
- !ruby/object:Gem::Version
|
96
68
|
version: '0'
|
97
69
|
- !ruby/object:Gem::Dependency
|
98
70
|
name: commander
|
99
71
|
requirement: !ruby/object:Gem::Requirement
|
100
72
|
requirements:
|
101
|
-
- -
|
73
|
+
- - ~>
|
102
74
|
- !ruby/object:Gem::Version
|
103
75
|
version: 4.2.0
|
104
76
|
type: :runtime
|
105
77
|
prerelease: false
|
106
78
|
version_requirements: !ruby/object:Gem::Requirement
|
107
79
|
requirements:
|
108
|
-
- -
|
80
|
+
- - ~>
|
109
81
|
- !ruby/object:Gem::Version
|
110
82
|
version: 4.2.0
|
111
|
-
- !ruby/object:Gem::Dependency
|
112
|
-
name: prawn
|
113
|
-
requirement: !ruby/object:Gem::Requirement
|
114
|
-
requirements:
|
115
|
-
- - ">="
|
116
|
-
- !ruby/object:Gem::Version
|
117
|
-
version: '0'
|
118
|
-
type: :runtime
|
119
|
-
prerelease: false
|
120
|
-
version_requirements: !ruby/object:Gem::Requirement
|
121
|
-
requirements:
|
122
|
-
- - ">="
|
123
|
-
- !ruby/object:Gem::Version
|
124
|
-
version: '0'
|
125
83
|
- !ruby/object:Gem::Dependency
|
126
84
|
name: deliver
|
127
85
|
requirement: !ruby/object:Gem::Requirement
|
128
86
|
requirements:
|
129
|
-
- -
|
87
|
+
- - ~>
|
130
88
|
- !ruby/object:Gem::Version
|
131
89
|
version: '0.3'
|
132
90
|
type: :runtime
|
133
91
|
prerelease: false
|
134
92
|
version_requirements: !ruby/object:Gem::Requirement
|
135
93
|
requirements:
|
136
|
-
- -
|
94
|
+
- - ~>
|
137
95
|
- !ruby/object:Gem::Version
|
138
96
|
version: '0.3'
|
139
97
|
- !ruby/object:Gem::Dependency
|
140
98
|
name: capybara
|
141
99
|
requirement: !ruby/object:Gem::Requirement
|
142
100
|
requirements:
|
143
|
-
- -
|
101
|
+
- - ~>
|
144
102
|
- !ruby/object:Gem::Version
|
145
103
|
version: 2.4.3
|
146
104
|
type: :runtime
|
147
105
|
prerelease: false
|
148
106
|
version_requirements: !ruby/object:Gem::Requirement
|
149
107
|
requirements:
|
150
|
-
- -
|
108
|
+
- - ~>
|
151
109
|
- !ruby/object:Gem::Version
|
152
110
|
version: 2.4.3
|
153
111
|
- !ruby/object:Gem::Dependency
|
154
112
|
name: poltergeist
|
155
113
|
requirement: !ruby/object:Gem::Requirement
|
156
114
|
requirements:
|
157
|
-
- -
|
115
|
+
- - ~>
|
158
116
|
- !ruby/object:Gem::Version
|
159
117
|
version: 1.5.1
|
160
118
|
type: :runtime
|
161
119
|
prerelease: false
|
162
120
|
version_requirements: !ruby/object:Gem::Requirement
|
163
121
|
requirements:
|
164
|
-
- -
|
122
|
+
- - ~>
|
165
123
|
- !ruby/object:Gem::Version
|
166
124
|
version: 1.5.1
|
167
125
|
- !ruby/object:Gem::Dependency
|
168
126
|
name: bundler
|
169
127
|
requirement: !ruby/object:Gem::Requirement
|
170
128
|
requirements:
|
171
|
-
- -
|
129
|
+
- - '>='
|
172
130
|
- !ruby/object:Gem::Version
|
173
131
|
version: '0'
|
174
132
|
type: :development
|
175
133
|
prerelease: false
|
176
134
|
version_requirements: !ruby/object:Gem::Requirement
|
177
135
|
requirements:
|
178
|
-
- -
|
136
|
+
- - '>='
|
179
137
|
- !ruby/object:Gem::Version
|
180
138
|
version: '0'
|
181
139
|
- !ruby/object:Gem::Dependency
|
182
140
|
name: rake
|
183
141
|
requirement: !ruby/object:Gem::Requirement
|
184
142
|
requirements:
|
185
|
-
- -
|
143
|
+
- - '>='
|
186
144
|
- !ruby/object:Gem::Version
|
187
145
|
version: '0'
|
188
146
|
type: :development
|
189
147
|
prerelease: false
|
190
148
|
version_requirements: !ruby/object:Gem::Requirement
|
191
149
|
requirements:
|
192
|
-
- -
|
150
|
+
- - '>='
|
193
151
|
- !ruby/object:Gem::Version
|
194
152
|
version: '0'
|
195
153
|
- !ruby/object:Gem::Dependency
|
196
154
|
name: rspec
|
197
155
|
requirement: !ruby/object:Gem::Requirement
|
198
156
|
requirements:
|
199
|
-
- -
|
157
|
+
- - ~>
|
200
158
|
- !ruby/object:Gem::Version
|
201
159
|
version: 3.1.0
|
202
160
|
type: :development
|
203
161
|
prerelease: false
|
204
162
|
version_requirements: !ruby/object:Gem::Requirement
|
205
163
|
requirements:
|
206
|
-
- -
|
164
|
+
- - ~>
|
207
165
|
- !ruby/object:Gem::Version
|
208
166
|
version: 3.1.0
|
209
167
|
- !ruby/object:Gem::Dependency
|
210
168
|
name: pry
|
211
169
|
requirement: !ruby/object:Gem::Requirement
|
212
170
|
requirements:
|
213
|
-
- -
|
171
|
+
- - '>='
|
214
172
|
- !ruby/object:Gem::Version
|
215
173
|
version: '0'
|
216
174
|
type: :development
|
217
175
|
prerelease: false
|
218
176
|
version_requirements: !ruby/object:Gem::Requirement
|
219
177
|
requirements:
|
220
|
-
- -
|
178
|
+
- - '>='
|
221
179
|
- !ruby/object:Gem::Version
|
222
180
|
version: '0'
|
223
181
|
- !ruby/object:Gem::Dependency
|
224
182
|
name: yard
|
225
183
|
requirement: !ruby/object:Gem::Requirement
|
226
184
|
requirements:
|
227
|
-
- -
|
185
|
+
- - ~>
|
228
186
|
- !ruby/object:Gem::Version
|
229
187
|
version: 0.8.7.4
|
230
188
|
type: :development
|
231
189
|
prerelease: false
|
232
190
|
version_requirements: !ruby/object:Gem::Requirement
|
233
191
|
requirements:
|
234
|
-
- -
|
192
|
+
- - ~>
|
235
193
|
- !ruby/object:Gem::Version
|
236
194
|
version: 0.8.7.4
|
237
195
|
- !ruby/object:Gem::Dependency
|
238
196
|
name: webmock
|
239
197
|
requirement: !ruby/object:Gem::Requirement
|
240
198
|
requirements:
|
241
|
-
- -
|
199
|
+
- - ~>
|
242
200
|
- !ruby/object:Gem::Version
|
243
201
|
version: 1.19.0
|
244
202
|
type: :development
|
245
203
|
prerelease: false
|
246
204
|
version_requirements: !ruby/object:Gem::Requirement
|
247
205
|
requirements:
|
248
|
-
- -
|
206
|
+
- - ~>
|
249
207
|
- !ruby/object:Gem::Version
|
250
208
|
version: 1.19.0
|
251
209
|
- !ruby/object:Gem::Dependency
|
252
210
|
name: codeclimate-test-reporter
|
253
211
|
requirement: !ruby/object:Gem::Requirement
|
254
212
|
requirements:
|
255
|
-
- -
|
213
|
+
- - '>='
|
256
214
|
- !ruby/object:Gem::Version
|
257
215
|
version: '0'
|
258
216
|
type: :development
|
259
217
|
prerelease: false
|
260
218
|
version_requirements: !ruby/object:Gem::Requirement
|
261
219
|
requirements:
|
262
|
-
- -
|
220
|
+
- - '>='
|
263
221
|
- !ruby/object:Gem::Version
|
264
222
|
version: '0'
|
265
223
|
description: To be announced soon
|
266
224
|
email:
|
267
225
|
- krausefx@gmail.com
|
268
|
-
executables:
|
226
|
+
executables:
|
227
|
+
- pem
|
269
228
|
extensions: []
|
270
229
|
extra_rdoc_files: []
|
271
|
-
files:
|
230
|
+
files:
|
231
|
+
- LICENSE
|
232
|
+
- README.md
|
233
|
+
- bin/pem
|
234
|
+
- lib/pem.rb
|
235
|
+
- lib/pem/cert_manager.rb
|
236
|
+
- lib/pem/dependency_checker.rb
|
237
|
+
- lib/pem/developer_center.rb
|
238
|
+
- lib/pem/helper.rb
|
239
|
+
- lib/pem/update_checker.rb
|
240
|
+
- lib/pem/version.rb
|
272
241
|
homepage: http://felixkrause.at
|
273
242
|
licenses:
|
274
243
|
- MIT
|
@@ -280,12 +249,12 @@ require_paths:
|
|
280
249
|
- lib
|
281
250
|
required_ruby_version: !ruby/object:Gem::Requirement
|
282
251
|
requirements:
|
283
|
-
- -
|
252
|
+
- - '>='
|
284
253
|
- !ruby/object:Gem::Version
|
285
254
|
version: 2.0.0
|
286
255
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
287
256
|
requirements:
|
288
|
-
- -
|
257
|
+
- - '>='
|
289
258
|
- !ruby/object:Gem::Version
|
290
259
|
version: '0'
|
291
260
|
requirements: []
|