authy 2.7.5 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/build.yml +21 -0
- data/.gitignore +1 -0
- data/CHANGELOG.md +18 -0
- data/Gemfile +1 -11
- data/README.md +39 -195
- data/authy.gemspec +0 -2
- data/examples/Gemfile +6 -0
- data/examples/README.md +48 -0
- data/examples/demo.rb +23 -19
- data/lib/authy.rb +0 -2
- data/lib/authy/api.rb +64 -58
- data/lib/authy/onetouch.rb +1 -1
- data/lib/authy/phone_verification.rb +5 -2
- data/lib/authy/url_helpers.rb +0 -4
- data/lib/authy/version.rb +1 -1
- data/spec/authy/api_spec.rb +384 -91
- data/spec/authy/onetouch_spec.rb +86 -30
- data/spec/authy/phone_verification_spec.rb +185 -54
- data/spec/authy/url_helpers_spec.rb +0 -12
- data/spec/spec_helper.rb +1 -2
- data/verify-legacy-v1.md +35 -0
- metadata +8 -11
- data/.travis.yml +0 -12
- data/Gemfile.lock +0 -118
- data/examples/pv-check.rb +0 -9
- data/examples/pv.rb +0 -12
- data/lib/authy/core_ext.rb +0 -26
- data/lib/authy/phone_intelligence.rb +0 -23
- data/spec/authy/phone_intelligence_spec.rb +0 -94
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '08aec3f3f8bccc8bb877ef1d515c45c2805fc7193104710d50dcaf35083d3b46'
|
4
|
+
data.tar.gz: d0016b8b9f0e3d6e6933d12df0b0e028df063ac882179c7c4212c332bb8b0261
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e6f18818888c63388541c56e048a87abb0faad1e4fc63cb49a6c69d54a14399698a193958d2c155471cbf60e1da395840a9dd1028642511feb4f008b69b6401e
|
7
|
+
data.tar.gz: 648994e063b3fb777137068fca315e344c84c1149eab9ba0fb5e9c2788cdfc27652efbc88b6918ca85f2e4fc1e63e3cc9540210120325890e20290e76f88aeb9
|
@@ -0,0 +1,21 @@
|
|
1
|
+
name: build
|
2
|
+
|
3
|
+
on: [push, pull_request]
|
4
|
+
|
5
|
+
jobs:
|
6
|
+
test:
|
7
|
+
runs-on: ubuntu-latest
|
8
|
+
strategy:
|
9
|
+
fail-fast: false
|
10
|
+
matrix:
|
11
|
+
ruby: [2.5, 2.6, 2.7, head]
|
12
|
+
steps:
|
13
|
+
- uses: actions/checkout@v2
|
14
|
+
- name: Set up Ruby ${{ matrix.ruby }}
|
15
|
+
uses: ruby/setup-ruby@v1
|
16
|
+
with:
|
17
|
+
ruby-version: ${{ matrix.ruby }}
|
18
|
+
- name: Install dependencies
|
19
|
+
run: bundle install
|
20
|
+
- name: Run tests
|
21
|
+
run: bundle exec rspec
|
data/.gitignore
CHANGED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# Changelog for `Authy`
|
2
|
+
|
3
|
+
## Ongoing [☰](https://github.com/twilio/authy-ruby/compare/v3.0.0...master)
|
4
|
+
|
5
|
+
## 3.0.0 (14 December, 2020) [☰](https://github.com/twilio/authy-ruby/compare/v2.7.5...v3.0.0)
|
6
|
+
|
7
|
+
* Major updates
|
8
|
+
* Removed Phone Intelligence APIs
|
9
|
+
* Deprecated Phone Verification APIs (use the Twilio Verify API instead: https://twil.io/verify-start-ruby)
|
10
|
+
* Mocked API requests in tests, removed Sandbox API
|
11
|
+
* Minor updates
|
12
|
+
* Added API calls for requesting an email and updating a user's email address (#68)
|
13
|
+
* Removed polyfills for try_convert
|
14
|
+
* Updates to insecure dependencies
|
15
|
+
|
16
|
+
## 2.7.5 and older
|
17
|
+
|
18
|
+
Please check the [commit log](https://github.com/twilio/authy-ruby/compare/f9e9236...v2.7.5).
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,232 +1,76 @@
|
|
1
|
-
|
1
|
+
[![Gem Version](https://badge.fury.io/rb/authy.svg)](https://rubygems.org/gems/authy/) [![Build Status](https://github.com/twilio/authy-ruby/workflows/build/badge.svg)](https://github.com/twilio/authy-ruby/actions) [![Code Climate](https://codeclimate.com/github/authy/authy-ruby.png)](https://codeclimate.com/github/authy/authy-ruby)
|
2
2
|
|
3
|
-
Ruby
|
3
|
+
# Ruby Client for Twilio Authy Two-Factor Authentication (2FA) API
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
```ruby
|
8
|
-
require 'authy'
|
9
|
-
|
10
|
-
Authy.api_key = 'your-api-key'
|
11
|
-
Authy.api_uri = 'https://api.authy.com'
|
12
|
-
```
|
13
|
-
|
14
|
-
Using Ruby on Rails? _Put this in **config/initializers** and create a new file called **authy.rb**._
|
15
|
-
|
16
|
-
## Registering a user
|
17
|
-
|
18
|
-
__NOTE: User is matched based on cellphone and country code not e-mail.
|
19
|
-
A cellphone is uniquely associated with an authy_id.__
|
20
|
-
|
21
|
-
|
22
|
-
`Authy::API.register_user` requires the user e-mail address and cellphone. Optionally you can pass in the country_code or we will asume
|
23
|
-
USA. The call will return you the authy id for the user that you need to store in your database.
|
24
|
-
|
25
|
-
Assuming you have a `users` database with a `authy_id` field in the `users` database.
|
26
|
-
|
27
|
-
```ruby
|
28
|
-
authy = Authy::API.register_user(:email => 'users@email.com', :cellphone => "111-111-1111", :country_code => "1")
|
29
|
-
|
30
|
-
if authy.ok?
|
31
|
-
self.authy_id = authy.id # this will give you the user authy id to store it in your database
|
32
|
-
else
|
33
|
-
authy.errors # this will return an error hash
|
34
|
-
end
|
35
|
-
```
|
36
|
-
|
37
|
-
## Verifying a user
|
38
|
-
|
39
|
-
|
40
|
-
__NOTE: Token verification is only enforced if the user has completed registration. To change this behaviour see Forcing Verification section below.__
|
41
|
-
|
42
|
-
>*Registration is completed once the user installs and registers the Authy mobile app or logins once successfully using SMS.*
|
43
|
-
|
44
|
-
`Authy::API.verify` takes the authy_id that you are verifying and the token that you want to verify. You should have the authy_id in your database
|
45
|
-
|
46
|
-
```ruby
|
47
|
-
response = Authy::API.verify(:id => user.authy_id, :token => 'token-user-entered')
|
48
|
-
|
49
|
-
if response.ok?
|
50
|
-
# token was valid, user can sign in
|
51
|
-
else
|
52
|
-
# token is invalid
|
53
|
-
end
|
54
|
-
```
|
55
|
-
|
56
|
-
### Forcing Verification
|
57
|
-
|
58
|
-
If you wish to verify tokens even if the user has not yet complete registration, pass force=true when verifying the token.
|
59
|
-
|
60
|
-
```ruby
|
61
|
-
response = Authy::API.verify(:id => user.authy_id, :token => 'token-user-entered', :force => true)
|
62
|
-
```
|
63
|
-
|
64
|
-
## Requesting a SMS token
|
65
|
-
|
66
|
-
`Authy::API.request_sms` takes the authy_id that you want to send a SMS token. This requires Authy SMS plugin to be enabled.
|
67
|
-
|
68
|
-
```ruby
|
69
|
-
response = Authy::API.request_sms(:id => user.authy_id)
|
70
|
-
|
71
|
-
if response.ok?
|
72
|
-
# sms was sent
|
73
|
-
else
|
74
|
-
response.errors
|
75
|
-
#sms failed to send
|
76
|
-
end
|
77
|
-
```
|
78
|
-
|
79
|
-
This call will be ignored if the user is using the Authy Mobile App. If you still want to send
|
80
|
-
the SMS pass force=true as an option
|
81
|
-
|
82
|
-
```ruby
|
83
|
-
response = Authy::API.request_sms(:id => user.authy_id, :force => true)
|
84
|
-
```
|
5
|
+
Documentation for Ruby usage of the Authy API lives in the [official Twilio documentation](https://www.twilio.com/docs/authy/api/).
|
85
6
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
```
|
7
|
+
The Authy API supports multiple channels of 2FA:
|
8
|
+
* One-time passwords via SMS and voice.
|
9
|
+
* Soft token ([TOTP](https://www.twilio.com/docs/glossary/totp) via the Authy App)
|
10
|
+
* Push authentication via the Authy App
|
91
11
|
|
92
|
-
If
|
12
|
+
If you only need SMS and Voice support for one-time passwords, we recommend using the [Twilio Verify API](https://www.twilio.com/docs/verify/api) instead.
|
93
13
|
|
94
|
-
|
14
|
+
[More on how to choose between Authy and Verify here.](https://www.twilio.com/docs/verify/authy-vs-verify)
|
95
15
|
|
96
|
-
|
16
|
+
### Authy Quickstart
|
97
17
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
if response.ok?
|
102
|
-
# call was done
|
103
|
-
else
|
104
|
-
response.errors
|
105
|
-
# call failed
|
106
|
-
end
|
107
|
-
```
|
18
|
+
For a full tutorial, check out either of the Ruby Authy Quickstart in our docs:
|
19
|
+
* [Ruby/Rails Authy Quickstart](https://www.twilio.com/docs/authy/quickstart/two-factor-authentication-ruby-rails)
|
108
20
|
|
109
|
-
|
21
|
+
## Authy Ruby Installation
|
110
22
|
|
111
|
-
```ruby
|
112
|
-
response = Authy::API.request_phone_call(:id => user.authy_id, :force => true)
|
113
23
|
```
|
114
|
-
|
115
|
-
## Requesting QR code for authenticator apps like Google Authenticator
|
116
|
-
|
117
|
-
`Authy::API.request_qr_code` takes authy_id that you want to deliver the qr code. This requires **Generic authenticator tokens** to be enabled in Authy console setting. Optinally, you can provide `qr_size` as a number to decide the output of qr image (For example: `qr_size: 400` will returns a 400x400 image) and `label` as a custom label to be shown by the authenticator app.
|
118
|
-
|
119
|
-
```ruby
|
120
|
-
response = Authy::API.request_qr_code(id: user.authy_id, qr_size: 500, label: "My Example App")
|
121
|
-
if response.ok?
|
122
|
-
# qr code was generated
|
123
|
-
else
|
124
|
-
response.errors
|
125
|
-
end
|
126
|
-
|
127
|
-
# You can access the iamge link with
|
128
|
-
link = response.qr_code
|
24
|
+
gem install authy
|
129
25
|
```
|
130
26
|
|
131
|
-
##
|
27
|
+
## Usage
|
132
28
|
|
133
|
-
|
29
|
+
To use the Authy client, require the `authy` gem and initialize it with our API URI and your production API Key found in the [Twilio Console](https://www.twilio.com/console/authy/applications/):
|
134
30
|
|
135
31
|
```ruby
|
136
|
-
|
137
|
-
|
138
|
-
if response.ok?
|
139
|
-
# the user was deleted
|
140
|
-
else
|
141
|
-
response.errors
|
142
|
-
# we were unavailable to delete the user
|
143
|
-
end
|
144
|
-
```
|
145
|
-
|
146
|
-
## User status
|
147
|
-
|
148
|
-
`Authy::API.user_status` takes the authy_id of the user that you want to get the status from your app.
|
32
|
+
require 'authy'
|
149
33
|
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
if response.ok?
|
154
|
-
# do something with user status
|
155
|
-
else
|
156
|
-
response.errors
|
157
|
-
# the user doesn't exist
|
158
|
-
end
|
34
|
+
Authy.api_uri = 'https://api.authy.com'
|
35
|
+
Authy.api_key = 'your-api-key'
|
159
36
|
```
|
160
37
|
|
161
|
-
|
162
|
-
|
163
|
-
### Starting a phone verification
|
164
|
-
|
165
|
-
`Authy::PhoneVerification.start` takes a country code, phone number and a method (sms or call) to deliver the code. You can also provide a custom_code but this feature needs to be enabled by support@twilio.com
|
166
|
-
|
167
|
-
```ruby
|
168
|
-
response = Authy::PhoneVerification.start(via: "sms", country_code: 1, phone_number: "111-111-1111")
|
169
|
-
if response.ok?
|
170
|
-
# verification was started
|
171
|
-
end
|
172
|
-
```
|
38
|
+
Rails users can put this in config/initializers and create a new file called `authy.rb`.
|
173
39
|
|
174
|
-
|
40
|
+
![authy api key in console](https://s3.amazonaws.com/com.twilio.prod.twilio-docs/images/account-security-api-key.width-800.png)
|
175
41
|
|
176
|
-
|
42
|
+
## 2FA Workflow
|
177
43
|
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
# verification was successful
|
182
|
-
end
|
183
|
-
```
|
44
|
+
1. [Create a user](https://www.twilio.com/docs/authy/api/users#enabling-new-user)
|
45
|
+
2. [Send a one-time password](https://www.twilio.com/docs/authy/api/one-time-passwords)
|
46
|
+
3. [Verify a one-time password](https://www.twilio.com/docs/authy/api/one-time-passwords#verify-a-one-time-password)
|
184
47
|
|
185
|
-
|
48
|
+
**OR**
|
186
49
|
|
187
|
-
|
188
|
-
|
50
|
+
1. [Create a user](https://www.twilio.com/docs/authy/api/users#enabling-new-user)
|
51
|
+
2. [Send a push authentication](https://www.twilio.com/docs/authy/api/push-authentications)
|
52
|
+
3. [Check a push authentication status](https://www.twilio.com/docs/authy/api/push-authentications#check-approval-request-status)
|
189
53
|
|
190
|
-
`Authy::OneTouch.send_approval_request` takes the Authy user ID, a message to fill up the push notification
|
191
|
-
body, an optional hash details for the user and another optional hash for hidden details for internal app
|
192
|
-
control.
|
193
54
|
|
194
|
-
|
195
|
-
one_touch = Authy::OneTouch.send_approval_request(
|
196
|
-
id: @user.authy_id,
|
197
|
-
message: "Request to Login",
|
198
|
-
details: {
|
199
|
-
'Email Address' => @user.email,
|
200
|
-
},
|
201
|
-
hidden_details: { ip: '1.1.1.1' }
|
202
|
-
)
|
203
|
-
```
|
55
|
+
## <a name="phone-verification"></a>Phone Verification
|
204
56
|
|
205
|
-
|
206
|
-
(set into Dashboard) updating user's `authy_status` flag. You might have an endpoint in a controller
|
207
|
-
such as:
|
57
|
+
[Phone verification now lives in the Twilio API](https://www.twilio.com/docs/verify/api) and has [Ruby support through the official Twilio helper libraries](https://www.twilio.com/docs/libraries/ruby).
|
208
58
|
|
209
|
-
|
210
|
-
def callback
|
211
|
-
authy_id = params[:authy_id]
|
212
|
-
@user = User.find_by authy_id: authy_id
|
213
|
-
@user.update(authy_status: params[:status])
|
214
|
-
end
|
215
|
-
```
|
59
|
+
[Legacy (V1) documentation here.](verify-legacy-v1.md) **Verify V1 is not recommended for new development. Please consider using [Verify V2](https://www.twilio.com/docs/verify/api)**.
|
216
60
|
|
61
|
+
## Demo
|
217
62
|
|
218
|
-
|
63
|
+
See the [`./examples`](./examples) directory for a demo CLI application that uses the Authy API.
|
219
64
|
|
65
|
+
## Contributing
|
220
66
|
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
|
221
67
|
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
|
222
68
|
* Fork the project.
|
223
69
|
* Start a feature/bugfix branch.
|
224
70
|
* Commit and push until you are happy with your contribution.
|
225
|
-
*
|
226
|
-
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit
|
71
|
+
* Add tests.
|
72
|
+
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit.
|
227
73
|
|
228
|
-
Copyright
|
229
|
-
==
|
74
|
+
## Copyright
|
230
75
|
|
231
|
-
Copyright (c) 2011-2020 Authy Inc. See LICENSE.txt for
|
232
|
-
further details.
|
76
|
+
Copyright (c) 2011-2020 Authy Inc. See [LICENSE](https://github.com/twilio/authy-ruby/blob/master/LICENSE.txt) for further details.
|
data/authy.gemspec
CHANGED
@@ -13,8 +13,6 @@ Gem::Specification.new do |s|
|
|
13
13
|
s.description = %q{Ruby library to access Authy services}
|
14
14
|
s.license = 'MIT'
|
15
15
|
|
16
|
-
s.rubyforge_project = "authy"
|
17
|
-
|
18
16
|
s.files = `git ls-files`.split("\n")
|
19
17
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
20
18
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
data/examples/Gemfile
ADDED
data/examples/README.md
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# Demo
|
2
|
+
|
3
|
+
In this `example` directory is a demo of using the Twilio Authy Two-Factor Authentication (2FA) API with the Authy gem.
|
4
|
+
|
5
|
+
## How does it work?
|
6
|
+
|
7
|
+
The demo works on the command line and allows you to register a user in your Authy application, request a token for that user and verify a token for a user. Registered users and their authy_id token are stored in a local SQLite database.
|
8
|
+
|
9
|
+
## How to use the demo
|
10
|
+
|
11
|
+
Change into the `examples` directory and install the dependencies with:
|
12
|
+
|
13
|
+
```bash
|
14
|
+
bundle install
|
15
|
+
```
|
16
|
+
|
17
|
+
Then, run the demo with:
|
18
|
+
|
19
|
+
```bash
|
20
|
+
./demo.rb
|
21
|
+
```
|
22
|
+
|
23
|
+
You will be asked for your Authy API key then presented a list of options.
|
24
|
+
|
25
|
+
```bash
|
26
|
+
$ ./demo.rb
|
27
|
+
|
28
|
+
Enter your Authy API Key (won't be displayed):
|
29
|
+
1. Register a user
|
30
|
+
2. Request token
|
31
|
+
3. Verify token
|
32
|
+
What do you want to do?
|
33
|
+
```
|
34
|
+
|
35
|
+
On the first run, choose to register a user. This will ask you for an email address, country code and phone number. With those details a user will be registered with your Authy application and stored in your local database.
|
36
|
+
|
37
|
+
Then you can run the application and either request a token, to have a token sent by SMS, or verify a token, you can use the token from an SMS or the Authy app to verify.
|
38
|
+
|
39
|
+
### API keys
|
40
|
+
|
41
|
+
You will be asked for your Authy API key each time you run the demo. You can avoid this by [adding your Authy API key to your environment variables](https://www.twilio.com/blog/2017/01/how-to-set-environment-variables.html).
|
42
|
+
|
43
|
+
```
|
44
|
+
export AUTHY_API_KEY=ABC123xxxxx
|
45
|
+
./demo.rb
|
46
|
+
```
|
47
|
+
|
48
|
+
Check out the code in `./examples/demo.rb` to see how the API and this gem is used.
|
data/examples/demo.rb
CHANGED
@@ -5,12 +5,11 @@ require 'sqlite3'
|
|
5
5
|
require 'active_record'
|
6
6
|
require 'highline/import' # gem install highline
|
7
7
|
|
8
|
-
|
9
|
-
Authy.api_key = "a1ffc30aa2d775c7ebebe45585727fe0"
|
8
|
+
trap("SIGINT") { exit! }
|
10
9
|
|
11
10
|
# setup db
|
12
11
|
ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => "db")
|
13
|
-
class AddUsers < ActiveRecord::Migration
|
12
|
+
class AddUsers < ActiveRecord::Migration[6.0]
|
14
13
|
def self.up
|
15
14
|
create_table :users do |t|
|
16
15
|
t.string :email
|
@@ -28,11 +27,15 @@ class User < ActiveRecord::Base
|
|
28
27
|
validates_uniqueness_of :email
|
29
28
|
end
|
30
29
|
|
30
|
+
api_key = ENV["AUTHY_API_KEY"] || ask("Enter your Authy API Key (won't be displayed): "){ |q| q.echo = false }
|
31
|
+
|
32
|
+
Authy.api_url = "https://api.authy.com"
|
33
|
+
Authy.api_key = api_key
|
31
34
|
|
32
35
|
choose do |menu|
|
33
|
-
menu.prompt = "
|
36
|
+
menu.prompt = "What do you want to do? "
|
34
37
|
|
35
|
-
menu.choice(
|
38
|
+
menu.choice("Register a user") do
|
36
39
|
loop do
|
37
40
|
email = ask("email: ")
|
38
41
|
country_code = ask("country code: ")
|
@@ -55,9 +58,8 @@ choose do |menu|
|
|
55
58
|
end
|
56
59
|
end
|
57
60
|
|
58
|
-
menu.choice(
|
61
|
+
menu.choice("Request token") do
|
59
62
|
email = ask("email: ")
|
60
|
-
token = ask("token: ")
|
61
63
|
|
62
64
|
user = User.where(:email => email).first
|
63
65
|
if !user
|
@@ -65,17 +67,18 @@ choose do |menu|
|
|
65
67
|
return
|
66
68
|
end
|
67
69
|
|
68
|
-
#
|
69
|
-
|
70
|
+
# send sms to the user. `force` makes it send the sms even if the user uses a smartphone
|
71
|
+
# this api call will return an error if the account doesn't have the SMS addon enabled
|
72
|
+
response = Authy::API.request_sms(:id => user.authy_id, :force => true)
|
70
73
|
|
71
|
-
if
|
72
|
-
puts "
|
74
|
+
if response.ok?
|
75
|
+
puts "Message was sent"
|
73
76
|
else
|
74
|
-
puts "
|
77
|
+
puts "Failed to send message: #{response.errors.inspect}"
|
75
78
|
end
|
76
79
|
end
|
77
80
|
|
78
|
-
menu.choice(
|
81
|
+
menu.choice("Verify token") do
|
79
82
|
email = ask("email: ")
|
80
83
|
|
81
84
|
user = User.where(:email => email).first
|
@@ -84,14 +87,15 @@ choose do |menu|
|
|
84
87
|
return
|
85
88
|
end
|
86
89
|
|
87
|
-
|
88
|
-
# this api call will return an error if the account doesn't have the SMS addon enabled
|
89
|
-
response = Authy::API.request_sms(:id => user.authy_id, :force => true)
|
90
|
+
token = ask("token: ")
|
90
91
|
|
91
|
-
if
|
92
|
-
|
92
|
+
# verify if the given token is correct. `force` makes it validate the code even if the user has not confirmed its account
|
93
|
+
otp = Authy::API.verify(:id => user.authy_id, :token => token, :force => true)
|
94
|
+
|
95
|
+
if otp.ok?
|
96
|
+
puts "Welcome back!"
|
93
97
|
else
|
94
|
-
puts "
|
98
|
+
puts "Wrong email or token :("
|
95
99
|
end
|
96
100
|
end
|
97
101
|
end
|