passbook-iid 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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