autodiscover 0.1.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/.travis.yml +6 -0
- data/Gemfile +4 -0
- data/README.md +32 -31
- data/Rakefile +19 -0
- data/autodiscover.gemspec +30 -0
- data/lib/autodiscover.rb +12 -25
- data/lib/autodiscover/client.rb +22 -215
- data/lib/autodiscover/errors.rb +5 -0
- data/lib/autodiscover/pox_request.rb +72 -0
- data/lib/autodiscover/pox_response.rb +32 -0
- data/lib/autodiscover/server_version_parser.rb +45 -0
- data/lib/autodiscover/version.rb +3 -0
- data/test/fixtures/pox_response.xml +93 -0
- data/test/test_helper.rb +12 -0
- data/test/units/client_test.rb +35 -0
- data/test/units/pox_request_test.rb +46 -0
- data/test/units/pox_response_test.rb +54 -0
- data/test/units/server_version_parser_test.rb +18 -0
- metadata +154 -81
- data/lib/autodiscover/credentials.rb +0 -51
- data/lib/autodiscover/services.rb +0 -40
- data/test/unit_test.rb +0 -195
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 67ca31043ee9ef53aebe329e4a07d65b0d0fb36a
|
4
|
+
data.tar.gz: 1bc1b1a2063a628051f8cb218b983db4088fc41e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c643328d246d2d51e3882af159b2c3fabd27854441deb673b5bde042542faf352901b17a96a7c266becf63cb4748730393d16e859772dbce0db038dab59f50bc
|
7
|
+
data.tar.gz: 22413ce0ba0e22beefccec5409ac183ac8244d70567a601128bdab8831bd652e2eb2f98cc9900f967de6b8e6a211c309349ea7ed9d33eb2a5e6caeb2a2ab91db
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/README.md
CHANGED
@@ -1,9 +1,11 @@
|
|
1
|
+
[![Build Status](https://travis-ci.org/WinRb/autodiscover.svg?branch=master)](https://travis-ci.org/WinRb/autodiscover)
|
2
|
+
|
1
3
|
Autodiscover
|
2
4
|
============
|
3
5
|
|
4
6
|
Ruby client for Microsoft's Autodiscover Service.
|
5
7
|
|
6
|
-
The Autodiscover Service is a component of the
|
8
|
+
The Autodiscover Service is a component of the Microsoft Exchange architecture. Autoservice clients can access the URLs and settings needed to communicate with Exchange servers, such as the URL of the endpoint to use with the Exchange Web Services (EWS) API.
|
7
9
|
|
8
10
|
This library implements Microsoft's "Autodiscover HTTP Service Protocol Specification" to discover the endpoint for an Autodiscover server that supports a specified e-mail address and Microsoft's "Autodiscover Publishing and Lookup Protocol Specification" to get URLs and settings that are required to access Web services available from Exchange servers.
|
9
11
|
|
@@ -14,53 +16,58 @@ This library requires the following Gems:
|
|
14
16
|
|
15
17
|
* HTTPClient
|
16
18
|
* Nokogiri
|
19
|
+
* Nori
|
17
20
|
|
18
21
|
The HTTPClient Gem in turn requires the rubyntlm Gem for Negotiate/NTLM authentication.
|
19
22
|
|
20
|
-
For unit testing the webmock Gem is also used.
|
21
|
-
|
22
23
|
How to Use
|
23
24
|
----------
|
24
25
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
26
|
+
```ruby
|
27
|
+
require 'autodiscover'
|
28
|
+
|
29
|
+
client = Autodiscover::Client.new(email: "blumbergh@initech.local", password: "tps_eq_awesome")
|
30
|
+
data = client.autodiscover
|
31
|
+
|
32
|
+
# Get the EWS endpoint
|
33
|
+
data.ews_url
|
34
|
+
|
35
|
+
# Get an Exchange Version ingestible by EWS
|
36
|
+
data.exchange_version
|
37
|
+
|
38
|
+
# Access the raw Autodiscover data in its entirety
|
39
|
+
data.response
|
40
|
+
```
|
32
41
|
|
33
42
|
Options
|
34
43
|
-------
|
35
44
|
|
36
|
-
|
37
|
-
|
38
|
-
For debugging, we extend the use of the debug_dev option in the HTTPClient library.
|
45
|
+
Besides `:email` and `:password`, `Autodiscover::Client` can take a few other options as can the #autodiscover method.
|
39
46
|
|
40
|
-
|
41
|
-
credentials = Autodiscover::Credentials.new('<e-mail address>', '<password>')
|
42
|
-
client = Autodiscover::Client.new(:debug_dev => debug_file)
|
43
|
-
services = client.get_services(credentials)
|
44
|
-
debug_file.close
|
47
|
+
Examples:
|
45
48
|
|
46
|
-
|
49
|
+
```ruby
|
50
|
+
# Use a different username than your e-mail.
|
51
|
+
client = Autodiscover::Client.new(email: "blumbergh@initech.local", password: "tps_eq_awesome", username: 'INITECH\blumbergh')
|
47
52
|
|
48
|
-
|
53
|
+
# Override the domain
|
54
|
+
client = Autodiscover::Client.new(email: "blumbergh@initech.local", password: "tps_eq_awesome", domain: "tpsreports.local")
|
49
55
|
|
50
|
-
|
56
|
+
# Ignore SSL Errors
|
57
|
+
client.autodiscover(ignore_ssl_errors: true)
|
58
|
+
```
|
51
59
|
|
52
|
-
The units are seconds.
|
53
60
|
|
54
61
|
Installation
|
55
62
|
------------
|
56
63
|
|
57
64
|
### Configuring a Rails App to use the latest GitHub master version
|
58
65
|
|
59
|
-
gem 'autodiscover', :git => 'git://github.com/
|
66
|
+
gem 'autodiscover', :git => 'git://github.com/WinRb/autodiscover.git'
|
60
67
|
|
61
68
|
### To install the latest development version from the GitHub master
|
62
69
|
|
63
|
-
git clone http://github.com/
|
70
|
+
git clone http://github.com/WinRb/autodiscover.git
|
64
71
|
cd autodiscover
|
65
72
|
gem build autodiscover.gemspec
|
66
73
|
sudo gem install autodiscover-<version>.gem
|
@@ -71,11 +78,5 @@ Bugs and Issues
|
|
71
78
|
Limitations:
|
72
79
|
|
73
80
|
* Doesn't support querying the DNS for SRV Records
|
74
|
-
* Only returns the TTL and EWS_Url values from the EXPR Protocol response
|
75
|
-
|
76
|
-
Please submit additional bugs and issues here [http://github.com/wimm/autodiscover/issues](http://github.com/wimm/autodiscover/issues)
|
77
|
-
|
78
|
-
Copyright
|
79
|
-
---------
|
80
81
|
|
81
|
-
|
82
|
+
Please submit additional bugs and issues here [http://github.com/WinRb/autodiscover/issues](http://github.com/WinRb/autodiscover/issues)
|
data/Rakefile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require "rake/testtask"
|
3
|
+
|
4
|
+
task :default => :test
|
5
|
+
|
6
|
+
Rake::TestTask.new(:test) do |t|
|
7
|
+
t.libs << 'lib'
|
8
|
+
t.libs << 'test'
|
9
|
+
t.pattern = 'test/**/*_test.rb'
|
10
|
+
t.verbose = false
|
11
|
+
end
|
12
|
+
|
13
|
+
desc "Open a Pry console for this library"
|
14
|
+
task :console do
|
15
|
+
require "pry"
|
16
|
+
require "autodiscover"
|
17
|
+
ARGV.clear
|
18
|
+
Pry.start
|
19
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'autodiscover/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = 'autodiscover'
|
8
|
+
s.version = Autodiscover::VERSION
|
9
|
+
s.summary = "Ruby client for Microsoft's Autodiscover Service"
|
10
|
+
s.description = "The Autodiscover Service provides information about a Microsoft Exchange environment such as service URLs, versions and many other attributes."
|
11
|
+
s.required_ruby_version = '>= 2.1.0'
|
12
|
+
|
13
|
+
s.authors = ["David King", "Dan Wanek"]
|
14
|
+
s.email = ["dking@bestinclass.com", "dan.wanek@gmail.com"]
|
15
|
+
s.homepage = 'http://github.com/WinRb/autodiscover'
|
16
|
+
|
17
|
+
s.files = `git ls-files -z`.split("\x0")
|
18
|
+
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_runtime_dependency "nokogiri"
|
22
|
+
s.add_runtime_dependency "nori"
|
23
|
+
s.add_runtime_dependency "httpclient"
|
24
|
+
|
25
|
+
s.add_development_dependency "minitest", "~> 5.6.0"
|
26
|
+
s.add_development_dependency "mocha", "~> 1.1.0"
|
27
|
+
s.add_development_dependency "bundler"
|
28
|
+
s.add_development_dependency "rake"
|
29
|
+
s.add_development_dependency "pry"
|
30
|
+
end
|
data/lib/autodiscover.rb
CHANGED
@@ -1,26 +1,13 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
# a copy of this software and associated documentation files (the
|
6
|
-
# "Software"), to deal in the Software without restriction, including
|
7
|
-
# without limitation the rights to use, copy, modify, merge, publish,
|
8
|
-
# distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
-
# permit persons to whom the Software is furnished to do so, subject to
|
10
|
-
# the following conditions:
|
11
|
-
#
|
12
|
-
# The above copyright notice and this permission notice shall be
|
13
|
-
# included in all copies or substantial portions of the Software.
|
14
|
-
#
|
15
|
-
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
-
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
-
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
-
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
19
|
-
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
20
|
-
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
|
-
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
-
#++
|
1
|
+
require "autodiscover/version"
|
2
|
+
require "nokogiri"
|
3
|
+
require "nori"
|
4
|
+
require "httpclient"
|
23
5
|
|
24
|
-
|
25
|
-
|
26
|
-
|
6
|
+
module Autodiscover
|
7
|
+
end
|
8
|
+
|
9
|
+
require "autodiscover/errors"
|
10
|
+
require "autodiscover/client"
|
11
|
+
require "autodiscover/pox_request"
|
12
|
+
require "autodiscover/pox_response"
|
13
|
+
require "autodiscover/server_version_parser"
|
data/lib/autodiscover/client.rb
CHANGED
@@ -1,226 +1,33 @@
|
|
1
|
-
#--
|
2
|
-
# Copyright (c) 2010-2011 WIMM Labs, Inc.
|
3
|
-
#
|
4
|
-
# Permission is hereby granted, free of charge, to any person obtaining
|
5
|
-
# a copy of this software and associated documentation files (the
|
6
|
-
# "Software"), to deal in the Software without restriction, including
|
7
|
-
# without limitation the rights to use, copy, modify, merge, publish,
|
8
|
-
# distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
-
# permit persons to whom the Software is furnished to do so, subject to
|
10
|
-
# the following conditions:
|
11
|
-
#
|
12
|
-
# The above copyright notice and this permission notice shall be
|
13
|
-
# included in all copies or substantial portions of the Software.
|
14
|
-
#
|
15
|
-
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
-
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
-
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
-
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
19
|
-
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
20
|
-
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
|
-
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
-
#++
|
23
|
-
|
24
|
-
require 'httpclient'
|
25
|
-
require 'nokogiri'
|
26
|
-
|
27
1
|
module Autodiscover
|
28
|
-
REDIRECT_LIMIT = 10 # attempts
|
29
|
-
CONNECT_TIMEOUT_DEFAULT = 10 # seconds
|
30
|
-
|
31
|
-
# Client objects are used to make queries to the autodiscover server, to
|
32
|
-
# specify configuration values, and to maintain state between requests.
|
33
2
|
class Client
|
34
|
-
# Creates a Client object.
|
35
|
-
#
|
36
|
-
# The following options can be specified:
|
37
|
-
#
|
38
|
-
# <tt>:connect_timeout</tt>:: Number of seconds to wait when trying to establish
|
39
|
-
# a connection. The default value is 10 seconds.
|
40
|
-
# <tt>:debug_dev</tt>:: Device that debug messages and all HTTP
|
41
|
-
# requests and responses are dumped to. The debug
|
42
|
-
# device must respond to <tt><<</tt> for dump.
|
43
|
-
def initialize(options={})
|
44
|
-
@debug_dev = options[:debug_dev]
|
45
3
|
|
4
|
+
attr_reader :domain, :email, :http
|
5
|
+
|
6
|
+
# @param email [String] An e-mail to use for autodiscovery. It will be
|
7
|
+
# used as the default username.
|
8
|
+
# @param password [String]
|
9
|
+
# @param username [String] An optional username if you want to authenticate
|
10
|
+
# with something other than the e-mail. For instance DOMAIN\user
|
11
|
+
# @param domain [String] An optional domain to provide as an override for
|
12
|
+
# the one parsed from the e-mail.
|
13
|
+
def initialize(email:, password:, username: nil, domain: nil)
|
14
|
+
@email = email
|
15
|
+
@domain = domain || @email.split("@").last
|
46
16
|
@http = HTTPClient.new
|
47
|
-
@
|
48
|
-
@http.
|
49
|
-
|
50
|
-
@redirect_count = 0
|
51
|
-
end
|
52
|
-
|
53
|
-
# Get a Services object from an \Autodiscover server that is
|
54
|
-
# available on an authenticated endpoint determined by the
|
55
|
-
# specified Credentials object.
|
56
|
-
def get_services(credentials, reset_redirect_count=true)
|
57
|
-
@redirect_count = 0 if reset_redirect_count
|
58
|
-
|
59
|
-
req_body = build_request_body credentials.email
|
60
|
-
|
61
|
-
try_standard_secure_urls(credentials, req_body) ||
|
62
|
-
try_standard_redirection_url(credentials, req_body) ||
|
63
|
-
try_dns_serv_record
|
64
|
-
end
|
65
|
-
|
66
|
-
private
|
67
|
-
|
68
|
-
def try_standard_secure_urls(credentials, req_body)
|
69
|
-
response = nil
|
70
|
-
[ "https://#{credentials.smtp_domain}/autodiscover/autodiscover.xml",
|
71
|
-
"https://autodiscover.#{credentials.smtp_domain}/autodiscover/autodiscover.xml"
|
72
|
-
].each do |url|
|
73
|
-
@debug_dev << "AUTODISCOVER: trying #{url}\n" if @debug_dev
|
74
|
-
response = try_secure_url(url, credentials, req_body)
|
75
|
-
break if response
|
76
|
-
end
|
77
|
-
response
|
17
|
+
@username = username || email
|
18
|
+
@http.set_auth(nil, @username, password)
|
78
19
|
end
|
79
20
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
if response.status_code == 302
|
87
|
-
try_redirect_url(response.header['Location'].first, credentials, req_body)
|
21
|
+
# @param type [Symbol] The type of response. Right now this is just :pox
|
22
|
+
# @param [Hash] **options
|
23
|
+
def autodiscover(type: :pox, **options)
|
24
|
+
case type
|
25
|
+
when :pox
|
26
|
+
PoxRequest.new(self, **options).autodiscover
|
88
27
|
else
|
89
|
-
|
28
|
+
raise Autodiscover::ArgumentError, "Not a valid autodiscover type (#{type})."
|
90
29
|
end
|
91
30
|
end
|
92
31
|
|
93
|
-
def try_secure_url(url, credentials, req_body)
|
94
|
-
@http.set_auth(url, credentials.email, credentials.password)
|
95
|
-
|
96
|
-
response = @http.post(url, req_body, {'Content-Type' => 'text/xml; charset=utf-8'}) rescue nil
|
97
|
-
return nil unless response
|
98
|
-
|
99
|
-
if response.status_code == 302
|
100
|
-
try_redirect_url(response.header['Location'].first, credentials, req_body)
|
101
|
-
elsif HTTP::Status.successful?(response.status_code)
|
102
|
-
result = parse_response(response.content)
|
103
|
-
case result
|
104
|
-
when Autodiscover::Services
|
105
|
-
return result
|
106
|
-
when Autodiscover::RedirectUrl
|
107
|
-
try_redirect_url(result.url, credentials, req_body)
|
108
|
-
when Autodiscover::RedirectAddress
|
109
|
-
begin
|
110
|
-
credentials.email = result.address
|
111
|
-
rescue ArgumentError
|
112
|
-
# An invalid email address was returned
|
113
|
-
return nil
|
114
|
-
end
|
115
|
-
|
116
|
-
try_redirect_addr(credentials)
|
117
|
-
end
|
118
|
-
else
|
119
|
-
nil
|
120
|
-
end
|
121
|
-
end
|
122
|
-
|
123
|
-
def try_redirect_url(url, credentials, req_body)
|
124
|
-
@redirect_count += 1
|
125
|
-
return nil if @redirect_count > REDIRECT_LIMIT
|
126
|
-
|
127
|
-
# Only permit redirects to secure addresses
|
128
|
-
return nil unless url =~ /^https:/i
|
129
|
-
try_secure_url(url, credentials, req_body)
|
130
|
-
end
|
131
|
-
|
132
|
-
def try_redirect_addr(credentials)
|
133
|
-
@redirect_count += 1
|
134
|
-
return nil if @redirect_count > REDIRECT_LIMIT
|
135
|
-
|
136
|
-
get_services(credentials, false)
|
137
|
-
end
|
138
|
-
|
139
|
-
def try_dns_serv_record
|
140
|
-
nil
|
141
|
-
end
|
142
|
-
|
143
|
-
def build_request_body(email)
|
144
|
-
Nokogiri::XML::Builder.new do |xml|
|
145
|
-
xml.Autodiscover('xmlns' => 'http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006') {
|
146
|
-
xml.Request {
|
147
|
-
xml.EMailAddress email
|
148
|
-
xml.AcceptableResponseSchema 'http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a'
|
149
|
-
}
|
150
|
-
}
|
151
|
-
end.to_xml
|
152
|
-
end
|
153
|
-
|
154
|
-
NAMESPACES = {
|
155
|
-
'a' => 'http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006',
|
156
|
-
'o' => 'http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a'
|
157
|
-
} #:nodoc:
|
158
|
-
|
159
|
-
def parse_response(body)
|
160
|
-
doc = parse_xml body
|
161
|
-
return nil unless doc
|
162
|
-
|
163
|
-
# The response must include an Account element. Return an error if not found.
|
164
|
-
account_e = doc.at_xpath('a:Autodiscover/o:Response/o:Account', NAMESPACES)
|
165
|
-
return nil unless account_e
|
166
|
-
|
167
|
-
# The response must include an Action element. Return an error if not found.
|
168
|
-
action_e = account_e.at_xpath('o:Action', NAMESPACES)
|
169
|
-
return nil unless action_e
|
170
|
-
|
171
|
-
case action_e.content
|
172
|
-
when /^settings$/i
|
173
|
-
# Response contains configuration settings in <Protocol> elements
|
174
|
-
# Only care about about "EXPR" type protocol configuration values
|
175
|
-
# for accessing Exchange services outside of the firewall
|
176
|
-
settings = {}
|
177
|
-
if protocol_e = account_e.at_xpath('o:Protocol[o:Type="EXPR"]', NAMESPACES)
|
178
|
-
# URL for the Web services virtual directory.
|
179
|
-
ews_url_e = protocol_e.at_xpath('o:EwsUrl', NAMESPACES)
|
180
|
-
settings['ews_url'] = ews_url_e.content if ews_url_e
|
181
|
-
# Time to Live (TTL) in hours. Default is 1 hour if no element is
|
182
|
-
# returned.
|
183
|
-
ttl_e = protocol_e.at_xpath('o:TTL', NAMESPACES)
|
184
|
-
settings['ttl'] = ttl_e ? ttl_e.content : 1
|
185
|
-
end
|
186
|
-
Autodiscover::Services.new(settings)
|
187
|
-
when /^redirectAddr$/i
|
188
|
-
# Response contains a new address that must be used to re-Autodiscover
|
189
|
-
redirect_addr_e = account_e.at_xpath('o:RedirectAddr', NAMESPACES)
|
190
|
-
address = redirect_addr_e ? redirect_addr_e.content : nil
|
191
|
-
return nil unless address
|
192
|
-
Autodiscover::RedirectAddress.new(address)
|
193
|
-
when /^redirectUrl$/i
|
194
|
-
# Response contains a new URL that must be used to re-Autodiscover
|
195
|
-
redirect_url_e = account_e.at_xpath('o:RedirectUrl', NAMESPACES)
|
196
|
-
url = redirect_url_e ? redirect_url_e.content : nil
|
197
|
-
return nil unless url
|
198
|
-
Autodiscover::RedirectUrl.new(url)
|
199
|
-
else
|
200
|
-
nil
|
201
|
-
end
|
202
|
-
end
|
203
|
-
|
204
|
-
def parse_xml(doc)
|
205
|
-
Nokogiri::XML(doc) { |c| c.options = Nokogiri::XML::ParseOptions::STRICT }
|
206
|
-
rescue Nokogiri::XML::SyntaxError
|
207
|
-
nil
|
208
|
-
end
|
209
|
-
end
|
210
|
-
|
211
|
-
class RedirectUrl #:nodoc: all
|
212
|
-
attr_reader :url
|
213
|
-
|
214
|
-
def initialize(url)
|
215
|
-
@url = url
|
216
|
-
end
|
217
|
-
end
|
218
|
-
|
219
|
-
class RedirectAddress #:nodoc: all
|
220
|
-
attr_reader :address
|
221
|
-
|
222
|
-
def initialize(address)
|
223
|
-
@address = address
|
224
|
-
end
|
225
32
|
end
|
226
|
-
end
|
33
|
+
end
|