stellar 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.project +18 -0
- data/.rspec +3 -0
- data/Gemfile +21 -0
- data/Gemfile.lock +75 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +18 -0
- data/Rakefile +76 -0
- data/VERSION +1 -0
- data/lib/stellar.rb +18 -0
- data/lib/stellar/auth.rb +130 -0
- data/lib/stellar/client.rb +66 -0
- data/lib/stellar/courses.rb +112 -0
- data/lib/stellar/homework.rb +242 -0
- data/lib/stellar/mitca.crt +21 -0
- data/spec/fixtures/.gitkeep +0 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/stellar/auth_spec.rb +32 -0
- data/spec/stellar/client_spec.rb +58 -0
- data/spec/stellar/courses_spec.rb +41 -0
- data/spec/stellar/homework_spec.rb +32 -0
- data/spec/stellar/homework_submissions_spec.rb +124 -0
- data/spec/support/test-credentials.rb +24 -0
- metadata +205 -0
data/.document
ADDED
data/.project
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<projectDescription>
|
3
|
+
<name>stellar</name>
|
4
|
+
<comment></comment>
|
5
|
+
<projects>
|
6
|
+
</projects>
|
7
|
+
<buildSpec>
|
8
|
+
<buildCommand>
|
9
|
+
<name>com.aptana.ide.core.unifiedBuilder</name>
|
10
|
+
<arguments>
|
11
|
+
</arguments>
|
12
|
+
</buildCommand>
|
13
|
+
</buildSpec>
|
14
|
+
<natures>
|
15
|
+
<nature>com.aptana.ruby.core.rubynature</nature>
|
16
|
+
<nature>com.aptana.projects.webnature</nature>
|
17
|
+
</natures>
|
18
|
+
</projectDescription>
|
data/.rspec
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
source :rubygems
|
2
|
+
|
3
|
+
# Add dependencies required to use your gem here.
|
4
|
+
gem 'mechanize', '>= 2.0.2'
|
5
|
+
gem 'nokogiri', '>= 1.5.0'
|
6
|
+
|
7
|
+
# Add dependencies to develop your gem here.
|
8
|
+
# Include everything needed to run rake, tests, features, etc.
|
9
|
+
group :development do
|
10
|
+
gem 'rdoc', '>= 3.9.4'
|
11
|
+
gem 'rspec', '>= 2.6.0'
|
12
|
+
gem 'yard', '>= 0.7.2'
|
13
|
+
gem 'yard-rspec', '>= 0.1'
|
14
|
+
gem 'bundler', '>= 1.0.21'
|
15
|
+
gem 'jeweler', '>= 1.6.4'
|
16
|
+
gem 'rcov', '>= 0'
|
17
|
+
gem 'ruby-debug', :platform => :mri_18
|
18
|
+
gem 'ruby-debug19', :platform => :mri_19
|
19
|
+
|
20
|
+
gem 'highline', '>= 1.6.2'
|
21
|
+
end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
archive-tar-minitar (0.5.2)
|
5
|
+
columnize (0.3.4)
|
6
|
+
diff-lcs (1.1.3)
|
7
|
+
git (1.2.5)
|
8
|
+
highline (1.6.2)
|
9
|
+
jeweler (1.6.4)
|
10
|
+
bundler (~> 1.0)
|
11
|
+
git (>= 1.2.5)
|
12
|
+
rake
|
13
|
+
json (1.6.1)
|
14
|
+
linecache (0.46)
|
15
|
+
rbx-require-relative (> 0.0.4)
|
16
|
+
linecache19 (0.5.12)
|
17
|
+
ruby_core_source (>= 0.1.4)
|
18
|
+
mechanize (2.0.2)
|
19
|
+
net-http-digest_auth (~> 1.1, >= 1.1.1)
|
20
|
+
net-http-persistent (~> 1.8)
|
21
|
+
nokogiri (~> 1.4)
|
22
|
+
webrobots (~> 0.0, >= 0.0.9)
|
23
|
+
net-http-digest_auth (1.1.1)
|
24
|
+
net-http-persistent (1.9)
|
25
|
+
nokogiri (1.5.0)
|
26
|
+
rake (0.9.2)
|
27
|
+
rbx-require-relative (0.0.5)
|
28
|
+
rcov (0.9.11)
|
29
|
+
rdoc (3.10)
|
30
|
+
json (~> 1.4)
|
31
|
+
rspec (2.6.0)
|
32
|
+
rspec-core (~> 2.6.0)
|
33
|
+
rspec-expectations (~> 2.6.0)
|
34
|
+
rspec-mocks (~> 2.6.0)
|
35
|
+
rspec-core (2.6.4)
|
36
|
+
rspec-expectations (2.6.0)
|
37
|
+
diff-lcs (~> 1.1.2)
|
38
|
+
rspec-mocks (2.6.0)
|
39
|
+
ruby-debug (0.10.4)
|
40
|
+
columnize (>= 0.1)
|
41
|
+
ruby-debug-base (~> 0.10.4.0)
|
42
|
+
ruby-debug-base (0.10.4)
|
43
|
+
linecache (>= 0.3)
|
44
|
+
ruby-debug-base19 (0.11.25)
|
45
|
+
columnize (>= 0.3.1)
|
46
|
+
linecache19 (>= 0.5.11)
|
47
|
+
ruby_core_source (>= 0.1.4)
|
48
|
+
ruby-debug19 (0.11.6)
|
49
|
+
columnize (>= 0.3.1)
|
50
|
+
linecache19 (>= 0.5.11)
|
51
|
+
ruby-debug-base19 (>= 0.11.19)
|
52
|
+
ruby_core_source (0.1.5)
|
53
|
+
archive-tar-minitar (>= 0.5.2)
|
54
|
+
webrobots (0.0.12)
|
55
|
+
nokogiri (>= 1.4.4)
|
56
|
+
yard (0.7.2)
|
57
|
+
yard-rspec (0.1)
|
58
|
+
yard
|
59
|
+
|
60
|
+
PLATFORMS
|
61
|
+
ruby
|
62
|
+
|
63
|
+
DEPENDENCIES
|
64
|
+
bundler (>= 1.0.21)
|
65
|
+
highline (>= 1.6.2)
|
66
|
+
jeweler (>= 1.6.4)
|
67
|
+
mechanize (>= 2.0.2)
|
68
|
+
nokogiri (>= 1.5.0)
|
69
|
+
rcov
|
70
|
+
rdoc (>= 3.9.4)
|
71
|
+
rspec (>= 2.6.0)
|
72
|
+
ruby-debug
|
73
|
+
ruby-debug19
|
74
|
+
yard (>= 0.7.2)
|
75
|
+
yard-rspec (>= 0.1)
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Victor Costan
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
= stellar
|
2
|
+
|
3
|
+
Automated access to MIT's Stellar data, so we don't have to put up with
|
4
|
+
Stellar's craptastic UI.
|
5
|
+
|
6
|
+
== Contributing to stellar
|
7
|
+
|
8
|
+
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
|
9
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
|
10
|
+
* Fork the project
|
11
|
+
* Start a feature/bugfix branch
|
12
|
+
* Commit and push until you are happy with your contribution
|
13
|
+
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
14
|
+
* 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 so I can cherry-pick around it.
|
15
|
+
|
16
|
+
== Copyright
|
17
|
+
|
18
|
+
Copyright (c) 2011 Victor Costan. See LICENSE.txt for further details.
|
data/Rakefile
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
begin
|
6
|
+
Bundler.setup(:default, :development)
|
7
|
+
rescue Bundler::BundlerError => e
|
8
|
+
$stderr.puts e.message
|
9
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
10
|
+
exit e.status_code
|
11
|
+
end
|
12
|
+
require 'rake'
|
13
|
+
|
14
|
+
require 'jeweler'
|
15
|
+
Jeweler::Tasks.new do |gem|
|
16
|
+
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
17
|
+
gem.name = "stellar"
|
18
|
+
gem.homepage = "http://github.com/pwnall/stellar"
|
19
|
+
gem.license = "MIT"
|
20
|
+
gem.summary = %Q{Automated access to MIT's Stellar data}
|
21
|
+
gem.description = %Q{So we don't have to put up with Stellar's craptastic ui}
|
22
|
+
gem.email = "victor@costan.us"
|
23
|
+
gem.authors = ["Victor Costan"]
|
24
|
+
# dependencies defined in Gemfile
|
25
|
+
end
|
26
|
+
Jeweler::RubygemsDotOrgTasks.new
|
27
|
+
|
28
|
+
require 'rspec/core'
|
29
|
+
require 'rspec/core/rake_task'
|
30
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
31
|
+
spec.pattern = FileList['spec/**/*_spec.rb']
|
32
|
+
end
|
33
|
+
|
34
|
+
RSpec::Core::RakeTask.new(:rcov) do |spec|
|
35
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
36
|
+
spec.rcov = true
|
37
|
+
end
|
38
|
+
|
39
|
+
task :default => :spec
|
40
|
+
|
41
|
+
require 'yard'
|
42
|
+
YARD::Rake::YardocTask.new
|
43
|
+
|
44
|
+
# Fixtures.
|
45
|
+
|
46
|
+
require 'highline/import'
|
47
|
+
require 'readline'
|
48
|
+
require 'yaml'
|
49
|
+
|
50
|
+
task :spec => :fixtures
|
51
|
+
|
52
|
+
krb_file = 'spec/fixtures/kerberos.b64'
|
53
|
+
file krb_file do
|
54
|
+
kerberos = {}
|
55
|
+
kerberos[:user] = ask('MIT Kerberos Username: ') { |q| q.echo = true }
|
56
|
+
kerberos[:pass] = ask('MIT Kerberos Password: ') { |q| q.echo = '*' }
|
57
|
+
kerberos[:mit_id] = ask('MIT ID: ') { |q| q.echo = true }
|
58
|
+
File.open(krb_file, 'w') {|f| f.write [kerberos.to_yaml].pack('m') }
|
59
|
+
end
|
60
|
+
task :fixtures => krb_file
|
61
|
+
|
62
|
+
cert_file = 'spec/fixtures/mit_cert.yml'
|
63
|
+
file cert_file => krb_file do
|
64
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), 'lib'))
|
65
|
+
require 'stellar'
|
66
|
+
|
67
|
+
kerberos = YAML.load File.read(krb_file).unpack('m').first
|
68
|
+
cert = Stellar::Auth.get_certificate kerberos
|
69
|
+
yaml = {:cert => cert[:cert].to_pem, :key => cert[:key].to_pem}.to_yaml
|
70
|
+
File.open(cert_file, 'wb') { |f| f.write yaml }
|
71
|
+
|
72
|
+
# Write the certificate in PKCS#12 format for debugging purposes.
|
73
|
+
p12 = OpenSSL::PKCS12.create(nil, 'MIT Stellar', cert[:key], cert[:cert])
|
74
|
+
File.open('spec/fixtures/mit_cert.p12', 'wb') { |f| f.write p12.to_der }
|
75
|
+
end
|
76
|
+
task :fixtures => cert_file
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
data/lib/stellar.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# Standard library.
|
2
|
+
require 'openssl'
|
3
|
+
require 'uri'
|
4
|
+
|
5
|
+
# Gems.
|
6
|
+
require 'nokogiri'
|
7
|
+
require 'mechanize'
|
8
|
+
|
9
|
+
# TODO(pwnall): documentation
|
10
|
+
module Stellar
|
11
|
+
|
12
|
+
end # namespace Stellar
|
13
|
+
|
14
|
+
# Code.
|
15
|
+
require 'stellar/auth.rb'
|
16
|
+
require 'stellar/client.rb'
|
17
|
+
require 'stellar/courses.rb'
|
18
|
+
require 'stellar/homework.rb'
|
data/lib/stellar/auth.rb
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
# :nodoc: namespace
|
4
|
+
module Stellar
|
5
|
+
|
6
|
+
# Support for authentication on MIT systems.
|
7
|
+
module Auth
|
8
|
+
# Path to the MIT CA self-signed certificate.
|
9
|
+
def mitca_path
|
10
|
+
File.join File.dirname(__FILE__), 'mitca.crt'
|
11
|
+
end
|
12
|
+
|
13
|
+
# Authenticates using some credentials, e.g. an MIT certificate.
|
14
|
+
#
|
15
|
+
# @param [Hash] options credentials to be used for authentication
|
16
|
+
# @option options [String] :cert path to MIT client certificate for the user
|
17
|
+
# @option options [Hash] :kerberos:: Kerberos credentials, encoded as a Hash
|
18
|
+
# with :user and :pass keys
|
19
|
+
# @return [Stellar::Client] self, for convenient method chaining
|
20
|
+
def auth(options = {})
|
21
|
+
# Reset any prior credentials.
|
22
|
+
@mech = mech
|
23
|
+
if options[:cert]
|
24
|
+
log = Logger.new(STDERR)
|
25
|
+
log.level = Logger::INFO
|
26
|
+
@mech.log = log
|
27
|
+
|
28
|
+
key = options[:cert][:key]
|
29
|
+
if key.respond_to?(:to_str)
|
30
|
+
if File.exist?(key)
|
31
|
+
@mech.key = key
|
32
|
+
else
|
33
|
+
@mech.key = OpenSSL::PKey::RSA.new key
|
34
|
+
end
|
35
|
+
else
|
36
|
+
@mech.key = key
|
37
|
+
end
|
38
|
+
cert = options[:cert][:cert]
|
39
|
+
if cert.respond_to?(:to_str)
|
40
|
+
if File.exist?(cert)
|
41
|
+
@mech.cert = cert
|
42
|
+
else
|
43
|
+
@mech.cert = OpenSSL::X509::Certificate.new cert
|
44
|
+
end
|
45
|
+
else
|
46
|
+
@mech.cert = cert
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Go to a page that is guaranteed to redirect to shitoleth.
|
51
|
+
step1_page = get '/atstellar'
|
52
|
+
# Fill in the form.
|
53
|
+
step1_form = step1_page.form_with :action => /WAYF/
|
54
|
+
step1_form.checkbox_with(:name => /perm/).checked = :checked
|
55
|
+
step2_page = step1_form.submit
|
56
|
+
# Click through the stupid confirmation form.
|
57
|
+
step2_form = step2_page.form_with :action => /WAYF/
|
58
|
+
cred_page = step2_form.submit
|
59
|
+
|
60
|
+
# Fill in the credentials form.
|
61
|
+
if options[:cert]
|
62
|
+
cred_form = cred_page.form_with :action => /certificate/i
|
63
|
+
cred_form.checkbox_with(:name => /pref/).checked = :checked
|
64
|
+
puts cred_form
|
65
|
+
puts cred_form.submit(cred_form.buttons.first).body
|
66
|
+
return
|
67
|
+
elsif options[:kerberos]
|
68
|
+
cred_form = cred_page.form_with :action => /username/i
|
69
|
+
cred_form.field_with(:name => /user/).value = options[:kerberos][:user]
|
70
|
+
cred_form.field_with(:name => /pass/).value = options[:kerberos][:pass]
|
71
|
+
else
|
72
|
+
raise 'Unsupported credentials'
|
73
|
+
end
|
74
|
+
|
75
|
+
# Click through the SAML response form.
|
76
|
+
saml_page = cred_form.submit
|
77
|
+
unless saml_form = saml_page.form_with(:action => /SAML/)
|
78
|
+
raise ArgumentError, 'Authentication failed due to invalid credentials'
|
79
|
+
end
|
80
|
+
saml_form.submit
|
81
|
+
|
82
|
+
self
|
83
|
+
end
|
84
|
+
end # module Stellar::Auth
|
85
|
+
|
86
|
+
# :nodoc: class methods
|
87
|
+
module Auth
|
88
|
+
class <<self
|
89
|
+
include Auth
|
90
|
+
|
91
|
+
# Obtains a certificate using a Kerberos credentials.
|
92
|
+
#
|
93
|
+
# @param [Hash] kerberos MIT Kerberos credentials
|
94
|
+
# @option kerberos [String] :user the Kerberos username (e.g. "costan")
|
95
|
+
# @option kerberos [String] :pass the Kerberos password (handle with care!)
|
96
|
+
# @option kerberos [String] :mit_id 9-character string or 9-digit number
|
97
|
+
# starting with 9 (9........)
|
98
|
+
# @option kerberos [Fixnum] :ttl certificate lifetime, in days (optional;
|
99
|
+
# defaults to 1 day)
|
100
|
+
#
|
101
|
+
# @return [Hash] a Hash with a :cert key (the OpenSSL::X509::Certificate)
|
102
|
+
# and a :key key (the matching OpenSSL::PKey::PKey private key)
|
103
|
+
def get_certificate(kerberos)
|
104
|
+
mech = Mechanize.new
|
105
|
+
mech.ca_file = mitca_path
|
106
|
+
mech.user_agent_alias = 'Linux Firefox'
|
107
|
+
login_page = mech.get 'https://ca.mit.edu/ca/'
|
108
|
+
login_form = login_page.form_with :action => /login/
|
109
|
+
login_form.field_with(:name => /login/).value = kerberos[:user]
|
110
|
+
login_form.field_with(:name => /pass/).value = kerberos[:pass]
|
111
|
+
login_form.field_with(:name => /mitid/).value = kerberos[:mit_id]
|
112
|
+
keygen_page = login_form.submit login_form.buttons.first
|
113
|
+
|
114
|
+
keygen_form = keygen_page.form_with(:action => /ca/)
|
115
|
+
if /login/ =~ keygen_form.action
|
116
|
+
raise ArgumentError, 'Invalid Kerberos credentials'
|
117
|
+
end
|
118
|
+
keygen_form.field_with(:name => /life/).value = kerberos[:ttl] || 1
|
119
|
+
key_pair = keygen_form.keygens.first.key
|
120
|
+
response_page = keygen_form.submit keygen_form.buttons.first
|
121
|
+
|
122
|
+
cert_frame = response_page.frame_with(:name => /download/)
|
123
|
+
cert_bytes = mech.get_file cert_frame.uri
|
124
|
+
cert = OpenSSL::X509::Certificate.new cert_bytes
|
125
|
+
{:key => key_pair, :cert => cert}
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end # module Stellar::Auth
|
129
|
+
|
130
|
+
end # namespace Stellar
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# :nodoc: namespace
|
2
|
+
module Stellar
|
3
|
+
|
4
|
+
# Client session for accessing the Stellar API.
|
5
|
+
class Client
|
6
|
+
include Stellar::Auth
|
7
|
+
|
8
|
+
# Client for accessing public information.
|
9
|
+
#
|
10
|
+
# Call auth to authenticate as a user and access restricted functionality.
|
11
|
+
def initialize
|
12
|
+
@mech = mech
|
13
|
+
|
14
|
+
@courses = nil
|
15
|
+
end
|
16
|
+
|
17
|
+
# New Mechanize instance.
|
18
|
+
def mech
|
19
|
+
m = Mechanize.new
|
20
|
+
m.ca_file = mitca_path
|
21
|
+
m.user_agent_alias = 'Linux Firefox'
|
22
|
+
m
|
23
|
+
end
|
24
|
+
|
25
|
+
# Fetches a page from the Stellar site.
|
26
|
+
#
|
27
|
+
# @param [String] path relative URL of the page to be fetched
|
28
|
+
# @return [Mechanize::Page] the desired page, wrapped in the Mechanize API
|
29
|
+
def get(path)
|
30
|
+
uri = URI.join('https://stellar.mit.edu', path)
|
31
|
+
page_bytes = @mech.get uri
|
32
|
+
end
|
33
|
+
|
34
|
+
# Fetches a page from the Stellar site.
|
35
|
+
#
|
36
|
+
# @param [String] path relative URL of the page to be fetched
|
37
|
+
# @return [Nokogiri::HTML::Document] the desired page, parsed with Nokogiri
|
38
|
+
def get_nokogiri(path)
|
39
|
+
uri = URI.join('https://stellar.mit.edu', path)
|
40
|
+
raw_html = @mech.get_file uri
|
41
|
+
Nokogiri.HTML raw_html, uri.to_s
|
42
|
+
end
|
43
|
+
|
44
|
+
# Fetches a file from the Stellar site.
|
45
|
+
#
|
46
|
+
# @param [String] path relative URL of the file to be fetched
|
47
|
+
# @return [String] raw contents of the file
|
48
|
+
def get_file(path)
|
49
|
+
uri = URI.join('https://stellar.mit.edu', path)
|
50
|
+
@mech.get_file uri
|
51
|
+
end
|
52
|
+
|
53
|
+
# A Stellar client specialized to answer course queries.
|
54
|
+
#
|
55
|
+
# @return [Stellar::Courses] client specialized to course queries
|
56
|
+
def courses
|
57
|
+
@courses ||= Stellar::Courses.new self
|
58
|
+
end
|
59
|
+
|
60
|
+
# (see Stellar::Course#for)
|
61
|
+
def course(number, year, semester)
|
62
|
+
Stellar::Course.for number, year, semester, self
|
63
|
+
end
|
64
|
+
end # class Stellar::Client
|
65
|
+
|
66
|
+
end # namespace Stellar
|