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