omniauth-cas 1.0.4 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +6 -14
- data/.editorconfig +16 -0
- data/.travis.yml +16 -3
- data/CHANGELOG.md +27 -0
- data/Gemfile +5 -0
- data/README.md +47 -16
- data/lib/omniauth-cas.rb +1 -1
- data/lib/omniauth/cas/version.rb +1 -1
- data/lib/omniauth/strategies/cas.rb +105 -45
- data/lib/omniauth/strategies/cas/logout_request.rb +58 -0
- data/lib/omniauth/strategies/cas/service_ticket_validator.rb +24 -9
- data/omniauth-cas.gemspec +7 -9
- data/spec/fixtures/cas_success.xml +3 -0
- data/spec/fixtures/cas_success_jasig.xml +3 -0
- data/spec/omniauth/strategies/cas/logout_request_spec.rb +105 -0
- data/spec/omniauth/strategies/cas/service_ticket_validator_spec.rb +54 -13
- data/spec/omniauth/strategies/cas_spec.rb +180 -84
- data/spec/spec_helper.rb +0 -4
- metadata +44 -59
- data/.rvmrc +0 -1
- data/History.md +0 -58
- data/lib/omniauth/strategies/cas/configuration.rb +0 -34
- data/spec/omniauth/strategies/cas/configuration_spec.rb +0 -60
checksums.yaml
CHANGED
@@ -1,15 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
metadata.gz: !binary |-
|
9
|
-
ZTdkZjEzMDY4ODVjMzY1NDQ2MmUxZTAyMzRkMGUwNzcxYzAyOGM5MzBlNDZi
|
10
|
-
ZjU1OTk0OWU1ODIzNTIwYTIxNWM5ZDEzNzFiY2UyODJhYzY3NDRkZmM5ODcy
|
11
|
-
NTBlMGM5YWFhZWU1MDBlMTI2YWQ0NTRmODRkZjhiYmVmMjc2ZDI=
|
12
|
-
data.tar.gz: !binary |-
|
13
|
-
OTY0NjU1NTFhZGZmNjdlN2E2OTY5MjNkMTU0NDY5NWQ1ZjQ3N2RiZWFjNmZj
|
14
|
-
MDA3MmY4M2U1MjMwOThkZmRhNzYwZDI5NzljYjRiYzk5YjY5MWFlNjBjYjY0
|
15
|
-
NzBhNjE1OWViZjMzMGVmYzkzMWM5MTk5MGNmNjk1MTU3YjYxYWU=
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 20d02177b4bdbd637993a8d225133ada4d6d40092b59db98e54b38bbe8aef780
|
4
|
+
data.tar.gz: 9143ccd826882b5f1228b7686958cfa62c9527ab1fef041d6ce1bfb39e7e0f6d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a9edd4d3c46a7c7349a0ebf05a67747cb25cfda51fc2c4ebfbc054726e4e8b7034f969291f5f184d609999250507bdfe1da01f659cb43682b837d4fa44292271
|
7
|
+
data.tar.gz: e64e3311e11537b2abc77d25189ef38eebdcbb147eb6db53deb7562530371f40851e035eae08370f3621843873c36c47e9690d1e5f2a0678b31ea8dd87f79bca
|
data/.editorconfig
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# EditorConfig helps developers define and maintain consistent
|
2
|
+
# coding styles between different editors and IDEs
|
3
|
+
# editorconfig.org
|
4
|
+
|
5
|
+
root = true
|
6
|
+
|
7
|
+
[*]
|
8
|
+
# Change these settings to your own preference
|
9
|
+
indent_style = space
|
10
|
+
indent_size = 2
|
11
|
+
|
12
|
+
# We recommend you to keep these unchanged
|
13
|
+
end_of_line = lf
|
14
|
+
charset = utf-8
|
15
|
+
trim_trailing_whitespace = true
|
16
|
+
insert_final_newline = true
|
data/.travis.yml
CHANGED
@@ -1,8 +1,21 @@
|
|
1
|
+
dist: xenial
|
2
|
+
os: linux
|
3
|
+
language: ruby
|
1
4
|
rvm:
|
2
|
-
- 1
|
3
|
-
-
|
5
|
+
- 2.1
|
6
|
+
- 2.2
|
7
|
+
- 2.3
|
8
|
+
- 2.4
|
9
|
+
- 2.5
|
10
|
+
- 2.6
|
11
|
+
- 2.7
|
12
|
+
- ruby-edge
|
4
13
|
branches:
|
5
14
|
only:
|
6
15
|
- master
|
7
16
|
before_install:
|
8
|
-
- gem
|
17
|
+
- gem uninstall -v '>= 2' -i $(rvm gemdir)@global -ax bundler || true
|
18
|
+
- gem install bundler -v '< 2'
|
19
|
+
jobs:
|
20
|
+
allow_failures:
|
21
|
+
- rvm: ruby-edge
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# Change Log
|
2
|
+
|
3
|
+
All notable changes to this project will be documented in this file.
|
4
|
+
|
5
|
+
The format is based on [Keep a Changelog](http://keepachangelog.com/) and this
|
6
|
+
project adheres to [Semantic Versioning](http://semver.org/)
|
7
|
+
|
8
|
+
## 2.0.0 - 2010-11-14
|
9
|
+
|
10
|
+
### Added
|
11
|
+
|
12
|
+
* Add support for multivalued attributes ([#59](https://github.com/dlindahl/omniauth-cas/pull/59))
|
13
|
+
* Successfully test against Ruby 2.4 and up ([#60](https://github.com/dlindahl/omniauth-cas/pull/60))
|
14
|
+
|
15
|
+
### Changed
|
16
|
+
|
17
|
+
* Forward success response to `fetch_raw_info` callback ([#51](https://github.com/dlindahl/omniauth-cas/pull/51))
|
18
|
+
* Relax development dependencies to the latest versions
|
19
|
+
|
20
|
+
## 1.1.1 - 2016-09-19
|
21
|
+
|
22
|
+
### Changed
|
23
|
+
|
24
|
+
* Relax gemspec requirements, to add support for Rails 5.
|
25
|
+
|
26
|
+
Note that the only tested versions of Ruby are now 2.1, 2.2, and 2.3 - older
|
27
|
+
versions of Ruby should work, but are no longer officially supported.
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,14 +1,16 @@
|
|
1
1
|
# OmniAuth CAS Strategy [![Gem Version][version_badge]][version] [![Build Status][travis_status]][travis]
|
2
2
|
|
3
|
-
[version_badge]: https://badge.fury.io/rb/omniauth-cas.
|
4
|
-
[version]:
|
5
|
-
[travis]:
|
6
|
-
[travis_status]: https://secure.travis-ci.org/dlindahl/omniauth-cas.
|
3
|
+
[version_badge]: https://badge.fury.io/rb/omniauth-cas.svg
|
4
|
+
[version]: https://badge.fury.io/rb/omniauth-cas
|
5
|
+
[travis]: https://travis-ci.org/dlindahl/omniauth-cas
|
6
|
+
[travis_status]: https://secure.travis-ci.org/dlindahl/omniauth-cas.svg
|
7
|
+
[releases]: https://github.com/dlindahl/omniauth-cas/releases
|
7
8
|
|
8
9
|
This is a OmniAuth 1.0 compatible port of the previously available
|
9
10
|
[OmniAuth CAS strategy][old_omniauth_cas] that was bundled with OmniAuth 0.3.
|
10
11
|
|
11
|
-
[View the documentation][document_up]
|
12
|
+
* [View the documentation][document_up]
|
13
|
+
* [Changelog][releases]
|
12
14
|
|
13
15
|
## Installation
|
14
16
|
|
@@ -41,21 +43,49 @@ end
|
|
41
43
|
OmniAuth CAS requires at least one of the following two configuration options:
|
42
44
|
|
43
45
|
* `url` - Defines the URL of your CAS server (i.e. `http://example.org:8080`)
|
44
|
-
* `host` - Defines the host of your CAS server.
|
45
|
-
* `login_url` - Defines the URL used to prompt users for their login information. Defaults to `/login`
|
46
|
-
If no `host` is configured, the host application's domain will be used.
|
46
|
+
* `host` - Defines the host of your CAS server (i.e. `example.org`).
|
47
47
|
|
48
48
|
#### Optional
|
49
49
|
|
50
50
|
Other configuration options:
|
51
51
|
|
52
|
-
* `port` - The port to use for your configured CAS `host`. Optional if using `url
|
53
|
-
* `ssl` - TRUE to connect to your CAS server over SSL. Optional if using `url
|
54
|
-
* `service_validate_url` - The URL to use to validate a user. Defaults to `'/serviceValidate'
|
55
|
-
* `
|
56
|
-
* `
|
57
|
-
* `
|
52
|
+
* `port` - The port to use for your configured CAS `host`. Optional if using `url`.
|
53
|
+
* `ssl` - TRUE to connect to your CAS server over SSL. Optional if using `url`.
|
54
|
+
* `service_validate_url` - The URL to use to validate a user. Defaults to `'/serviceValidate'`.
|
55
|
+
* `callback_url` - The URL custom URL path which CAS uses to call back to the service. Defaults to `/users/auth/cas/callback`.
|
56
|
+
* `logout_url` - The URL to use to logout a user. Defaults to `'/logout'`.
|
57
|
+
* `login_url` - Defines the URL used to prompt users for their login information. Defaults to `/login` If no `host` is configured, the host application's domain will be used.
|
58
|
+
* `uid_field` - The user data attribute to use as your user's unique identifier. Defaults to `'user'` (which usually contains the user's login name).
|
59
|
+
* `ca_path` - Optional when `ssl` is `true`. Sets path of a CA certification directory. See [Net::HTTP][net_http] for more details.
|
58
60
|
* `disable_ssl_verification` - Optional when `ssl` is true. Disables verification.
|
61
|
+
* `merge_multivalued_attributes` - When set to `true` returns attributes with multiple values as arrays. Defaults to `false` and returns the last value as a string.
|
62
|
+
* `on_single_sign_out` - Optional. Callback used when a [CAS 3.1 Single Sign Out][sso]
|
63
|
+
request is received.
|
64
|
+
* `fetch_raw_info` - Optional. Callback used to return additional "raw" user
|
65
|
+
info from other sources.
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
provider :cas,
|
69
|
+
fetch_raw_info: Proc.new { |strategy, opts, ticket, user_info, rawxml|
|
70
|
+
return {} if user_info.empty? || rawxml.nil? # Auth failed
|
71
|
+
|
72
|
+
extra_info = ExternalService.get(user_info[:user]).attributes
|
73
|
+
extra_info.merge!({'roles' => rawxml.xpath('//cas:roles').map(&:text)})
|
74
|
+
extra_info
|
75
|
+
}
|
76
|
+
```
|
77
|
+
|
78
|
+
Configurable options for values returned by CAS:
|
79
|
+
|
80
|
+
* `uid_key` - The user ID data attribute to use as your user's unique identifier. Defaults to `'user'` (which usually contains the user's login name).
|
81
|
+
* `name_key` - The data attribute containing user first and last name. Defaults to `'name'`.
|
82
|
+
* `email_key` - The data attribute containing user email address. Defaults to `'email'`.
|
83
|
+
* `nickname_key` - The data attribute containing user's nickname. Defaults to `'user'`.
|
84
|
+
* `first_name_key` - The data attribute containing user first name. Defaults to `'first_name'`.
|
85
|
+
* `last_name_key` - The data attribute containing user last name. Defaults to `'last_name'`.
|
86
|
+
* `location_key` - The data attribute containing user location/address. Defaults to `'location'`.
|
87
|
+
* `image_key` - The data attribute containing user image/picture. Defaults to `'image'`.
|
88
|
+
* `phone_key` - The data attribute containing user contact phone number. Defaults to `'phone'`.
|
59
89
|
|
60
90
|
## Migrating from OmniAuth 0.3
|
61
91
|
|
@@ -93,5 +123,6 @@ Special thanks go out to the following people
|
|
93
123
|
* @rbq for README updates and OmniAuth 0.3 migration guide
|
94
124
|
|
95
125
|
[old_omniauth_cas]: https://github.com/intridea/omniauth/blob/0-3-stable/oa-enterprise/lib/omniauth/strategies/cas.rb
|
96
|
-
[document_up]:
|
97
|
-
[net_http]:
|
126
|
+
[document_up]: https://dlindahl.github.io/omniauth-cas/
|
127
|
+
[net_http]: https://ruby-doc.org/stdlib-1.9.3/libdoc/net/http/rdoc/Net/HTTP.html
|
128
|
+
[sso]: https://wiki.jasig.org/display/CASUM/Single+Sign+Out
|
data/lib/omniauth-cas.rb
CHANGED
@@ -1 +1 @@
|
|
1
|
-
require
|
1
|
+
require 'omniauth/cas'
|
data/lib/omniauth/cas/version.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
require 'omniauth
|
1
|
+
require 'omniauth'
|
2
2
|
require 'addressable/uri'
|
3
3
|
|
4
4
|
module OmniAuth
|
@@ -10,8 +10,8 @@ module OmniAuth
|
|
10
10
|
class MissingCASTicket < StandardError; end
|
11
11
|
class InvalidCASTicket < StandardError; end
|
12
12
|
|
13
|
-
autoload :Configuration, 'omniauth/strategies/cas/configuration'
|
14
13
|
autoload :ServiceTicketValidator, 'omniauth/strategies/cas/service_ticket_validator'
|
14
|
+
autoload :LogoutRequest, 'omniauth/strategies/cas/logout_request'
|
15
15
|
|
16
16
|
attr_accessor :raw_info
|
17
17
|
alias_method :user_info, :raw_info
|
@@ -22,84 +22,125 @@ module OmniAuth
|
|
22
22
|
option :port, nil
|
23
23
|
option :path, nil
|
24
24
|
option :ssl, true
|
25
|
+
option :merge_multivalued_attributes, false
|
25
26
|
option :service_validate_url, '/serviceValidate'
|
26
27
|
option :login_url, '/login'
|
27
28
|
option :logout_url, '/logout'
|
28
|
-
option :
|
29
|
+
option :on_single_sign_out, Proc.new {}
|
30
|
+
# A Proc or lambda that returns a Hash of additional user info to be
|
31
|
+
# merged with the info returned by the CAS server.
|
32
|
+
#
|
33
|
+
# @param [Object] An instance of OmniAuth::Strategies::CAS for the current request
|
34
|
+
# @param [String] The user's Service Ticket value
|
35
|
+
# @param [Hash] The user info for the Service Ticket returned by the CAS server
|
36
|
+
#
|
37
|
+
# @return [Hash] Extra user info
|
38
|
+
option :fetch_raw_info, Proc.new { Hash.new }
|
39
|
+
# Make all the keys configurable with some defaults set here
|
40
|
+
option :uid_field, 'user'
|
41
|
+
option :name_key, 'name'
|
42
|
+
option :email_key, 'email'
|
43
|
+
option :nickname_key, 'user'
|
44
|
+
option :first_name_key, 'first_name'
|
45
|
+
option :last_name_key, 'last_name'
|
46
|
+
option :location_key, 'location'
|
47
|
+
option :image_key, 'image'
|
48
|
+
option :phone_key, 'phone'
|
29
49
|
|
30
50
|
# As required by https://github.com/intridea/omniauth/wiki/Auth-Hash-Schema
|
31
|
-
AuthHashSchemaKeys = %w{name email first_name last_name location image phone}
|
51
|
+
AuthHashSchemaKeys = %w{name email nickname first_name last_name location image phone}
|
32
52
|
info do
|
33
53
|
prune!({
|
34
|
-
:
|
35
|
-
:
|
36
|
-
:
|
37
|
-
:
|
38
|
-
:
|
39
|
-
:
|
40
|
-
:
|
54
|
+
name: raw_info[options[:name_key].to_s],
|
55
|
+
email: raw_info[options[:email_key].to_s],
|
56
|
+
nickname: raw_info[options[:nickname_key].to_s],
|
57
|
+
first_name: raw_info[options[:first_name_key].to_s],
|
58
|
+
last_name: raw_info[options[:last_name_key].to_s],
|
59
|
+
location: raw_info[options[:location_key].to_s],
|
60
|
+
image: raw_info[options[:image_key].to_s],
|
61
|
+
phone: raw_info[options[:phone_key].to_s]
|
41
62
|
})
|
42
63
|
end
|
43
64
|
|
44
65
|
extra do
|
45
|
-
prune!
|
66
|
+
prune!(
|
67
|
+
raw_info.delete_if{ |k,v| AuthHashSchemaKeys.include?(k) }
|
68
|
+
)
|
46
69
|
end
|
47
70
|
|
48
71
|
uid do
|
49
|
-
raw_info[
|
72
|
+
raw_info[options[:uid_field].to_s]
|
50
73
|
end
|
51
74
|
|
52
75
|
credentials do
|
53
|
-
prune!({
|
54
|
-
:ticket => @ticket
|
55
|
-
})
|
56
|
-
end
|
57
|
-
|
58
|
-
def initialize( app, *args, &block )
|
59
|
-
super
|
60
|
-
@configuration = Configuration.new( @options )
|
76
|
+
prune!({ ticket: @ticket })
|
61
77
|
end
|
62
78
|
|
63
79
|
def callback_phase
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
80
|
+
if on_sso_path?
|
81
|
+
single_sign_out_phase
|
82
|
+
else
|
83
|
+
@ticket = request.params['ticket']
|
84
|
+
return fail!(:no_ticket, MissingCASTicket.new('No CAS Ticket')) unless @ticket
|
85
|
+
fetch_raw_info(@ticket)
|
86
|
+
return fail!(:invalid_ticket, InvalidCASTicket.new('Invalid CAS Ticket')) if raw_info.empty?
|
87
|
+
super
|
88
|
+
end
|
73
89
|
end
|
74
90
|
|
75
91
|
def request_phase
|
76
|
-
service_url = append_params(
|
92
|
+
service_url = append_params(callback_url, return_url)
|
77
93
|
|
78
94
|
[
|
79
95
|
302,
|
80
96
|
{
|
81
|
-
'Location' => login_url(
|
97
|
+
'Location' => login_url(service_url),
|
82
98
|
'Content-Type' => 'text/plain'
|
83
99
|
},
|
84
100
|
["You are being redirected to CAS for sign-in."]
|
85
101
|
]
|
86
102
|
end
|
87
103
|
|
104
|
+
def on_sso_path?
|
105
|
+
request.post? && request.params.has_key?('logoutRequest')
|
106
|
+
end
|
107
|
+
|
108
|
+
def single_sign_out_phase
|
109
|
+
logout_request_service.new(self, request).call(options)
|
110
|
+
end
|
111
|
+
|
88
112
|
# Build a CAS host with protocol and port
|
89
113
|
#
|
90
114
|
#
|
91
115
|
def cas_url
|
116
|
+
extract_url if options['url']
|
117
|
+
validate_cas_setup
|
92
118
|
@cas_url ||= begin
|
93
119
|
uri = Addressable::URI.new
|
94
|
-
uri.host
|
95
|
-
uri.scheme =
|
96
|
-
uri.port
|
97
|
-
uri.path
|
98
|
-
|
120
|
+
uri.host = options.host
|
121
|
+
uri.scheme = options.ssl ? 'https' : 'http'
|
122
|
+
uri.port = options.port
|
123
|
+
uri.path = options.path
|
99
124
|
uri.to_s
|
100
125
|
end
|
101
126
|
end
|
102
127
|
|
128
|
+
def extract_url
|
129
|
+
url = Addressable::URI.parse(options.delete('url'))
|
130
|
+
options.merge!(
|
131
|
+
'host' => url.host,
|
132
|
+
'port' => url.port,
|
133
|
+
'path' => url.path,
|
134
|
+
'ssl' => url.scheme == 'https'
|
135
|
+
)
|
136
|
+
end
|
137
|
+
|
138
|
+
def validate_cas_setup
|
139
|
+
if options.host.nil? || options.login_url.nil?
|
140
|
+
raise ArgumentError.new(":host and :login_url MUST be provided")
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
103
144
|
# Build a service-validation URL from +service+ and +ticket+.
|
104
145
|
# If +service+ has a ticket param, first remove it. URL-encode
|
105
146
|
# +service+ and add it and the +ticket+ as paraemters to the
|
@@ -110,10 +151,12 @@ module OmniAuth
|
|
110
151
|
#
|
111
152
|
# @return [String] a URL like `http://cas.mycompany.com/serviceValidate?service=...&ticket=...`
|
112
153
|
def service_validate_url(service_url, ticket)
|
113
|
-
service_url = Addressable::URI.parse(
|
154
|
+
service_url = Addressable::URI.parse(service_url)
|
114
155
|
service_url.query_values = service_url.query_values.tap { |qs| qs.delete('ticket') }
|
115
|
-
|
116
|
-
|
156
|
+
cas_url + append_params(options.service_validate_url, {
|
157
|
+
service: service_url.to_s,
|
158
|
+
ticket: ticket
|
159
|
+
})
|
117
160
|
end
|
118
161
|
|
119
162
|
# Build a CAS login URL from +service+.
|
@@ -122,7 +165,7 @@ module OmniAuth
|
|
122
165
|
#
|
123
166
|
# @return [String] a URL like `http://cas.mycompany.com/login?service=...`
|
124
167
|
def login_url(service)
|
125
|
-
cas_url + append_params(
|
168
|
+
cas_url + append_params(options.login_url, { service: service })
|
126
169
|
end
|
127
170
|
|
128
171
|
# Adds URL-escaped +parameters+ to +base+.
|
@@ -133,14 +176,28 @@ module OmniAuth
|
|
133
176
|
# @return [String] the new joined URL.
|
134
177
|
def append_params(base, params)
|
135
178
|
params = params.each { |k,v| v = Rack::Utils.escape(v) }
|
136
|
-
|
137
179
|
Addressable::URI.parse(base).tap do |base_uri|
|
138
|
-
base_uri.query_values = (base_uri.query_values || {}).merge(
|
180
|
+
base_uri.query_values = (base_uri.query_values || {}).merge(params)
|
139
181
|
end.to_s
|
140
182
|
end
|
141
183
|
|
184
|
+
# Validate the Service Ticket
|
185
|
+
# @return [Object] the validated Service Ticket
|
186
|
+
def validate_service_ticket(ticket)
|
187
|
+
ServiceTicketValidator.new(self, options, callback_url, ticket).call
|
188
|
+
end
|
189
|
+
|
142
190
|
private
|
143
191
|
|
192
|
+
def fetch_raw_info(ticket)
|
193
|
+
validator = validate_service_ticket(ticket)
|
194
|
+
ticket_user_info = validator.user_info
|
195
|
+
ticket_success_body = validator.success_body
|
196
|
+
custom_user_info = options.fetch_raw_info.call(self,
|
197
|
+
options, ticket, ticket_user_info, ticket_success_body)
|
198
|
+
self.raw_info = ticket_user_info.merge(custom_user_info)
|
199
|
+
end
|
200
|
+
|
144
201
|
# Deletes Hash pairs with `nil` values.
|
145
202
|
# From https://github.com/mkdynamic/omniauth-facebook/blob/972ed5e3456bcaed7df1f55efd7c05c216c8f48e/lib/omniauth/strategies/facebook.rb#L122-127
|
146
203
|
def prune!(hash)
|
@@ -152,13 +209,16 @@ module OmniAuth
|
|
152
209
|
|
153
210
|
def return_url
|
154
211
|
# If the request already has a `url` parameter, then it will already be appended to the callback URL.
|
155
|
-
if request.params
|
212
|
+
if request.params && request.params['url']
|
156
213
|
{}
|
157
214
|
else
|
158
|
-
{ :
|
215
|
+
{ url: request.referer }
|
159
216
|
end
|
160
217
|
end
|
161
218
|
|
219
|
+
def logout_request_service
|
220
|
+
LogoutRequest
|
221
|
+
end
|
162
222
|
end
|
163
223
|
end
|
164
224
|
end
|