This really does like to yell at me.

This commit is contained in:
Toastie 2024-11-23 17:12:14 +13:00
parent 61c006118a
commit f77f773b60
Signed by: toastie_t0ast
GPG key ID: 27F3B6855AFD40A4
18 changed files with 414 additions and 125 deletions

11
.gitignore vendored
View file

@ -1,8 +1,19 @@
/.bundle/
/.yardoc
/Gemfile.lock
/_yardoc/
/coverage/
/doc/
/pkg/
/spec/reports/
/tmp/
/.ruby-version
/.ruby-gemset
/.rvm
*.bundle
*.so
*.o
*.a
mkmf.log
scratch.rb
*.sw[op]

15
.idea/autodiscover.iml generated
View file

@ -1,15 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="RUBY_MODULE" version="4">
<component name="ModuleRunConfigurationManager">
<shared />
</component>
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/features" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/spec" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
</content>
<orderEntry type="jdk" jdkName="ruby-3.2.3-p157" jdkType="RUBY_SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

10
Gemfile
View file

@ -1,10 +1,4 @@
# frozen_string_literal: true
source "https://rubygems.org"
source 'https://rubygems.org'
# Specify your gem's dependencies in autodiscover.gemspec
gemspec
gem "rake", "~> 13.0"
gem "minitest", "~> 5.16"
gemspec

View file

@ -1,8 +1,19 @@
# frozen_string_literal: true
require "bundler/gem_tasks"
require "minitest/test_task"
require "rake/testtask"
Minitest::TestTask.create
task :default => :test
task default: :test
Rake::TestTask.new(:test) do |t|
t.libs << 'lib'
t.libs << 'test'
t.pattern = 'test/**/*_test.rb'
t.verbose = false
end
desc "Open a Pry console for this library"
task :console do
require "pry"
require "autodiscover"
ARGV.clear
Pry.start
end

View file

@ -1,40 +1,32 @@
# frozen_string_literal: true
# coding: utf-8
lib = File.expand_path('../lib', __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'autodiscover/version'
require_relative "lib/autodiscover/version"
Gem::Specification.new do |s|
s.name = 'autodiscover'
s.version = Autodiscover::VERSION
s.license = 'MIT'
s.summary = "Ruby client for Microsoft's Autodiscover Service"
s.description = "The Autodiscover Service provides information about a Microsoft Exchange environment such as service URLs, versions and many other attributes."
s.required_ruby_version = '>= 2.1.0'
Gem::Specification.new do |spec|
spec.name = "autodiscover"
spec.version = Autodiscover::VERSION
spec.authors = ["Toastie"]
spec.email = ["toastie@toastiet0ast.com"]
s.authors = ["David King", "Dan Wanek"]
s.email = ["dking@bestinclass.com", "dan.wanek@gmail.com"]
s.homepage = 'http://github.com/WinRb/autodiscover'
spec.summary = "TODO: Write a short summary, because RubyGems requires one."
spec.description = "TODO: Write a longer description or delete this line."
spec.homepage = "TODO: Put your gem's website or public repo URL here."
spec.license = "MIT"
spec.required_ruby_version = ">= 3.0.0"
s.files = `git ls-files -z`.split("\x0")
s.test_files = s.files.grep(%r{^(test|spec|features)/})
s.require_paths = ["lib"]
spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'"
s.add_runtime_dependency "nokogiri"
s.add_runtime_dependency "nori"
s.add_runtime_dependency "httpclient"
s.add_runtime_dependency "logging"
spec.metadata["homepage_uri"] = spec.homepage
spec.metadata["source_code_uri"] = "TODO: Put your gem's public repo URL here."
spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
# Specify which files should be added to the gem when it is released.
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
spec.files = Dir.chdir(__dir__) do
`git ls-files -z`.split("\x0").reject do |f|
(File.expand_path(f) == __FILE__) ||
f.start_with?(*%w[bin/ test/ spec/ features/ .git appveyor Gemfile])
end
end
spec.bindir = "exe"
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]
# Uncomment to register a new dependency of your gem
# spec.add_dependency "example-gem", "~> 1.0"
# For more information and examples about making a new gem, check out our
# guide at: https://bundler.io/guides/creating_gem.html
end
s.add_development_dependency "minitest", "~> 5.6.0"
s.add_development_dependency "mocha", "~> 1.1.0"
s.add_development_dependency "bundler"
s.add_development_dependency "rake"
s.add_development_dependency "pry"
end

View file

@ -1,8 +1,23 @@
# frozen_string_literal: true
require_relative "autodiscover/version"
require "autodiscover/version"
require "nokogiri"
require "nori"
require "httpclient"
require "logging"
module Autodiscover
class Error < StandardError; end
# Your code goes here...
Logging.logger["Autodiscover"].level = :info
def self.Logger
Logging.logger["Autodiscover"]
end
def logger
@logger ||= Logging.logger[self.class.name]
end
end
require "autodiscover/errors"
require "autodiscover/client"
require "autodiscover/pox_request"
require "autodiscover/pox_response"
require "autodiscover/server_version_parser"

View file

@ -1,4 +1,34 @@
# frozen_string_literal: true
module Autodiscover
class Client
DEFAULT_HTTP_TIMEOUT = 10
attr_accessor :domain, :email, :http
class Client
end
# @param email [String] An e-mail to use for autodiscovery. It will be
# used as the default username.
# @param password [String]
# @param username [String] An optional username if you want to authenticate
# with something other than the e-mail. For instance DOMAIN\user
# @param domain [String] An optional domain to provide as an override for
# the one parsed from the e-mail.
def initialize(email:, password:, username: nil, domain: nil, connect_timeout: DEFAULT_HTTP_TIMEOUT)
@email = email
@domain = domain || @email.split('@').last
@http = HTTPClient.new
@http.connect_timeout = connect_timeout if connect_timeout
@username = username || @email
@http.set_auth(nil, @username, password)
end
# @param type [Symbol] The type of response. Right now this is just :pox
# @param [Hash] **options
def autodiscover(type: :pox, **options)
case type
when :pox
PoxRequest.new(self, **options).autodiscover
else
raise Autodiscover::ArgumentError, "Not a valid autodiscover type (#{type})."
end
end
end
end

View file

@ -1,2 +1,4 @@
# frozen_string_literal: true
module Autodiscover
Logging.logger["Autodiscover"].level = :debug
Logging.logger["Autodiscover"].appenders = Logging.appenders.stdout
end

View file

@ -0,0 +1,5 @@
module Autodiscover
class Error < ::StandardError; end
class ArgumentError < Error; end
end

View file

@ -1,4 +1,77 @@
# frozen_string_literal: true
module Autodiscover
class PoxRequest
include Autodiscover
module PoxRequest
end
attr_reader :client, :options
# @param client [Autodiscover::Client]
# @param [Hash] **options
# @option **options [Boolean] :ignore_ssl_errors Whether to keep trying if
# there are SSL errors
def initialize(client, **options)
@client = client
@options = options
end
# @return [Autodiscover::PoxResponse, nil]
def autodiscover
available_urls.each do |url|
response = client.http.post(url, request_body, {'Content-Type' => 'text/xml; charset=utf-8'})
return PoxResponse.new(response.body) if good_response?(response)
end
end
private
def good_response?(response)
response.status == 200
end
def available_urls(&block)
return to_enum(__method__) unless block_given?
formatted_https_urls.each {|url|
logger.debug "Yielding HTTPS Url #{url}"
handle_allowed_errors do
yield url
end
}
handle_allowed_errors do
logger.debug "Yielding HTTP Redirected Url #{redirected_http_url}"
yield redirected_http_url
end
end
def formatted_https_urls
@formatted_urls ||= %W{
https://#{client.domain}/autodiscover/autodiscover.xml
https://autodiscover.#{client.domain}/autodiscover/autodiscover.xml
}
end
def redirected_http_url
@redirected_http_url ||=
begin
response = client.http.get("http://autodiscover.#{client.domain}/autodiscover/autodiscover.xml")
(response.status == 302) ? response.headers["Location"] : nil
end
end
def request_body
Nokogiri::XML::Builder.new do |xml|
xml.Autodiscover('xmlns' => 'http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006') {
xml.Request {
xml.EMailAddress client.email
xml.AcceptableResponseSchema 'http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a'
}
}
end.to_xml
end
def handle_allowed_errors
yield
rescue SocketError, Errno::EHOSTUNREACH, Errno::ENETUNREACH, Errno::ECONNREFUSED, HTTPClient::ConnectTimeoutError
rescue OpenSSL::SSL::SSLError
raise if !options[:ignore_ssl_errors]
end
end
end

View file

@ -1,4 +1,32 @@
# frozen_string_literal: true
module Autodiscover
class PoxResponse
class PoxResponse
end
attr_reader :response
def initialize(response)
raise ArgumentError, "Response must be an XML string" if(response.nil? || response.empty?)
@response = Nori.new(parser: :nokogiri).parse(response)["Autodiscover"]["Response"]
end
def exchange_version
ServerVersionParser.new(exch_proto["ServerVersion"]).exchange_version
end
def ews_url
expr_proto["EwsUrl"]
end
def exch_proto
@exch_proto ||= (response["Account"]["Protocol"].select{|p| p["Type"] == "EXCH"}.first || {})
end
def expr_proto
@expr_proto ||= (response["Account"]["Protocol"].select{|p| p["Type"] == "EXPR"}.first || {})
end
def web_proto
@web_proto ||= (response["Account"]["Protocol"].select{|p| p["Type"] == "WEB"}.first || {})
end
end
end

View file

@ -1,4 +1,45 @@
# frozen_string_literal: true
module Autodiscover
class ServerVersionParser
class ServerVersionParser
end
VERSIONS = {
8 => {
0 => "Exchange2007",
1 => "Exchange2007_SP1",
2 => "Exchange2007_SP1",
3 => "Exchange2007_SP1",
},
14 => {
0 => "Exchange2010",
1 => "Exchange2010_SP1",
2 => "Exchange2010_SP2",
3 => "Exchange2010_SP2",
},
15 => {
0 => "Exchange2013",
1 => "Exchange2013_SP1",
}
}
def initialize(hexversion)
@version = hexversion.hex.to_s(2).rjust(hexversion.size*4, '0')
end
def major
@version[4..9].to_i(2)
end
def minor
@version[10..15].to_i(2)
end
def build
@version[17..31].to_i(2)
end
def exchange_version
v = VERSIONS[major][minor]
v.nil? ? VERIONS[8][0] : v
end
end
end

View file

@ -1,5 +1,3 @@
# frozen_string_literal: true
module Autodiscover
VERSION = "0.1.0"
VERSION = "1.0.2"
end

View file

@ -1,6 +1,12 @@
# frozen_string_literal: true
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
require "autodiscover"
require File.expand_path('../../lib/autodiscover.rb', __FILE__)
require 'minitest/autorun'
require "minitest/autorun"
require "mocha/mini_test"
TEST_DIR = File.dirname(__FILE__)
class MiniTest::Spec
def load_sample(name)
File.read("#{TEST_DIR}/fixtures/#{name}")
end
end

View file

@ -1,17 +1,35 @@
# frozen_string_literal: true
require "test_helper"
require 'minitest/autorun'
describe Autodiscover::Client do
let(:_class) { Autodiscover::Client }
class ClientTest < Minitest::Test
def setup
# Do nothing
describe "#initialize" do
it "sets a username and domain from the email" do
inst = _class.new(email: "test@example.local", password: "test")
_(inst.domain).must_equal "example.local"
_(inst.instance_variable_get(:@username)).must_equal "test@example.local"
end
it "allows you to override the username and domain" do
inst = _class.new(email: "test@example.local", password: "test", username: 'DOMAIN\test', domain: "otherexample.local")
_(inst.domain).must_equal "otherexample.local"
_(inst.instance_variable_get(:@username)).must_equal 'DOMAIN\test'
end
end
def teardown
# Do nothing
describe "#autodiscover" do
it "dispatches autodiscover to a PoxRequest instance" do
inst = _class.new(email: "test@example.local", password: "test")
pox_request = mock("pox")
pox_request.expects(:autodiscover)
Autodiscover::PoxRequest.expects(:new).with(inst,{}).returns(pox_request)
inst.autodiscover
end
it "raises an exception if an invalid autodiscover type is passed" do
inst = _class.new(email: "test@example.local", password: "test")
->{ inst.autodiscover(type: :invalid) }.must_raise(Autodiscover::ArgumentError)
end
end
def test
skip 'Not implemented'
end
end
end

View file

@ -1,4 +1,46 @@
# frozen_string_literal: true
require "test_helper"
require "ostruct"
class PoxRequestTest
end
describe Autodiscover::PoxRequest do
let(:_class) {Autodiscover::PoxRequest }
let(:http) { mock("http") }
let(:client) { OpenStruct.new({http: http, domain: "example.local", email: "test@example.local"}) }
describe "#autodiscover" do
it "returns a PoxResponse if the autodiscover is successful" do
request_body = <<-EOF.gsub(/^ /,"")
<?xml version="1.0"?>
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006">
<Request>
<EMailAddress>test@example.local</EMailAddress>
<AcceptableResponseSchema>http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a</AcceptableResponseSchema>
</Request>
</Autodiscover>
EOF
http.expects(:post).with(
"https://example.local/autodiscover/autodiscover.xml", request_body,
{'Content-Type' => 'text/xml; charset=utf-8'}
).returns(OpenStruct.new({status: 200, body: "<Autodiscover><Response><test></test></Response></Autodiscover>"}))
inst = _class.new(client)
_(inst.autodiscover).must_be_instance_of(Autodiscover::PoxResponse)
end
it "will fail if :ignore_ssl_errors is not true" do
http.expects(:post).raises(OpenSSL::SSL::SSLError, "Test Error")
inst = _class.new(client)
-> {inst.autodiscover}.must_raise(OpenSSL::SSL::SSLError)
end
it "keeps trying if :ignore_ssl_errors is set" do
http.expects(:get).once.returns(OpenStruct.new({headers: {"Location" => "http://example.local"}, status: 302}))
http.expects(:post).times(3).
raises(OpenSSL::SSL::SSLError, "Test Error").then.
raises(OpenSSL::SSL::SSLError, "Test Error").then.
raises(Errno::ENETUNREACH, "Test Error")
inst = _class.new(client, ignore_ssl_errors: true)
_(inst.autodiscover).must_be_nil
end
end
end

View file

@ -1,17 +1,54 @@
# frozen_string_literal: true
require "test_helper"
require 'minitest/autorun'
describe Autodiscover::PoxResponse do
let(:_class) {Autodiscover::PoxResponse }
let(:response) { load_sample("pox_response.xml") }
class PoxResponseTest < Minitest::Test
def setup
# Do nothing
describe "#initialize" do
it "parses an XML string into a Hash when initialized" do
inst = _class.new response
_(inst.response).must_be_instance_of Hash
end
it "it raises an exception if the response is empty or nil" do
->{_class.new ""}.must_raise(Autodiscover::ArgumentError)
->{_class.new nil}.must_raise(Autodiscover::ArgumentError)
end
end
def teardown
# Do nothing
describe "#exchange_version" do
it "returns an Exchange version usable for EWS" do
_(_class.new(response).exchange_version).must_equal "Exchange2013_SP1"
end
end
def test
skip 'Not implemented'
describe "#ews_url" do
it "returns the EWS url" do
_(_class.new(response).ews_url).must_equal "https://outlook.office365.com/EWS/Exchange.asmx"
end
end
end
describe "Protocol Hashes" do
let(:_inst) { _class.new(response) }
it "returns the EXCH protocol Hash" do
_(_inst.exch_proto["Type"]).must_equal "EXCH"
end
it "returns the EXPR protocol Hash" do
_(_inst.expr_proto["Type"]).must_equal "EXPR"
end
it "returns the WEB protocol Hash" do
_(_inst.web_proto["Type"]).must_equal "WEB"
end
it "returns empty Hashes when the protocols are missing" do
_inst.response["Account"]["Protocol"] = []
_(_inst.exch_proto).must_equal({})
_(_inst.expr_proto).must_equal({})
_(_inst.web_proto).must_equal({})
end
end
end

View file

@ -1,17 +1,18 @@
# frozen_string_literal: true
require "test_helper"
require 'minitest/autorun'
describe Autodiscover::ServerVersionParser do
let(:_class) { Autodiscover::ServerVersionParser }
class ServerVersionParserTest < Minitest::Test
def setup
# Do nothing
it "parses a hex ServerVersion response" do
inst = _class.new("738180DA")
_(inst.major).must_equal 14
_(inst.minor).must_equal 1
_(inst.build).must_equal 218
end
def teardown
# Do nothing
it "returns an Exchange Server Version" do
inst = _class.new("738180DA")
inst.exchange_version.must_equal "Exchange2010_SP1"
end
def test
skip 'Not implemented'
end
end
end