telesign_lib 0.0.12

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7d531bc3040fa7c7d87a0d1c24c8e77cedf6c7bb
4
+ data.tar.gz: 2f0da94e24422ebaf88cf844bba37a842ba84810
5
+ SHA512:
6
+ metadata.gz: cb9380ab2921e864af25747cb6ac777bf7c23ba98308b4757a2eaebd98b7cc12fd307c32b2ad1a02fb935fb69d22c89e844d349c60e6d34e207a54c7406e5c41
7
+ data.tar.gz: 5109a12a692dbb580ca62eaaf1b8fa2d5c38fb9580ab9db76bbb53520945f9c8c2478fc2d5d3e873576ef6702a9a31f2aca28e9079a304b731fa7520e1a71b38
data/.gitignore ADDED
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ coverage
6
+ InstalledFiles
7
+ lib/bundler/man
8
+ pkg
9
+ rdoc
10
+ spec/reports
11
+ test/tmp
12
+ test/version_tmp
13
+ tmp
14
+
15
+ # YARD artifacts
16
+ .yardoc
17
+ _yardoc
18
+ doc/
19
+ .ruby-version
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in telesign.gemspec
4
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,65 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ telesign_lib (0.0.12)
5
+ faraday (~> 0.9.1)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ addressable (2.3.7)
11
+ coderay (1.1.0)
12
+ crack (0.4.2)
13
+ safe_yaml (~> 1.0.0)
14
+ faraday (0.9.1)
15
+ multipart-post (>= 1.2, < 3)
16
+ ffi (1.9.8-java)
17
+ json (1.8.1)
18
+ json (1.8.1-java)
19
+ method_source (0.8.2)
20
+ mimic (0.4.3)
21
+ json
22
+ plist
23
+ rack
24
+ sinatra
25
+ minitest (5.5.1)
26
+ multipart-post (2.0.0)
27
+ plist (3.1.0)
28
+ pry (0.10.1)
29
+ coderay (~> 1.1.0)
30
+ method_source (~> 0.8.1)
31
+ slop (~> 3.4)
32
+ pry (0.10.1-java)
33
+ coderay (~> 1.1.0)
34
+ method_source (~> 0.8.1)
35
+ slop (~> 3.4)
36
+ spoon (~> 0.0)
37
+ rack (1.5.2)
38
+ rack-protection (1.5.2)
39
+ rack
40
+ rake (10.4.2)
41
+ safe_yaml (1.0.4)
42
+ sinatra (1.4.4)
43
+ rack (~> 1.4)
44
+ rack-protection (~> 1.4)
45
+ tilt (~> 1.3, >= 1.3.4)
46
+ slop (3.6.0)
47
+ spoon (0.0.4)
48
+ ffi
49
+ tilt (1.4.1)
50
+ webmock (1.20.4)
51
+ addressable (>= 2.3.6)
52
+ crack (>= 0.3.2)
53
+
54
+ PLATFORMS
55
+ java
56
+ ruby
57
+
58
+ DEPENDENCIES
59
+ bundler (~> 1.3)
60
+ mimic (~> 0.4.3)
61
+ minitest (~> 5.5.1)
62
+ pry (~> 0.10.1)
63
+ rake (~> 10.4.2)
64
+ telesign_lib!
65
+ webmock (~> 1.20.4)
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 TeleSign Corp.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ 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, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,104 @@
1
+ telesign
2
+ =============
3
+
4
+ Ruby TeleSign SDK
5
+
6
+ example
7
+ =============
8
+ ```ruby
9
+ require 'telesign'
10
+
11
+ customer_id = 'FFFFFFFF-EEEE-DDDD-1234-AB1234567890'
12
+ secret_key = 'EXAMPLE----TE8sTgg45yusumoN6BYsBVkh+yRJ5czgsnCehZaOYldPJdmFh6NeX8kunZ2zU1YWaUw/0wV6xfw=='
13
+ phone_number = '4445551212'
14
+
15
+ require 'TeleSign'
16
+
17
+ ta = TeleSign::Api.new customer_id: customer_id,
18
+ secret_key: secret_key
19
+
20
+ phone_info = ta.phone_id.standard phone_number
21
+
22
+ p "##################"
23
+ p phone_info.data
24
+ p phone_info.headers
25
+ p phone_info.status
26
+ p phone_info.raw_data
27
+ p "##################"
28
+
29
+ phone_info = ta.phone_id.contact phone_number, 'PWRT'
30
+
31
+ p "##################"
32
+ p phone_info.data
33
+ p phone_info.headers
34
+ p phone_info.status
35
+ p phone_info.raw_data
36
+ p "##################"
37
+
38
+ phone_info = ta.phone_id.score phone_number, 'PWRT'
39
+
40
+ p "##################"
41
+ p phone_info.data
42
+ p phone_info.headers
43
+ p phone_info.status
44
+ p phone_info.raw_data
45
+ p "##################"
46
+
47
+ begin
48
+ phone_info = ta.phone_id.live phone_number, 'RXPF'
49
+ rescue TeleSign::AuthorizationError => e
50
+ puts e.message
51
+ end
52
+
53
+ p "##################"
54
+ p phone_info.data
55
+ p phone_info.headers
56
+ p phone_info.status
57
+ p phone_info.raw_data
58
+ p "##################"
59
+
60
+ phone_info = ta.verify.sms phone_number: phone_number #, verify_code: '12345'
61
+
62
+ p "##################"
63
+ p phone_info.data
64
+ p phone_info.headers
65
+ p phone_info.status
66
+ p phone_info.raw_data
67
+ p phone_info.verify_code
68
+ p "##################"
69
+
70
+ status_info = ta.verify.status phone_info.data['reference_id'], phone_info.verify_code
71
+ # status_info = ta.verify.status phone_info.data['reference_id'], '12345'
72
+
73
+ p "\n\n\n"
74
+ p "##################"
75
+ p status_info.data
76
+ p status_info.headers
77
+ p status_info.status
78
+ p status_info.raw_data
79
+ p status_info.verify_code
80
+ p "##################"
81
+ ```
82
+
83
+ stubs mode
84
+ =============
85
+ If you're running this in a Rails app, there is a stubs mode Sinatra app available.
86
+
87
+ Enable it by setting ENV['TELESIGN_STUBBED'] to something truthy.
88
+
89
+ The Sinatra app will run on "http://localhost:11989".
90
+
91
+ Currently stubs mode supports:
92
+
93
+ post '/v1/verify/sms'
94
+ post '/v1/verify/call'
95
+
96
+ Both of which always return a success response.
97
+
98
+ get '/v1/verify/:reference_id'
99
+
100
+ Returns VALID for any verify_code which does not contain '666'.
101
+
102
+ get '/v1/phoneid/standard/:phone_number'
103
+
104
+ See codes for triggering different phone-type responses.
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require 'bundler/gem_tasks'
2
+
3
+ require 'rake/testtask'
4
+
5
+ Rake::TestTask.new(:test) do |t|
6
+ t.libs << 'test'
7
+ t.pattern = "test/**/*_test.rb"
8
+ end
9
+
10
+ task default: :test
@@ -0,0 +1,52 @@
1
+ require 'faraday'
2
+ require 'json'
3
+
4
+ module TeleSign
5
+ class Api
6
+ # * - Attributes
7
+ # -
8
+ # * - `customer_id`
9
+ # - A string value that identifies your TeleSign account.
10
+ # * - `secret_key`
11
+ # - A base64-encoded string value that validates your access to the TeleSign web services.
12
+ # * - `ssl`
13
+ # - Specifies whether to use a secure connection with the TeleSign server. Defaults to *true*.
14
+ # * - `api_host`
15
+ # - The Internet host used in the base URI for REST web services.
16
+ # The default is *rest.telesign.com* (and the base URI is https://rest.telesign.com/).
17
+ # * - `proxy_host`
18
+ # - The host and port when going through a proxy server. ex: "localhost:8080. The default to no proxy.
19
+
20
+ # NOTE
21
+ # You can obtain both your Customer ID and Secret Key from the
22
+ # TeleSign Customer Portal <https://portal.telesign.com/account_profile_api_auth.php>
23
+
24
+ attr_accessor :verify, :phone_id
25
+
26
+ def initialize opts = {}
27
+ @customer_id = opts[:customer_id]
28
+ @secret_key = opts[:secret_key]
29
+ api_host = opts[:api_host] || 'rest.telesign.com'
30
+ ssl = opts[:ssl].nil? ? true : opts[:ssl]
31
+ proxy_host = opts[:proxy_host] || nil
32
+
33
+ http_root = ssl ? 'https' : 'http'
34
+ proxy = proxy_host ? "#{http_root}://#{proxy_host}" : nil
35
+ url = "#{http_root}://#{api_host}"
36
+
37
+ @conn = Faraday.new(url: url) do |faraday|
38
+ faraday.request :url_encoded
39
+ if defined? Rails
40
+ faraday.response :logger, Rails.logger
41
+ else
42
+ faraday.response :logger # log requests to STDOUT
43
+ end
44
+ faraday.adapter Faraday.default_adapter # make requests with Net::HTTP
45
+ end
46
+
47
+ @verify = Verify.new(conn: @conn, customer_id: opts[:customer_id], secret_key: opts[:secret_key])
48
+ @phone_id = PhoneId.new(conn: @conn, customer_id: opts[:customer_id], secret_key: opts[:secret_key])
49
+ end
50
+
51
+ end
52
+ end
@@ -0,0 +1,67 @@
1
+ # Authorization header definitions
2
+
3
+ require 'securerandom'
4
+ require 'base64'
5
+ require 'uri'
6
+ require 'openssl'
7
+
8
+ # NOTE: the following is the TeleSign crew who made the python version
9
+ # from which this codes was inspired
10
+
11
+ # __author__ = "Jeremy Cunningham, Michael Fox, and Radu Maierean"
12
+ # __copyright__ = "Copyright 2012, TeleSign Corp."
13
+ # __credits__ = ["Jeremy Cunningham", "Radu Maierean", "Michael Fox", "Nancy Vitug", "Humberto Morales"]
14
+ # __license__ = "MIT"
15
+
16
+ AUTH_METHOD = {
17
+ sha1: {hash: OpenSSL::Digest::SHA1, name: 'HMAC-SHA1'},
18
+ sha256: {hash: OpenSSL::Digest::SHA256, name: 'HMAC-SHA256'}
19
+ }
20
+
21
+ module TeleSign
22
+ class Auth
23
+ def self.generate_auth_headers(opts = {})
24
+ content_type = opts[:content_type] ? opts[:content_type] : ''
25
+ auth_method = opts[:auth_method] ? opts[:auth_method] : :sha1
26
+ fields = opts[:fields] ? opts[:fields] : nil
27
+
28
+ customer_id = opts[:customer_id]
29
+ secret_key = opts[:secret_key]
30
+ resource = opts[:resource]
31
+ method = opts[:method]
32
+
33
+ current_date = Time.now.strftime("%a, %d %b %Y %H:%M:%S %z")
34
+ nonce = SecureRandom.uuid
35
+
36
+ if %w(POST PUT).include? method
37
+ content_type = "application/x-www-form-urlencoded"
38
+ end
39
+
40
+ string_to_sign = "%s\n%s\n\nx-ts-auth-method:%s\nx-ts-date:%s\nx-ts-nonce:%s" % [
41
+ method,
42
+ content_type,
43
+ AUTH_METHOD[auth_method][:name],
44
+ current_date,
45
+ nonce]
46
+
47
+ if fields
48
+ string_to_sign += "\n%s" % URI.encode_www_form(fields)
49
+ end
50
+
51
+ string_to_sign += "\n%s" % resource
52
+
53
+ digest = AUTH_METHOD[auth_method][:hash].new
54
+ signer = OpenSSL::HMAC.digest digest, Base64.decode64(secret_key), string_to_sign
55
+
56
+ signature = Base64.encode64 signer
57
+
58
+ {
59
+ 'Authorization' => "TSA %s:%s" % [customer_id, signature],
60
+ 'x-ts-date' => current_date,
61
+ 'x-ts-auth-method' => AUTH_METHOD[auth_method][:name],
62
+ 'x-ts-nonce' => nonce
63
+ }
64
+ end
65
+ end
66
+ end
67
+
@@ -0,0 +1,19 @@
1
+ module TeleSign
2
+ class AuthorizationError < TeleSignError
3
+ # """
4
+ # Either the client failed to authenticate with the REST API server, or the service cannot be executed using the specified credentials.
5
+
6
+ # * - Attributes
7
+ # -
8
+ # * - `data`
9
+ # - The data returned by the service, in a dictionary form.
10
+ # * - `http_response`
11
+ # - The full HTTP Response object, including the HTTP status code, headers, and raw returned data.
12
+
13
+ # """
14
+
15
+ def initialize errors, http_response
16
+ super
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,22 @@
1
+ module TeleSign
2
+ module Helpers
3
+ def random_with_N_digits n
4
+ range_start = 10 ** (n - 1)
5
+ range_end = (10 ** n) - 1
6
+ Random.new.rand(range_start...range_end)
7
+ end
8
+
9
+ def validate_response response
10
+ resp_obj = JSON.load response.body
11
+ if response.status != 200
12
+ if response.status == 401
13
+ raise AuthorizationError.new resp_obj, response
14
+ else
15
+ raise TeleSignError.new resp_obj, response
16
+ end
17
+ end
18
+
19
+ resp_obj
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,215 @@
1
+ if defined? Rails
2
+ Rails.logger.info '***************************'
3
+ Rails.logger.info 'Starting fake telesign server'
4
+ Rails.logger.info '***************************'
5
+ end
6
+
7
+ require 'mimic'
8
+ require 'json'
9
+ require 'securerandom'
10
+
11
+ Mimic.mimic port: (ENV['TELESIGN_PORT'].to_i || 11989), host: (ENV['TELESIGN_URL'] || 'localhost'), fork: false do
12
+ # 'https://rest.telesign.com/v1/verify/sms'
13
+ # params['phone_number']
14
+ # params['language']
15
+ # params['verify_code']
16
+ # params['template']
17
+ post '/v1/verify/sms' do
18
+ http_status = 200
19
+ headers = StdHelpers.standard_headers
20
+
21
+ response_body = StdHelpers.std_response_body StdHelpers.std_reference_id
22
+
23
+ content_type :json
24
+ [http_status, headers, response_body.to_json]
25
+ end
26
+
27
+ # 'https://rest.telesign.com/v1/verify/call'
28
+ # params['phone_number']
29
+ # params['language']
30
+ # params['verify_code']
31
+ post '/v1/verify/call' do
32
+ http_status = 200
33
+ headers = StdHelpers.standard_headers
34
+ response_body = StdHelpers.std_response_body StdHelpers.std_reference_id
35
+
36
+ content_type :json
37
+ [http_status, headers, response_body.to_json]
38
+ end
39
+
40
+ # 'https://rest.telesign.com/v1/verify/%s' % @expected_ref_id
41
+ # params[:reference_id]
42
+ # params[:verify_code]
43
+ get '/v1/verify/:reference_id' do
44
+ http_status = 200
45
+ headers = StdHelpers.standard_headers
46
+
47
+ status_code = if params[:verify_code] =~ /666/
48
+ 'INVALID'
49
+ else
50
+ 'VALID'
51
+ end
52
+
53
+ response_body = StdHelpers.std_response_body params[:reference_id], status_code
54
+
55
+ content_type :json
56
+ [http_status, headers, response_body.to_json]
57
+ end
58
+
59
+ # not using phoneid calls yet, but here are the stubs
60
+ # # 'https://rest.telesign.com/v1/phoneid/%s/%s'
61
+
62
+ # '/v1/phoneid/standard/%s' % phone_number
63
+ get '/v1/phoneid/standard/:phone_number' do
64
+ http_status = 200
65
+ headers = StdHelpers.standard_headers
66
+
67
+ phone_type_hash = if params[:phone_number] =~ /111/
68
+ {'code' =>'1', 'description' =>'FIXED'}
69
+ elsif params[:phone_number] =~ /222/
70
+ {'code' =>'2', 'description' =>'MOBILE'}
71
+ elsif params[:phone_number] =~ /333/
72
+ {'code' =>'3', 'description' =>'PREPAID'}
73
+ elsif params[:phone_number] =~ /444/
74
+ {'code' =>'4', 'description' =>'TOLLFREE'}
75
+ elsif params[:phone_number] =~ /555/ # this could be problematic
76
+ {'code' =>'5', 'description' =>'VOIP'}
77
+ elsif params[:phone_number] =~ /666/
78
+ {'code' =>'6', 'description' =>'PAGER'}
79
+ elsif params[:phone_number] =~ /777/
80
+ {'code' =>'7', 'description' =>'PAYPHONE'}
81
+ elsif params[:phone_number] =~ /888/
82
+ {'code' =>'8', 'description' =>'INVALID'}
83
+ elsif params[:phone_number] =~ /999/
84
+ {'code' =>'9', 'description' =>'RESTRICTED'}
85
+ elsif params[:phone_number] =~ /101010/
86
+ {'code' =>'10', 'description' =>'PERSONAL'}
87
+ elsif params[:phone_number] =~ /110110/
88
+ {'code' =>'11', 'description' =>'VOICEMAIL'}
89
+ else
90
+ {'code' =>'20', 'description' =>'OTHER'}
91
+ end
92
+
93
+ response_body = StdHelpers.std_standard_body params[:phone_number], phone_type_hash
94
+
95
+ content_type :json
96
+ [http_status, headers, response_body.to_json]
97
+ end
98
+
99
+ # # /v1/phoneid/score/%s' % phone_number
100
+ # get '/v1/phoneid/score/:phone_number' do
101
+
102
+ # end
103
+
104
+ # # /v1/phoneid/contact/%s' % phone_number
105
+ # get '/v1/phoneid/contact/:phone_number' do
106
+
107
+ # end
108
+
109
+ # # /v1/phoneid/live/%s' % phone_number
110
+ # get '/v1/phoneid/live/:phone_number' do
111
+
112
+ # end
113
+ end
114
+
115
+ BEGIN {
116
+ class StdHelpers
117
+ class << self
118
+ def standard_headers
119
+ # scraped from TeleSign api response
120
+ { 'date' =>Time.now.strftime('%a, %d %b %Y %H:%M:%S %z'),
121
+ 'server' =>'Apache',
122
+ 'allow' =>'GET,POST,HEAD',
123
+ 'connection' =>'close',
124
+ 'content-type' =>'application/json'}
125
+ end
126
+
127
+ def std_response_body reference_id, status_code = 'UNKNOWN'
128
+ # scraped from TeleSign api response
129
+ { 'reference_id' => reference_id,
130
+ 'resource_uri' => '/v1/verify/#{reference_id}',
131
+ 'sub_resource' => 'sms',
132
+ 'errors' => [],
133
+ 'status' => {
134
+ 'updated_on' => standard_updated_on,
135
+ 'code' => 290,
136
+ 'description' => 'Message in progress'
137
+ },
138
+ 'verify' => {
139
+ 'code_state' => status_code,
140
+ 'code_entered' => ''
141
+ }
142
+ }
143
+ end
144
+
145
+ def std_standard_body phone_number, phone_type_hash
146
+ simple_phone = phone_number[0]=='1' ? phone_number.slice(1..-1) : phone_number
147
+ { 'reference_id' => std_reference_id,
148
+ 'resource_uri' => nil,
149
+ 'sub_resource' => 'standard',
150
+ 'status' => {
151
+ 'updated_on' => standard_updated_on,
152
+ 'code' => 300,
153
+ 'description' => 'Transaction successfully completed'
154
+ },
155
+ 'errors' => [],
156
+ 'location' => {
157
+ 'city' => 'Reno',
158
+ 'state' => 'NV',
159
+ 'zip' => '89501',
160
+ 'metro_code' => '6720',
161
+ 'county' => '',
162
+ 'country' => {
163
+ 'name' => 'United States',
164
+ 'iso2' => 'US',
165
+ 'iso3' => 'USA'
166
+ },
167
+ 'coordinates' => {
168
+ 'latitude' => 39.52598,
169
+ 'longitude' => -119.80796
170
+ },
171
+ 'time_zone' => {
172
+ 'name' =>' America/Los_Angeles',
173
+ 'utc_offset_min' => '-8',
174
+ 'utc_offset_max' => '-8'
175
+ }
176
+ },
177
+ 'numbering' => {
178
+ 'original' => {
179
+ 'complete_phone_number' => '1'+simple_phone,
180
+ 'country_code' => '1',
181
+ 'phone_number' => simple_phone
182
+ },
183
+ 'cleansing' => {
184
+ 'call' => {
185
+ 'country_code' => '1',
186
+ 'phone_number' => simple_phone,
187
+ 'cleansed_code' => 100,
188
+ 'min_length' => 10,
189
+ 'max_length' => 10},
190
+ 'sms' => {
191
+ 'country_code' => '1',
192
+ 'phone_number' => simple_phone,
193
+ 'cleansed_code' => 100,
194
+ 'min_length' => 10,
195
+ 'max_length' => 10
196
+ }
197
+ }
198
+ },
199
+ 'phone_type' => phone_type_hash,
200
+ 'carrier' => {
201
+ 'name' => 'AT&T - PSTN'
202
+ }
203
+ }
204
+ end
205
+
206
+ def std_reference_id
207
+ SecureRandom.uuid.gsub('-','').upcase
208
+ end
209
+
210
+ def standard_updated_on
211
+ Time.now.utc.strftime('%Y-%m-%dT%H:%M:%S.%6N')
212
+ end
213
+ end
214
+ end
215
+ }
@@ -0,0 +1,9 @@
1
+ module TeleSign
2
+ module MockService
3
+ class Railtie < ::Rails::Railtie
4
+ initializer 'telesign_railtie.configure_rails_initialization' do
5
+ require 'telesign/mock_service/spin_up'
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,18 @@
1
+ module TeleSign
2
+ module MockService
3
+ module SpinUp
4
+ if ENV['TELESIGN_STUBBED']
5
+ port = (ENV['TELESIGN_PORT'].to_i || 11989)
6
+ if `lsof -i :#{port}`.blank? # no process running on #port
7
+ Process.detach(pid = Process.fork do
8
+ require 'telesign/mock_service/fake_server'
9
+ end)
10
+ else
11
+ Rails.logger.warn "TELESIGN STUB MODE FAILED TO START\nProcess already listening on #{port}"
12
+ end
13
+ end
14
+
15
+ at_exit { pid && `kill #{pid}` }
16
+ end
17
+ end
18
+ end