apple_receipt 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a381d5ac7037e9d434c0e0af717672f32d84f157b1086753aaf93e01951a2b42
4
- data.tar.gz: f2722e6a2b589703f451035b4a34c2aa97aedbaa469e362443448c50d7bfcb3c
3
+ metadata.gz: f3234acf1d422b851c6e997c905ee974df3be03d446431d894498bf1f0e7e61e
4
+ data.tar.gz: db024ff11f3ae9a78a98275c9feb9f7fef8d536949a895866ad760a16d3eb185
5
5
  SHA512:
6
- metadata.gz: e4f99e107fd8623e73bcc07ce0e095f6c1c44c345207355f915d0e49d31957325fb838869b5d4176907e23307901431be6d28e9ead4b7c81fde6003ee6bad39d
7
- data.tar.gz: 5be1092bdcc3577e4d581284d6c998c8293e02aab9632fea8dee360c9c6b442e4bf6bcb7be1852c0a83534c86a97399ffac97797f0e312e4ec6cab0db6f87f57
6
+ metadata.gz: b7fdcd0d79dee2feedc4c95e8aebd124b58790fc0e12c916fe0a2f948db329eb4a859f299f09507019ee49e8f76809f0e630987d17cfac1267989f0765223cee
7
+ data.tar.gz: 551a6353876874102e79864b16dca85487054ed6d975a927b2b9ed69bbe22b5228e8aae820c5da249d8627a470e90694dd5ab7e0020bebc7a80f44525ad520f6
@@ -1,2 +1,6 @@
1
1
  AllCops:
2
2
  TargetRubyVersion: 2.5
3
+
4
+ Metrics/BlockLength:
5
+ Exclude:
6
+ - spec/**/*
@@ -3,3 +3,5 @@ language: ruby
3
3
  rvm:
4
4
  - 2.5.0
5
5
  before_install: gem install bundler -v 1.16.1
6
+ notifications:
7
+ email: false
@@ -0,0 +1,7 @@
1
+ # Apple Receipt changelog
2
+
3
+ ## v0.2.0
4
+
5
+ - Added ISC license [\#5](https://github.com/koenrh/apple_receipt/pull/5)
6
+ - Added missing dependencies ([\#3](https://github.com/koenrh/apple_receipt/pull/3) and [\#4](https://github.com/koenrh/apple_receipt/pull/4))
7
+ - Added support for 'version 2' receipts ([\#1](https://github.com/koenrh/apple_receipt/pull/1))
@@ -23,7 +23,7 @@ include:
23
23
  Examples of unacceptable behavior by participants include:
24
24
 
25
25
  * The use of sexualized language or imagery and unwelcome sexual attention or
26
- advances
26
+ advances
27
27
  * Trolling, insulting/derogatory comments, and personal or political attacks
28
28
  * Public or private harassment
29
29
  * Publishing others' private information, such as a physical or electronic
@@ -67,8 +67,5 @@ members of the project's leadership.
67
67
 
68
68
  ## Attribution
69
69
 
70
- This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
- available at [http://contributor-covenant.org/version/1/4][version]
72
-
73
- [homepage]: http://contributor-covenant.org
74
- [version]: http://contributor-covenant.org/version/1/4/
70
+ This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/),
71
+ [version 1.4](https://www.contributor-covenant.org/version/1/4/code-of-conduct.html).
@@ -1,21 +1,15 @@
1
- The MIT License (MIT)
1
+ ISC License
2
2
 
3
- Copyright (c) 2018 Koen Rouwhorst
3
+ Copyright (c) 2018, Koen Rouwhorst
4
4
 
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
5
+ Permission to use, copy, modify, and/or distribute this software for any purpose
6
+ with or without fee is hereby granted, provided that the above copyright notice
7
+ and this permission notice appear in all copies.
11
8
 
12
- The above copyright notice and this permission notice shall be included in
13
- all copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
- THE SOFTWARE.
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
11
+ FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
13
+ OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
14
+ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
15
+ THIS SOFTWARE.
data/README.md CHANGED
@@ -1,8 +1,15 @@
1
- # AppleReceipt
1
+ # Apple Receipt
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/apple_receipt.svg)](https://badge.fury.io/rb/apple_receipt)
3
4
  [![Build Status](https://travis-ci.org/koenrh/apple_receipt.svg?branch=master)](https://travis-ci.org/koenrh/apple_receipt)
5
+ [![Dependency Status](https://beta.gemnasium.com/badges/github.com/koenrh/apple_receipt.svg)](https://beta.gemnasium.com/projects/github.com/koenrh/apple_receipt)
4
6
 
5
- This gem allows you to to locally/cryptographically verify Apple receipts.
7
+ This gem allows you to read and verify Apple receipts. It was originally built
8
+ to locally (server-side) verify the validity of receipts that are embedded in
9
+ [Status Update Notifications](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/StoreKitGuide/Chapters/Subscriptions.html#//apple_ref/doc/uid/TP40008267-CH7-SW13).
10
+ These receipts have a different format than [documented](https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateLocally.html#//apple_ref/doc/uid/TP40010573-CH1-SW2)
11
+ App Store receipts you might be familiar with, which are [PKCS #7](https://tools.ietf.org/html/rfc2315)
12
+ containers with a payload (receipt data) encoded using [ASN.1](https://www.itu.int/itu-t/recommendations/rec.aspx?rec=X.690).
6
13
 
7
14
  ## Installation
8
15
 
@@ -25,39 +32,74 @@ Or install it yourself as:
25
32
  ```ruby
26
33
  require 'apple_receipt'
27
34
 
28
- # check validity (certificate chain, and signature)
35
+ # Check receipt's validity (certificate chain, and signature)
29
36
  receipt_raw = File.read('./receipt.txt')
30
37
  receipt = AppleReceipt::Receipt.new(receipt_raw)
31
38
  receipt.valid?
32
39
  # => true
33
40
 
34
- # read purchase info
41
+ # Read receipt's data (data in example shortened for brevity)
35
42
  receipt.purchase_info
36
- # => {"original-purchase-date-pst"=>"2017-12-23 09:03:53 America/Los_Angeles",
37
- # "quantity"=>"1",
38
- # "unique-vendor-identifier"=>"D895D8DB-AEDF-4530-B7E5-E0C9A9A394B6",
39
- # "original-purchase-date-ms"=>"1514048633000",
40
- # "expires-date-formatted"=>"2018-01-23 17:03:44 Etc/GMT",
41
- # "is-in-intro-offer-period"=>"false",
42
- # "purchase-date-ms"=>"1514048624000",
43
- # "expires-date-formatted-pst"=>"2018-01-23 09:03:44 America/Los_Angeles",
44
- # "is-trial-period"=>"false",
45
- # "item-id"=>"1190360447",
46
- # "unique-identifier"=>"fed543dc24065fa2ab23ef08b0b44c0a0c9ed375",
47
- # "original-transaction-id"=>"160000408504141",
48
- # "expires-date"=>"1516727024000",
49
- # "app-item-id"=>"947936149",
50
- # "transaction-id"=>"160000408504141",
51
- # "bvrs"=>"7000",
52
- # "web-order-line-item-id"=>"160000091314729",
53
- # "version-external-identifier"=>"825366855",
54
- # "bid"=>"com.foo.bar",
55
- # "product-id"=>"com.foo.bar.monthly",
56
- # "purchase-date"=>"2017-12-23 17:03:44 Etc/GMT",
57
- # "purchase-date-pst"=>"2017-12-23 09:03:44 America/Los_Angeles",
58
- # "original-purchase-date"=>"2017-12-23 17:03:53 Etc/GMT"}
43
+ # => {
44
+ # "quantity"=>"1",
45
+ # "expires-date-formatted"=>"2018-01-23 17:03:44 Etc/GMT",
46
+ # "is-in-intro-offer-period"=>"false",
47
+ # "is-trial-period"=>"false",
48
+ # "item-id"=>"1190360447",
49
+ # "app-item-id"=>"947936149",
50
+ # "transaction-id"=>"160000408504141",
51
+ # "web-order-line-item-id"=>"160000011000001",
52
+ # "bid"=>"com.foo.bar",
53
+ # "product-id"=>"com.foo.bar.monthly",
54
+ # "purchase-date"=>"2017-12-23 17:03:44 Etc/GMT",
55
+ # "original-purchase-date"=>"2017-12-23 17:03:53 Etc/GMT"
56
+ # }
59
57
  ```
60
58
 
59
+ ## Apple receipts
60
+
61
+ A receipt is encoded as base64, and is formatted as a [NeXTSTEP](https://en.wikipedia.org/wiki/Property_list#NeXTSTEP)
62
+ dictionary:
63
+
64
+ ```
65
+ {
66
+ "signature" = "[base64-encoded signature]";
67
+ "purchase-info" = "[base64-encoded purchase data]";
68
+ "pod" = "[integer]";
69
+ "signing-status" = "0";
70
+ }
71
+ ```
72
+
73
+ ### Signature
74
+
75
+ The `signature` entry contains base64-encoded binary data, which has the following
76
+ layout:
77
+
78
+ - **1 byte** - Receipt version (e.g. version 3).
79
+ - **128 bytes** (version 2) or **256 bytes** (version 3) - Signature.
80
+ - **4 bytes** - Length (in number of bytes) of the certificate.
81
+ - **N bytes** - DER-encoded certificate.
82
+
83
+ The version 2 and 3 receipt certificates are signed, respectively, by:
84
+
85
+ - **Apple iTunes Store Certification Authority** (version 2)
86
+ - Serial: 26 (`0x1a`)
87
+ - Subject: `C=US, O=Apple Inc., OU=Apple Certification Authority, CN=Apple iTunes Store Certification Authority`
88
+ - **Apple Worldwide Developer Relations Certification Authority** (version 3)
89
+ - Serial: 134752589830791184 (`0x1debcc4396da010`)
90
+ - Subject: `C=US, O=Apple Inc., OU=Apple Worldwide Developer Relations, CN=Apple Worldwide Developer Relations Certification Authority`
91
+
92
+ Both certificates chain up to:
93
+
94
+ - **Apple Root CA**
95
+ - Serial: 2 (`0x2`)
96
+ - Subject: `C=US, O=Apple Inc., OU=Apple Certification Authority, CN=Apple Root CA`
97
+
98
+ ### Purchase info
99
+
100
+ The `purchase-info` entry contains a base64-encoded NeXTSTEP dictionary that contains
101
+ the actual receipt data (purchase info).
102
+
61
103
  ## Contributing
62
104
 
63
105
  Bug reports and pull requests are welcome on [GitHub](https://github.com/koenrh/apple_receipt).
@@ -67,4 +109,4 @@ code of conduct.
67
109
 
68
110
  ## License
69
111
 
70
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
112
+ The gem is available as open source under the terms of the [ISC License](https://opensource.org/licenses/ISC).
@@ -5,6 +5,7 @@ lib = File.expand_path('../lib', __FILE__)
5
5
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
6
  require 'apple_receipt/version'
7
7
 
8
+ # rubocop:disable Metrics/BlockLength
8
9
  Gem::Specification.new do |spec|
9
10
  spec.name = 'apple_receipt'
10
11
  spec.version = AppleReceipt::VERSION
@@ -30,8 +31,13 @@ Gem::Specification.new do |spec|
30
31
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
31
32
  spec.require_paths = ['lib']
32
33
 
34
+ spec.add_dependency 'json', '~> 2.0'
35
+ spec.add_dependency 'openssl', '~> 2.0'
36
+
33
37
  spec.add_development_dependency 'bundler', '~> 1.16'
34
38
  spec.add_development_dependency 'minitest', '~> 5.0'
35
- spec.add_development_dependency 'rake', '~> 10.0'
39
+ spec.add_development_dependency 'mocha', '~> 1.3'
40
+ spec.add_development_dependency 'rake', '~> 12.0'
36
41
  spec.add_development_dependency 'rubocop', '~> 0.50'
37
42
  end
43
+ # rubocop:enable Metrics/BlockLength
@@ -11,17 +11,20 @@ module AppleReceipt
11
11
  class Receipt
12
12
  def initialize(raw_receipt)
13
13
  receipt_decoded = Base64.decode64(raw_receipt)
14
- @version, @signature, @cert, @data = ReceiptParser.parse(receipt_decoded)
14
+ @version,
15
+ @signature,
16
+ @certificate,
17
+ @data = ReceiptParser.parse(receipt_decoded)
15
18
  end
16
19
 
17
20
  def purchase_info
18
- NextStepParser.parse(data)
21
+ @purchase_info ||= NextStepParser.parse(data)
19
22
  end
20
23
 
21
24
  def valid?
22
25
  Validator.new(self).valid?
23
26
  end
24
27
 
25
- attr_reader :version, :signature, :cert, :data
28
+ attr_reader :version, :signature, :certificate, :data
26
29
  end
27
30
  end
@@ -3,6 +3,7 @@
3
3
  require 'base64'
4
4
  require 'openssl'
5
5
  require 'json'
6
+ require 'set'
6
7
 
7
8
  require 'apple_receipt/next_step_parser'
8
9
 
@@ -11,24 +12,38 @@ module AppleReceipt
11
12
  module ReceiptParser
12
13
  module_function
13
14
 
14
- def bla(input)
15
+ SIGNATURE_LENGTH_MAPPING = {
16
+ 2 => 128,
17
+ 3 => 256
18
+ }.freeze
19
+
20
+ def parse(input)
15
21
  receipt_hash = NextStepParser.parse(input)
22
+
23
+ unless Set['signature', 'purchase-info'].subset?(receipt_hash.keys.to_set)
24
+ raise ArgumentError, 'Missing required fields'
25
+ end
26
+
16
27
  signature_decoded = Base64.decode64(receipt_hash['signature'])
17
- data = Base64.decode64(receipt_hash['purchase-info'])
28
+ data_decoded = Base64.decode64(receipt_hash['purchase-info'])
18
29
 
19
- sig = StringIO.new(signature_decoded)
20
- [sig, data]
30
+ version, signature, receipt_cert = read_signature(signature_decoded)
31
+ [version, signature, receipt_cert, data_decoded]
21
32
  end
22
33
 
23
- def parse(input)
24
- sig, data = bla(input)
25
-
34
+ def read_signature(signature_decoded)
35
+ sig = StringIO.new(signature_decoded)
26
36
  version = sig.read(1).unpack('C').first # 8-bit unsigned (unsigned char)
27
- signature = sig.read(256)
37
+
38
+ unless SIGNATURE_LENGTH_MAPPING.keys.include?(version)
39
+ raise ArgumentError, "Unsupported receipt version: #{version}"
40
+ end
41
+
42
+ signature = sig.read(SIGNATURE_LENGTH_MAPPING[version])
28
43
  cert_size = sig.read(4).unpack('L>')[0] # 32-bit unsigned, big-endian
29
44
  receipt_cert = OpenSSL::X509::Certificate.new(sig.read(cert_size))
30
45
 
31
- [version, signature, receipt_cert, data]
46
+ [version, signature, receipt_cert]
32
47
  end
33
48
  end
34
49
  end
@@ -5,27 +5,44 @@ require 'openssl'
5
5
  module AppleReceipt
6
6
  # Validator allows one to check the validity of a receipt.
7
7
  class Validator
8
- def initialize(receipt)
9
- root_cert_pem = File.read('./certificates/AppleIncRootCertificate.cer')
10
- root_cert = OpenSSL::X509::Certificate.new(root_cert_pem)
8
+ INTERMEDIATE_CERT_MAPPING = {
9
+ 3 => 'AppleWorldwideDeveloperRelationsCertificationAuthority',
10
+ 2 => 'AppleITunesStoreCertificationAuthority'
11
+ }.freeze
11
12
 
12
- intermediate_cert_pem = File.read('./certificates/AppleWWDRCA.cer')
13
- intermediate_cert = OpenSSL::X509::Certificate.new(intermediate_cert_pem)
13
+ def initialize(receipt, certificates: [])
14
+ populate_certificate_store(receipt.version, certificates)
15
+ @receipt = receipt
16
+ end
17
+
18
+ def populate_certificate_store(version, provided_certificates)
19
+ if provided_certificates.any?
20
+ add_certificates(provided_certificates)
21
+ else
22
+ add_named_certificate('AppleRootCA')
23
+ add_named_certificate(INTERMEDIATE_CERT_MAPPING[version])
24
+ end
25
+ end
14
26
 
15
- store.add_cert(root_cert)
16
- store.add_cert(intermediate_cert)
27
+ def add_named_certificate(name)
28
+ cert_file = File.read("./certificates/#{name}.cer")
29
+ store.add_cert(OpenSSL::X509::Certificate.new(cert_file))
30
+ end
17
31
 
18
- @receipt = receipt
32
+ def add_certificates(certificates)
33
+ certificates.each do |cert|
34
+ store.add_cert(cert)
35
+ end
19
36
  end
20
37
 
21
38
  def valid?
22
- store.verify(receipt.cert) &&
39
+ store.verify(receipt.certificate) &&
23
40
  public_key.verify(OpenSSL::Digest::SHA1.new,
24
41
  receipt.signature, signed_data)
25
42
  end
26
43
 
27
44
  def public_key
28
- receipt.cert.public_key
45
+ receipt.certificate.public_key
29
46
  end
30
47
 
31
48
  def signed_data
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AppleReceipt
4
- VERSION = '0.1.1'
4
+ VERSION = '0.2.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: apple_receipt
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Koen Rouwhorst
@@ -10,6 +10,34 @@ bindir: exe
10
10
  cert_chain: []
11
11
  date: 2018-01-27 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: json
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: openssl
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.0'
13
41
  - !ruby/object:Gem::Dependency
14
42
  name: bundler
15
43
  requirement: !ruby/object:Gem::Requirement
@@ -38,20 +66,34 @@ dependencies:
38
66
  - - "~>"
39
67
  - !ruby/object:Gem::Version
40
68
  version: '5.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: mocha
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.3'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.3'
41
83
  - !ruby/object:Gem::Dependency
42
84
  name: rake
43
85
  requirement: !ruby/object:Gem::Requirement
44
86
  requirements:
45
87
  - - "~>"
46
88
  - !ruby/object:Gem::Version
47
- version: '10.0'
89
+ version: '12.0'
48
90
  type: :development
49
91
  prerelease: false
50
92
  version_requirements: !ruby/object:Gem::Requirement
51
93
  requirements:
52
94
  - - "~>"
53
95
  - !ruby/object:Gem::Version
54
- version: '10.0'
96
+ version: '12.0'
55
97
  - !ruby/object:Gem::Dependency
56
98
  name: rubocop
57
99
  requirement: !ruby/object:Gem::Requirement
@@ -76,16 +118,16 @@ files:
76
118
  - ".gitignore"
77
119
  - ".rubocop.yml"
78
120
  - ".travis.yml"
121
+ - CHANGELOG.md
79
122
  - CODE_OF_CONDUCT.md
80
123
  - Gemfile
81
124
  - LICENSE.txt
82
125
  - README.md
83
126
  - Rakefile
84
127
  - apple_receipt.gemspec
85
- - bin/console
86
- - bin/setup
87
- - certificates/AppleIncRootCertificate.cer
88
- - certificates/AppleWWDRCA.cer
128
+ - certificates/AppleITunesStoreCertificationAuthority.cer
129
+ - certificates/AppleRootCA.cer
130
+ - certificates/AppleWorldwideDeveloperRelationsCertificationAuthority.cer
89
131
  - lib/apple_receipt.rb
90
132
  - lib/apple_receipt/next_step_parser.rb
91
133
  - lib/apple_receipt/receipt.rb
@@ -1,15 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- require 'bundler/setup'
5
- require 'apple_receipt'
6
-
7
- # You can add fixtures and/or initialization code here to make experimenting
8
- # with your gem easier. You can also use a different console, if you like.
9
-
10
- # (If you use this, don't forget to add pry to your Gemfile!)
11
- # require "pry"
12
- # Pry.start
13
-
14
- require 'irb'
15
- IRB.start(__FILE__)
data/bin/setup DELETED
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
- IFS=$'\n\t'
4
- set -vx
5
-
6
- bundle install
7
-
8
- # Do any other automated setup that you need to do here