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.
- data/Gemfile +15 -0
- data/LICENSE.md +22 -0
- data/README.md +125 -0
- data/Rakefile +48 -0
- data/examples/assets/background.png +0 -0
- data/examples/assets/background@2x.png +0 -0
- data/examples/assets/icon.png +0 -0
- data/examples/assets/icon@2x.png +0 -0
- data/examples/assets/logo.png +0 -0
- data/examples/assets/logo@2x.png +0 -0
- data/examples/assets/thumbnail.png +0 -0
- data/examples/assets/thumbnail@2x.png +0 -0
- data/examples/simple.rb +87 -0
- data/lib/passifier.rb +26 -0
- data/lib/passifier/archive.rb +52 -0
- data/lib/passifier/manifest.rb +37 -0
- data/lib/passifier/manifest_signature.rb +33 -0
- data/lib/passifier/pass.rb +82 -0
- data/lib/passifier/signing.rb +38 -0
- data/lib/passifier/spec.rb +31 -0
- data/lib/passifier/static_file.rb +24 -0
- data/lib/passifier/storage.rb +94 -0
- data/lib/passifier/url_source.rb +30 -0
- data/test/assets/background.png +0 -0
- data/test/assets/background@2x.png +0 -0
- data/test/assets/icon.png +0 -0
- data/test/assets/icon@2x.png +0 -0
- data/test/assets/logo.png +0 -0
- data/test/assets/logo@2x.png +0 -0
- data/test/assets/thumbnail.png +0 -0
- data/test/assets/thumbnail@2x.png +0 -0
- data/test/helper.rb +159 -0
- data/test/passifier/test_archive.rb +44 -0
- data/test/passifier/test_manifest.rb +27 -0
- data/test/passifier/test_manifest_signature.rb +13 -0
- data/test/passifier/test_pass.rb +45 -0
- data/test/passifier/test_signing.rb +47 -0
- data/test/passifier/test_spec.rb +20 -0
- data/test/passifier/test_static_file.rb +25 -0
- data/test/passifier/test_storage.rb +157 -0
- data/test/passifier/test_url_source.rb +30 -0
- data/test/test_passifier.rb +10 -0
- 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
|
+
|