passifier 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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
+