passifier 0.0.2

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 (43) hide show
  1. data/Gemfile +15 -0
  2. data/LICENSE.md +22 -0
  3. data/README.md +125 -0
  4. data/Rakefile +48 -0
  5. data/examples/assets/background.png +0 -0
  6. data/examples/assets/background@2x.png +0 -0
  7. data/examples/assets/icon.png +0 -0
  8. data/examples/assets/icon@2x.png +0 -0
  9. data/examples/assets/logo.png +0 -0
  10. data/examples/assets/logo@2x.png +0 -0
  11. data/examples/assets/thumbnail.png +0 -0
  12. data/examples/assets/thumbnail@2x.png +0 -0
  13. data/examples/simple.rb +87 -0
  14. data/lib/passifier.rb +26 -0
  15. data/lib/passifier/archive.rb +52 -0
  16. data/lib/passifier/manifest.rb +37 -0
  17. data/lib/passifier/manifest_signature.rb +33 -0
  18. data/lib/passifier/pass.rb +82 -0
  19. data/lib/passifier/signing.rb +38 -0
  20. data/lib/passifier/spec.rb +31 -0
  21. data/lib/passifier/static_file.rb +24 -0
  22. data/lib/passifier/storage.rb +94 -0
  23. data/lib/passifier/url_source.rb +30 -0
  24. data/test/assets/background.png +0 -0
  25. data/test/assets/background@2x.png +0 -0
  26. data/test/assets/icon.png +0 -0
  27. data/test/assets/icon@2x.png +0 -0
  28. data/test/assets/logo.png +0 -0
  29. data/test/assets/logo@2x.png +0 -0
  30. data/test/assets/thumbnail.png +0 -0
  31. data/test/assets/thumbnail@2x.png +0 -0
  32. data/test/helper.rb +159 -0
  33. data/test/passifier/test_archive.rb +44 -0
  34. data/test/passifier/test_manifest.rb +27 -0
  35. data/test/passifier/test_manifest_signature.rb +13 -0
  36. data/test/passifier/test_pass.rb +45 -0
  37. data/test/passifier/test_signing.rb +47 -0
  38. data/test/passifier/test_spec.rb +20 -0
  39. data/test/passifier/test_static_file.rb +25 -0
  40. data/test/passifier/test_storage.rb +157 -0
  41. data/test/passifier/test_url_source.rb +30 -0
  42. data/test/test_passifier.rb +10 -0
  43. metadata +172 -0
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module Passifier
4
+
5
+ class Pass
6
+
7
+ attr_reader :archive,
8
+ :image_files,
9
+ :manifest,
10
+ :serial_number,
11
+ :signature,
12
+ :spec
13
+
14
+ # @param [String] serial_number An ID for this pass, used as the serial number in pass.json
15
+ # @param [Hash] spec_hash The pass's spec (pass.json)
16
+ # @param [Hash] images The pass's image assets
17
+ # ex. { "background.png" => "https://www.google.com/images/srpr/logo3w.png",
18
+ # "thumbnail.png" => "~/thumb.png" }
19
+ # @param [Signing] signing A valid signing
20
+ def initialize(serial_number, spec_hash, images, signing, options = {})
21
+ @signing = signing
22
+ @spec = Spec.new(serial_number, spec_hash)
23
+ @image_files = to_image_files(images)
24
+ @manifest = Manifest.new(@image_files, signing)
25
+ @signature = ManifestSignature.new(@manifest, signing)
26
+ end
27
+
28
+ # File objects that should be included in the archive
29
+ # @return [Array<Spec, Manifest, ManifestSignature, StaticFile, UrlSource>] File objects that will appear in
30
+ # this pass' archive
31
+ def files_for_archive
32
+ [@spec, @manifest, @signature, @image_files].flatten.compact
33
+ end
34
+
35
+ # Create the Archive file for this Pass
36
+ # @param [String] path The desired path of the Archive
37
+ # @return [Archive] The complete stored archive
38
+ def create_archive(path, options = {})
39
+ @archive = Archive.new(path, @spec.serial_number, files_for_archive)
40
+ @archive.store(options)
41
+ @archive
42
+ end
43
+ alias_method :generate, :create_archive
44
+ alias_method :save, :create_archive
45
+
46
+ # Convert a Time object to Apple's preferred String time format
47
+ # @param [Time, Date, DateTime] The time object to convert to a String
48
+ # @return [String] The converted time object in Apple's preferred format
49
+ def self.to_apple_datetime(time_with_zone)
50
+ time_with_zone.strftime("%Y-%m-%dT%H:%M%:z")
51
+ end
52
+
53
+ # Create a Pass and corresponding Archive file
54
+ # @param [String] path The desired path of the Archive
55
+ # @param [String] serial_number An ID for this pass, used as the serial number in pass.json
56
+ # @param [Hash] spec_hash The pass's spec (pass.json)
57
+ # @param [Hash] images The pass's image assets
58
+ # ex. { "background.png" => "https://www.google.com/images/srpr/logo3w.png",
59
+ # "thumbnail.png" => "~/thumb.png" }
60
+ # @param [Signing] signing A valid signing
61
+ # @return [Archive] The complete stored archive
62
+ def self.create_archive(path, serial_number, spec_hash, images, signing, options = {})
63
+ pass = new(serial_number, spec_hash, images, signing, options)
64
+ pass.create_archive(path, options)
65
+ end
66
+
67
+ protected
68
+
69
+ # Convert a list of images to Passifier file objects
70
+ # @param [Hash] images A Hash of filenames and corresponding file paths or urls
71
+ # @return [Array<StaticFile, UrlSource>] The resulting StaticFile and/or UrlSource objects
72
+ def to_image_files(images)
73
+ images.map do |filename, image|
74
+ klass = (image =~ /https?:\/\/[\S]+/) ? UrlSource : StaticFile
75
+ klass.new(filename, image)
76
+ end.flatten.compact
77
+ end
78
+
79
+ end
80
+
81
+ end
82
+
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module Passifier
4
+
5
+ # Class for Pass signing functionality
6
+ class Signing
7
+
8
+ # @param [String] key_pem The key pem file location
9
+ # @param [String] pass_phrase The key pass phrase
10
+ # @param [String] certificate_pem The certificate pem file location
11
+ def initialize(key_pem, pass_phrase, certificate_pem)
12
+ @certificate = File.read(certificate_pem)
13
+ @key = File.read(key_pem)
14
+ @pass_phrase = pass_phrase
15
+ end
16
+
17
+ # Generate a digest of the given content
18
+ # @param [String] content The content to generate a digest from
19
+ # @return [String] The resulting digest
20
+ def sha(content)
21
+ signed_contents = sign(content)
22
+ Digest::SHA1.hexdigest(signed_contents)
23
+ end
24
+
25
+ # Sign the given content
26
+ # @param [String] content The content to generate a signing of
27
+ # @return [String] The resulting signing
28
+ def sign(content)
29
+ key = OpenSSL::PKey::RSA.new(@key, @pass_phrase)
30
+ certificate = OpenSSL::X509::Certificate.new(@certificate)
31
+ OpenSSL::PKCS7.sign(certificate, key, content, nil, OpenSSL::PKCS7::BINARY | OpenSSL::PKCS7::NOATTR | OpenSSL::PKCS7::DETACHED).to_der
32
+ end
33
+
34
+ end
35
+
36
+ end
37
+
38
+
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module Passifier
4
+
5
+ # Pass specification, representing the pass.json file
6
+ class Spec
7
+
8
+ attr_reader :hash, :serial_number
9
+ alias_method :to_hash, :hash
10
+
11
+ # @param [String] serial_number An ID for this Spec, used in the pass.json file as the Serial Number
12
+ # @param [Hash] hash The pass.json contents as a hash
13
+ def initialize(serial_number, hash)
14
+ @serial_number = serial_number
15
+ @hash = hash
16
+ end
17
+
18
+ # The contents of the pass.json file
19
+ # @return [String] The pass.json contents as a String
20
+ def to_json
21
+ to_hash.to_json
22
+ end
23
+ alias_method :content, :to_json
24
+
25
+ def filename
26
+ "pass.json"
27
+ end
28
+
29
+ end
30
+
31
+ end
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module Passifier
4
+
5
+ # A single local static file asset
6
+ class StaticFile
7
+
8
+ attr_reader :content, :name, :path
9
+ alias_method :filename, :name
10
+
11
+ # @param [String, Symbol] name The name of the asset
12
+ # @param [String] path_to_file The local path to the asset file
13
+ def initialize(name, path_to_file)
14
+ @name = name
15
+ @path = path_to_file
16
+ @content = File.open(@path, 'rb') {|file| file.read }
17
+ end
18
+
19
+ end
20
+
21
+ end
22
+
23
+
24
+
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module Passifier
4
+
5
+ # Disk storage for a pass
6
+ class Storage
7
+
8
+ attr_reader :assets, :scratch_directory
9
+
10
+ # @param [String] scratch directory The directory to use for file storage
11
+ # @param [Array<Object>] assets The file assets to store
12
+ def initialize(scratch_directory, assets)
13
+ @assets = [assets].flatten
14
+ @scratch_directory = scratch_directory
15
+ end
16
+
17
+ # Path to the stored version
18
+ # @param [String] filename The filename to return the path for
19
+ # @return [String] The full path to the file with the passed-in filename
20
+ def path(filename)
21
+ "#{@scratch_directory}/#{filename}"
22
+ end
23
+
24
+ # Store the files for a group of pass assets
25
+ # @param [Array<Object>] The pass asset objects to store files for
26
+ def store
27
+ ensure_directory_exists
28
+ @assets.each { |asset| write_file(asset) }
29
+ end
30
+
31
+ # Create a zip archive given a filename for the archive and a set of pass assets
32
+ # @param [String] zip_path The desired output path
33
+ # @return [String] The full path of the resulting zip archive
34
+ def zip(zip_path)
35
+ remove_zip(zip_path) # ensure that older version is deleted if it exists
36
+ Zip::ZipFile.open(zip_path, Zip::ZipFile::CREATE) do |zipfile|
37
+ @assets.each do |asset|
38
+ zipfile.add(asset.filename, path(asset.filename))
39
+ end
40
+ end
41
+ zip_path
42
+ end
43
+
44
+ # Clean up temp files
45
+ def cleanup
46
+ remove_temp_files
47
+ remove_directory
48
+ end
49
+
50
+ # Remove a zip archive
51
+ # @param [String] zip_path The path of the archive to delete
52
+ def remove_zip(zip_path)
53
+ File.delete(zip_path) if File.exists?(zip_path)
54
+ end
55
+
56
+ protected
57
+
58
+ # Store the file for the given pass asset to disk
59
+ # @param [Object] asset Store the file for the given pass asset
60
+ def write_file(asset)
61
+ File.open(path(asset.filename), 'w') do |file|
62
+ file.write(asset.content) if asset.respond_to?(:content)
63
+ end
64
+ end
65
+
66
+ def remove_directory(directory = nil)
67
+ directory ||= @scratch_directory
68
+ Dir.rmdir(directory) if File.exists?(directory) && File.directory?(directory)
69
+ end
70
+
71
+ # Remove assets (created by Passifier::Storage#store)
72
+ def remove_temp_files
73
+ @assets.map do |asset|
74
+ path = path(asset.filename)
75
+ File.delete(path) if File.exists?(path)
76
+ end
77
+ end
78
+
79
+ def ensure_directory_exists(directory = nil)
80
+ directory ||= @scratch_directory
81
+ last_directory = ""
82
+ tree = directory.split("/")
83
+ tree.each do |directory|
84
+ dir = "#{last_directory}#{directory}"
85
+ Dir.mkdir(dir) unless File.exists?(dir) || dir == ""
86
+ last_directory = "#{dir}/"
87
+ end
88
+ last_directory
89
+ end
90
+
91
+ end
92
+
93
+ end
94
+
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module Passifier
4
+
5
+ # Asset derived from a url
6
+ # Used to pull in the background images
7
+ class UrlSource
8
+
9
+ attr_reader :content, :name, :url
10
+ alias_method :filename, :name
11
+
12
+ # @param [String, Symbol] name The name of the asset
13
+ # @param [String] url The url to request the asset content from
14
+ def initialize(name, url)
15
+ @name = name
16
+ @url = url
17
+ populate_content
18
+ end
19
+
20
+ private
21
+
22
+ def populate_content
23
+ uri = URI(@url)
24
+ res = Net::HTTP.get_response(uri)
25
+ @content = res.body
26
+ end
27
+
28
+ end
29
+
30
+ end
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
data/test/helper.rb ADDED
@@ -0,0 +1,159 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ require 'test/unit'
4
+
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
7
+ require 'passifier'
8
+
9
+ class Test::Unit::TestCase
10
+ end
11
+
12
+ # So that as few tests as possible will require valid keys and certificates
13
+ # only the Signing test uses the real goods
14
+ class MockSigning
15
+
16
+ def sha(content)
17
+ "sha rite"
18
+ end
19
+
20
+ def sign(content)
21
+ "****"
22
+ end
23
+
24
+ end
25
+
26
+ module Helper
27
+
28
+ extend self
29
+ include Passifier
30
+
31
+ def serial
32
+ "ARE YOU SERIAL"
33
+ end
34
+
35
+ def spec_hash
36
+ {
37
+ "formatVersion" => 1,
38
+ "passTypeIdentifier" => "pass.example.example",
39
+ "teamIdentifier" => "ATEAMID",
40
+ "relevantDate" => "event time***",
41
+ "organizationName" => "Example Inc.",
42
+ "serialNumber" => serial,
43
+ "description" => "this is a pass",
44
+ "generic" => {
45
+ "headerFields" => [
46
+ {
47
+ "key" => "date",
48
+ "label" => "event time***",
49
+ "value" => "event date***"
50
+ }
51
+ ],
52
+ "primaryFields" => [
53
+ {
54
+ "key" => "title",
55
+ "label" => "",
56
+ "value" => "This is the pass title!"
57
+ }
58
+ ],
59
+ "secondaryFields" => [
60
+ {
61
+ "key" => "host",
62
+ "label" => "Host",
63
+ "value" => "paperlesspost.com",
64
+ "textAlignment" => "PKTextAlignmentRight"
65
+ }
66
+ ]
67
+ }
68
+ }
69
+ end
70
+
71
+ def zip_path
72
+ "test/zip.zip"
73
+ end
74
+
75
+ def scratch_directory
76
+ "test/scratch_directory"
77
+ end
78
+
79
+ def create_archive_through_pass
80
+ Pass.create_archive(zip_path, serial, spec_hash, new_images, MockSigning.new, :scratch_directory => scratch_directory)
81
+ end
82
+
83
+ def new_archive
84
+ Archive.new(zip_path, serial, new_image_files)
85
+ end
86
+
87
+ def new_pass
88
+ Pass.new(serial, spec_hash, new_images, MockSigning.new)
89
+ end
90
+
91
+ def new_spec
92
+ Spec.new(serial, spec_hash)
93
+ end
94
+
95
+ def new_storage
96
+ Storage.new(scratch_directory, new_image_files)
97
+ end
98
+
99
+ def new_manifest
100
+ Manifest.new(new_image_files, MockSigning.new)
101
+ end
102
+
103
+ def new_manifest_signature
104
+ ManifestSignature.new(new_manifest, MockSigning.new)
105
+ end
106
+
107
+ def new_images
108
+ {
109
+ "background.png" => "test/assets/background.png",
110
+ "background@2x.png" => "test/assets/background@2x.png",
111
+ "icon.png" => "test/assets/icon.png",
112
+ "icon@2x.png" => "test/assets/icon@2x.png",
113
+ "logo.png" => "http://blog.paperlesspost.com/wp-content/uploads/2012/04/PP_2012-Logo_Registered-2.jpg",
114
+ "logo@2x.png" => "http://blog.paperlesspost.com/wp-content/uploads/2012/04/PP_2012-Logo_Registered-2.jpg",
115
+ "thumbnail.png" => "test/assets/thumbnail.png",
116
+ "thumbnail@2x.png"=> "test/assets/thumbnail@2x.png"
117
+ }
118
+ end
119
+
120
+ def new_image_files
121
+ [
122
+ StaticFile.new("background.png", "test/assets/background.png"),
123
+ StaticFile.new("background@2x.png", "test/assets/background@2x.png"),
124
+ StaticFile.new("icon.png", "test/assets/icon.png"),
125
+ StaticFile.new("icon@2x.png", "test/assets/icon@2x.png"),
126
+ UrlSource.new("logo.png", "http://blog.paperlesspost.com/wp-content/uploads/2012/04/PP_2012-Logo_Registered-2.jpg"),
127
+ UrlSource.new("logo@2x.png", "http://blog.paperlesspost.com/wp-content/uploads/2012/04/PP_2012-Logo_Registered-2.jpg"),
128
+ StaticFile.new("thumbnail.png", "test/assets/thumbnail.png"),
129
+ StaticFile.new("thumbnail@2x.png", "test/assets/thumbnail@2x.png")
130
+ ]
131
+ end
132
+
133
+ def new_url_source
134
+ Passifier::UrlSource.new("background.png", image_url)
135
+ end
136
+
137
+ def new_static_file
138
+ Passifier::StaticFile.new("background.png", "test/assets/background.png")
139
+ end
140
+
141
+ def image_url
142
+ "http://blog.paperlesspost.com/wp-content/uploads/2012/04/PP_2012-Logo_Registered-2.jpg"
143
+ end
144
+
145
+ def signing_pass_phrase
146
+ File.read(TestSigning::PASS_PHRASE_FILE).strip.lstrip
147
+ end
148
+
149
+ def signing_assets_exist?
150
+ if File.exist?(TestSigning::CERTIFICATE) && File.exist?(TestSigning::KEY) && File.exist?(TestSigning::PASS_PHRASE_FILE)
151
+ true
152
+ else
153
+ warn "** Warning: Skipping Signing tests because .pem files are missing. See test/passifier/test_signing.rb for more info. "
154
+ false
155
+ end
156
+ end
157
+
158
+ end
159
+