passbook-iid 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. data/.travis.yml +7 -0
  2. data/Gemfile +17 -0
  3. data/Gemfile.lock +65 -0
  4. data/LICENSE +22 -0
  5. data/README.md +290 -0
  6. data/Rakefile +36 -0
  7. data/VERSION +1 -0
  8. data/bin/pk +22 -0
  9. data/lib/commands/build.rb +62 -0
  10. data/lib/commands/commands.rb +31 -0
  11. data/lib/commands/generate.rb +44 -0
  12. data/lib/commands/templates/boarding-pass.json +56 -0
  13. data/lib/commands/templates/coupon.json +33 -0
  14. data/lib/commands/templates/event-ticket.json +33 -0
  15. data/lib/commands/templates/generic.json +33 -0
  16. data/lib/commands/templates/store-card.json +33 -0
  17. data/lib/passbook.rb +15 -0
  18. data/lib/passbook/pkpass.rb +120 -0
  19. data/lib/passbook/push_notification.rb +10 -0
  20. data/lib/passbook/signer.rb +40 -0
  21. data/lib/passbook/version.rb +3 -0
  22. data/lib/rack/passbook_rack.rb +98 -0
  23. data/lib/rails/generators/passbook/config/config_generator.rb +16 -0
  24. data/lib/rails/generators/passbook/config/templates/initializer.rb +13 -0
  25. data/lib/utils/command_utils.rb +12 -0
  26. data/passbook.gemspec +110 -0
  27. data/spec/data/icon.png +0 -0
  28. data/spec/data/icon@2x.png +0 -0
  29. data/spec/data/logo.png +0 -0
  30. data/spec/data/logo@2x.png +0 -0
  31. data/spec/lib/commands/build_spec.rb +92 -0
  32. data/spec/lib/commands/commands_spec.rb +102 -0
  33. data/spec/lib/commands/commands_spec_helper.rb +69 -0
  34. data/spec/lib/commands/generate_spec.rb +72 -0
  35. data/spec/lib/passbook/pkpass_spec.rb +108 -0
  36. data/spec/lib/passbook/push_notification_spec.rb +22 -0
  37. data/spec/lib/passbook/signer_spec.rb +84 -0
  38. data/spec/lib/rack/passbook_rack_spec.rb +233 -0
  39. data/spec/spec_helper.rb +9 -0
  40. metadata +216 -0
@@ -0,0 +1,10 @@
1
+ module Passbook
2
+ class PushNotification
3
+ def self.send_notification(device_token)
4
+ pusher = Grocer.pusher({:certificate => Passbook.notification_cert, :gateway => Passbook.notification_gateway})
5
+ notification = Grocer::PassbookNotification.new(:device_token => device_token)
6
+
7
+ pusher.push notification
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,40 @@
1
+ require 'openssl'
2
+ require 'base64'
3
+
4
+ module Passbook
5
+ class Signer
6
+ attr_accessor :certificate, :password, :key, :wwdc_cert, :key_hash, :p12_cert
7
+
8
+ def initialize params = {}
9
+ @certificate = params[:certificate] || Passbook.p12_certificate
10
+ @password = params[:password] || Passbook.p12_password
11
+ @key = params[:key] || (params.empty? ? Passbook.p12_key : nil)
12
+ @wwdc_cert = params[:wwdc_cert] || Passbook.wwdc_cert
13
+ compute_cert
14
+ end
15
+
16
+ def sign data
17
+ wwdc = OpenSSL::X509::Certificate.new File.read(wwdc_cert)
18
+ pk7 = OpenSSL::PKCS7.sign key_hash[:cert], key_hash[:key], data.to_s, [wwdc], OpenSSL::PKCS7::BINARY | OpenSSL::PKCS7::DETACHED
19
+ data = OpenSSL::PKCS7.write_smime pk7
20
+
21
+ str_debut = "filename=\"smime.p7s\"\n\n"
22
+ data = data[data.index(str_debut)+str_debut.length..data.length-1]
23
+ str_end = "\n\n------"
24
+ data = data[0..data.index(str_end)-1]
25
+
26
+ return Base64.decode64(data)
27
+ end
28
+
29
+ def compute_cert
30
+ @key_hash = {}
31
+ if key
32
+ @key_hash[:key] = OpenSSL::PKey::RSA.new File.read(key), password
33
+ @key_hash[:cert] = OpenSSL::X509::Certificate.new File.read(certificate)
34
+ else
35
+ p12 = OpenSSL::PKCS12.new File.read(certificate), password
36
+ @key_hash[:key], @key_hash[:cert] = p12.key, p12.certificate
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,3 @@
1
+ module Passbook
2
+ VERSION = "0.0.4"
3
+ end
@@ -0,0 +1,98 @@
1
+ module Rack
2
+ class PassbookRack
3
+
4
+ def initialize(app)
5
+ @app = app
6
+ @parameters = {}
7
+ end
8
+
9
+ def call(env)
10
+ @parameters['authToken'] = env['HTTP_AUTHORIZATION'].gsub(/ApplePass /,'') if env['HTTP_AUTHORIZATION']
11
+ @parameters.merge!(Rack::Utils.parse_nested_query(env['QUERY_STRING']))
12
+ method_and_params = find_method env['PATH_INFO']
13
+ if method_and_params
14
+ case method_and_params[:method]
15
+ when 'device_register_delete'
16
+ if env['REQUEST_METHOD'] == 'POST'
17
+ [Passbook::PassbookNotification.
18
+ register_pass(method_and_params[:params].merge! JSON.parse(env['rack.input'].read 1000))[:status],
19
+ {}, ['']]
20
+ elsif env['REQUEST_METHOD'] == 'DELETE'
21
+ [Passbook::PassbookNotification.unregister_pass(method_and_params[:params])[:status], {}, {}]
22
+ end
23
+ when 'passes_for_device'
24
+ response = Passbook::PassbookNotification.passes_for_device(method_and_params[:params])
25
+ [response ? 200 : 204, {}, [response.to_json]]
26
+ when 'latest_pass'
27
+ response = Passbook::PassbookNotification.latest_pass(method_and_params[:params])
28
+ if response
29
+ [200, {'Content-Type' => 'application/vnd.apple.pkpass',
30
+ 'Content-Disposition' => 'attachment',
31
+ 'filename' => "#{method_and_params[:params]['serialNumber']}.pkpass"}, [response]]
32
+ else
33
+ [204, {}, {}]
34
+ end
35
+ when 'log'
36
+ Passbook::PassbookNotification.passbook_log JSON.parse(env['rack.input'].read 10000)
37
+ [200, {}, {}]
38
+ else
39
+ end
40
+ else
41
+ @app.call env
42
+ end
43
+ end
44
+
45
+ def append_parameter_separator url
46
+ end
47
+
48
+ def each(&block)
49
+ end
50
+
51
+
52
+ def find_method(path)
53
+ parsed_path = path.split '/'
54
+ url_beginning = parsed_path.index 'v1'
55
+ if url_beginning
56
+ args_length = parsed_path.size - url_beginning
57
+
58
+ if (parsed_path[url_beginning + 1 ] == 'devices') and
59
+ (parsed_path[url_beginning + 3 ] == 'registrations')
60
+ if args_length == 6
61
+ return method_and_params_hash 'device_register_delete', path
62
+ elsif args_length == 5
63
+ return method_and_params_hash 'passes_for_device', path
64
+ end
65
+ elsif parsed_path[url_beginning + 1] == 'passes' and args_length == 4
66
+ return method_and_params_hash 'latest_pass', path
67
+ elsif parsed_path[url_beginning + 1] == 'log' and args_length == 2
68
+ return {:method => 'log'}
69
+ end
70
+ end
71
+
72
+ return nil
73
+ end
74
+
75
+ private
76
+
77
+ def method_and_params_hash(method, path)
78
+ parsed_path = path.split '/'
79
+ url_beginning = parsed_path.index 'v1'
80
+ if method == 'latest_pass'
81
+ {:method => 'latest_pass',
82
+ :params => @parameters.merge!({'passTypeIdentifier' => parsed_path[url_beginning + 2],
83
+ 'serialNumber' => parsed_path[url_beginning + 3]})}
84
+ else
85
+ return_hash = {:method => method, :params =>
86
+ @parameters.merge!({'deviceLibraryIdentifier' => parsed_path[url_beginning + 2],
87
+ 'passTypeIdentifier' => parsed_path[url_beginning + 4]})}
88
+ if method == 'device_register_delete'
89
+ return_hash[:params]['serialNumber'] = parsed_path[url_beginning + 5]
90
+ end
91
+ return_hash
92
+ end
93
+ end
94
+
95
+
96
+ end
97
+ end
98
+
@@ -0,0 +1,16 @@
1
+ module Passbook
2
+ module Generators
3
+ class ConfigGenerator < Rails::Generators::Base
4
+ source_root File.expand_path('../templates', __FILE__)
5
+
6
+ argument :wwdc_cert_path, type: :string, default: '', optional: true, banner: "Absolute path to your wwdc cert file"
7
+ argument :p12_cert_path, type: :string, default: '', optional: true, banner: "Absolute path to your cert.p12 file"
8
+ argument :p12_password, type: :string, default: '', optional: true, banner: "Password for your certificate"
9
+
10
+ desc 'Create passbook initializer'
11
+ def create_initializer_file
12
+ template 'initializer.rb', File.join('config', 'initializers', 'passbook.rb')
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,13 @@
1
+ require 'passbook'
2
+
3
+ Passbook.configure do |passbook|
4
+
5
+ # Path to your wwdc cert file
6
+ passbook.wwdc_cert = '<%= wwdc_cert_path %>'
7
+
8
+ # Path to your cert.p12 file
9
+ passbook.p12_cert = '<%= p12_cert_path %>'
10
+
11
+ # Password for your certificate
12
+ passbook.p12_password = '<%= p12_password %>'
13
+ end
@@ -0,0 +1,12 @@
1
+ # this was added for testability because I couldn't figure out something better.
2
+ class CommandUtils
3
+ def self.get_assets(directory)
4
+ Dir[File.join(directory, '*')]
5
+ end
6
+
7
+ def self.get_current_directory
8
+ File.dirname(__FILE__)
9
+ end
10
+ end
11
+
12
+
data/passbook.gemspec ADDED
@@ -0,0 +1,110 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+ # stub: passbook 0.4.0 ruby lib
6
+
7
+ Gem::Specification.new do |s|
8
+ s.name = "passbook-iid"
9
+ s.version = "0.4.0"
10
+
11
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
12
+ s.require_paths = ["lib"]
13
+ s.authors = ["Thomas Lauro", "Lance Gleason"]
14
+ s.date = "2014-06-22"
15
+ s.description = "This gem allows you to create IOS Passbooks. Unlike some, this works with Rails but does not require it."
16
+ s.email = ["thomas@lauro.fr", "lgleason@polyglotprogramminginc.com"]
17
+ s.executables = ["pk"]
18
+ s.extra_rdoc_files = [
19
+ "LICENSE",
20
+ "README.md"
21
+ ]
22
+ s.files = [
23
+ ".travis.yml",
24
+ "Gemfile",
25
+ "Gemfile.lock",
26
+ "LICENSE",
27
+ "README.md",
28
+ "Rakefile",
29
+ "VERSION",
30
+ "bin/pk",
31
+ "lib/commands/build.rb",
32
+ "lib/commands/commands.rb",
33
+ "lib/commands/generate.rb",
34
+ "lib/commands/templates/boarding-pass.json",
35
+ "lib/commands/templates/coupon.json",
36
+ "lib/commands/templates/event-ticket.json",
37
+ "lib/commands/templates/generic.json",
38
+ "lib/commands/templates/store-card.json",
39
+ "lib/passbook.rb",
40
+ "lib/passbook/pkpass.rb",
41
+ "lib/passbook/push_notification.rb",
42
+ "lib/passbook/signer.rb",
43
+ "lib/passbook/version.rb",
44
+ "lib/rack/passbook_rack.rb",
45
+ "lib/rails/generators/passbook/config/config_generator.rb",
46
+ "lib/rails/generators/passbook/config/templates/initializer.rb",
47
+ "lib/utils/command_utils.rb",
48
+ "passbook.gemspec",
49
+ "spec/data/icon.png",
50
+ "spec/data/icon@2x.png",
51
+ "spec/data/logo.png",
52
+ "spec/data/logo@2x.png",
53
+ "spec/lib/commands/build_spec.rb",
54
+ "spec/lib/commands/commands_spec.rb",
55
+ "spec/lib/commands/commands_spec_helper.rb",
56
+ "spec/lib/commands/generate_spec.rb",
57
+ "spec/lib/passbook/pkpass_spec.rb",
58
+ "spec/lib/passbook/push_notification_spec.rb",
59
+ "spec/lib/passbook/signer_spec.rb",
60
+ "spec/lib/rack/passbook_rack_spec.rb",
61
+ "spec/spec_helper.rb"
62
+ ]
63
+ s.homepage = "http://github.com/frozon/passbook"
64
+ s.licenses = ["MIT"]
65
+ s.rubygems_version = "2.2.2"
66
+ s.summary = "A IOS Passbook generator."
67
+
68
+ if s.respond_to? :specification_version then
69
+ s.specification_version = 4
70
+
71
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
72
+ s.add_runtime_dependency(%q<rubyzip>, ["~> 1.0.0"])
73
+ s.add_runtime_dependency(%q<grocer>, [">= 0"])
74
+ s.add_runtime_dependency(%q<commander>, [">= 0"])
75
+ s.add_runtime_dependency(%q<terminal-table>, [">= 0"])
76
+ s.add_development_dependency(%q<rack-test>, [">= 0"])
77
+ s.add_development_dependency(%q<activesupport>, [">= 0"])
78
+ s.add_development_dependency(%q<jeweler>, [">= 0"])
79
+ s.add_development_dependency(%q<simplecov>, [">= 0"])
80
+ s.add_development_dependency(%q<rspec>, [">= 0"])
81
+ s.add_development_dependency(%q<rake>, [">= 0"])
82
+ s.add_development_dependency(%q<yard>, [">= 0"])
83
+ else
84
+ s.add_dependency(%q<rubyzip>, [">= 1.0.0"])
85
+ s.add_dependency(%q<grocer>, [">= 0"])
86
+ s.add_dependency(%q<commander>, [">= 0"])
87
+ s.add_dependency(%q<terminal-table>, [">= 0"])
88
+ s.add_dependency(%q<rack-test>, [">= 0"])
89
+ s.add_dependency(%q<activesupport>, [">= 0"])
90
+ s.add_dependency(%q<jeweler>, [">= 0"])
91
+ s.add_dependency(%q<simplecov>, [">= 0"])
92
+ s.add_dependency(%q<rspec>, [">= 0"])
93
+ s.add_dependency(%q<rake>, [">= 0"])
94
+ s.add_dependency(%q<yard>, [">= 0"])
95
+ end
96
+ else
97
+ s.add_dependency(%q<rubyzip>, ["~> 1.0.0"])
98
+ s.add_dependency(%q<grocer>, [">= 0"])
99
+ s.add_dependency(%q<commander>, [">= 0"])
100
+ s.add_dependency(%q<terminal-table>, [">= 0"])
101
+ s.add_dependency(%q<rack-test>, [">= 0"])
102
+ s.add_dependency(%q<activesupport>, [">= 0"])
103
+ s.add_dependency(%q<jeweler>, [">= 0"])
104
+ s.add_dependency(%q<simplecov>, [">= 0"])
105
+ s.add_dependency(%q<rspec>, [">= 0"])
106
+ s.add_dependency(%q<rake>, [">= 0"])
107
+ s.add_dependency(%q<yard>, [">= 0"])
108
+ end
109
+ end
110
+
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,92 @@
1
+ require 'lib/commands/commands_spec_helper'
2
+ require 'passbook'
3
+
4
+ describe 'Build' do
5
+
6
+ before :each do
7
+ $stderr = StringIO.new
8
+ mock_terminal
9
+ end
10
+
11
+ context 'command' do
12
+ specify 'missing directory' do
13
+ run_command 'build' do
14
+ @output.string.should eq "\e[31mMissing argument\e[0m\n"
15
+ end
16
+ end
17
+
18
+ context 'good directory' do
19
+
20
+ before :each do
21
+ File.should_receive(:directory?).with('scraps').and_return true
22
+ File.should_receive(:exist?).once.with('scraps/pass.json').and_return true
23
+ File.should_receive(:exist?).once.with('scraps.pkpass').and_return false
24
+ end
25
+
26
+ specify 'no certificate file' do
27
+ run_command 'build', 'scraps' do
28
+ @output.string.should eq "\e[31mMissing or invalid certificate file\e[0m\n"
29
+ end
30
+ end
31
+
32
+ context 'good certificate file' do
33
+ before :each do
34
+ File.should_receive(:exist?).with('jackels').and_return true
35
+ end
36
+
37
+ specify 'no certificate password entered' do
38
+ run_command 'build', 'scraps', '-w', 'jackels' do
39
+ @output.string.should eq "Enter certificate password:\n\n"
40
+ end
41
+ end
42
+
43
+ context 'all required values' do
44
+
45
+ let(:pass_json) {'{"this":"is awesome json"}'}
46
+ let(:pass_assets) {['pass.json', 'something.jpeg']}
47
+
48
+ before :each do
49
+ Passbook.should_receive(:wwdc_cert=).with 'jackels'
50
+ Passbook.should_receive(:p12_key=).with 'badger_key'
51
+ Passbook.should_receive(:p12_certificate=).with 'badger_cert'
52
+ Passbook.should_receive(:p12_password=).with 'bees'
53
+ CommandUtils.should_receive(:get_assets).with('scraps').and_return pass_assets
54
+ @pk_pass = double 'pk pass'
55
+ File.should_receive(:read).with('pass.json').and_return pass_json
56
+ Passbook::PKPass.should_receive(:new).with(pass_json).and_return @pk_pass
57
+ @pk_pass.should_receive(:addFiles).with pass_assets
58
+ end
59
+
60
+ specify 'are set from command line' do
61
+ pass_stream = double 'passbook stream'
62
+ pass_stream.should_receive(:string).and_return 'my badass pass'
63
+ @pk_pass.should_receive(:stream).and_return pass_stream
64
+ File.should_receive(:open).with('scraps.pkpass', 'w')
65
+
66
+ run_command 'build', 'scraps', '-w', 'jackels',
67
+ '-p', 'bees', '-k', 'badger_key', '-c', 'badger_cert' do
68
+ @output.string.should eq ""
69
+ end
70
+ end
71
+
72
+ specify 'should catch a general error' do
73
+ @pk_pass.should_receive(:stream).and_raise(StandardError.new('I have failed'))
74
+ run_command 'build', 'scraps', '-w', 'jackels',
75
+ '-p', 'bees', '-k', 'badger_key', '-c', 'badger_cert' do
76
+ @output.string.should eq "\e[31mError: I have failed\e[0m\n"
77
+ end
78
+ end
79
+
80
+ specify 'should catch a general error' do
81
+ @pk_pass.should_receive(:stream).and_raise(OpenSSL::PKCS12::PKCS12Error.new('I am a failure'))
82
+ run_command 'build', 'scraps', '-w', 'jackels',
83
+ '-p', 'bees', '-k', 'badger_key', '-c', 'badger_cert' do
84
+ @output.string.should eq "\e[31mError: I am a failure\e[0m\n\e[33mYou may be getting this error because the certificate password is either incorrect or missing\e[0m\n"
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+
92
+ end
@@ -0,0 +1,102 @@
1
+ require 'lib/commands/commands_spec_helper'
2
+
3
+ describe 'Commands' do
4
+
5
+ before :each do
6
+ program :version, '1.2.3'
7
+ program :description, "Honey Badger Don't Care"
8
+ end
9
+
10
+ context 'determine directory' do
11
+
12
+ specify 'no directories' do
13
+ Dir.should_receive(:[]).and_return []
14
+ determine_directory!
15
+ @directory.should eq nil
16
+ end
17
+
18
+ specify 'one directory present' do
19
+ Dir.should_receive(:[]).and_return ['ass']
20
+ File.should_receive(:dirname).with('ass').and_return('/honey/badger/is/bad/ass')
21
+ determine_directory!
22
+ @directory.should eq '/honey/badger/is/bad/ass'
23
+ end
24
+
25
+ specify 'multiple directories' do
26
+ Dir.should_receive(:[]).and_return ['cobras', 'bee_larvae']
27
+ File.should_receive(:dirname).with('cobras').and_return('/yummy/cobras')
28
+ File.should_receive(:dirname).with('bee_larvae').and_return('/disgusting/bee_larvae')
29
+ self.should_receive(:choose).with('Select a directory:',
30
+ '/yummy/cobras', '/disgusting/bee_larvae').and_return '/yummy/cobras'
31
+ determine_directory!
32
+ @directory.should eq '/yummy/cobras'
33
+ end
34
+
35
+ end
36
+
37
+ context 'validate directory' do
38
+
39
+ specify 'missing directory' do
40
+ self.should_receive(:say_error).with('Missing argument').and_return true
41
+ lambda {
42
+ @directory = nil
43
+ validate_directory!
44
+ }.should exit_with_code(1)
45
+ end
46
+
47
+ specify 'directory does not exist' do
48
+ self.should_receive(:say_error).with("Directory scraps does not exist").and_return true
49
+ File.should_receive(:directory?).with('scraps').and_return false
50
+ lambda {
51
+ @directory = 'scraps'
52
+ validate_directory!
53
+ }.should exit_with_code(1)
54
+ end
55
+
56
+ specify 'directory does not have a valid pass' do
57
+ self.should_receive(:say_error).with("Directory scraps is not a valid pass").and_return true
58
+ File.should_receive(:directory?).with('scraps').and_return true
59
+ File.should_receive(:exist?).with('scraps/pass.json').and_return false
60
+ lambda {
61
+ @directory = 'scraps'
62
+ validate_directory!
63
+ }.should exit_with_code(1)
64
+ end
65
+
66
+ specify 'directory has valid pass' do
67
+ File.should_receive(:directory?).with('scraps').and_return true
68
+ File.should_receive(:exist?).with('scraps/pass.json').and_return true
69
+ lambda {
70
+ @directory = 'scraps'
71
+ validate_directory!
72
+ }.should_not exit_with_code(1)
73
+ end
74
+ end
75
+
76
+ context 'validate certificate' do
77
+ specify 'nil certificate' do
78
+ self.should_receive(:say_error).with("Missing or invalid certificate file").and_return true
79
+ lambda {
80
+ @certificate = nil
81
+ validate_certificate!
82
+ }.should exit_with_code(1)
83
+ end
84
+
85
+ specify 'certificate file does not exist' do
86
+ self.should_receive(:say_error).with("Missing or invalid certificate file").and_return true
87
+ File.should_receive(:exist?).with('jackels').and_return false
88
+ lambda {
89
+ @certificate = 'jackels'
90
+ validate_certificate!
91
+ }.should exit_with_code(1)
92
+ end
93
+
94
+ specify 'certificate file exists' do
95
+ File.should_receive(:exist?).with('jackels').and_return true
96
+ lambda {
97
+ @certificate = 'jackels'
98
+ validate_certificate!
99
+ }.should_not exit_with_code(1)
100
+ end
101
+ end
102
+ end