rewrite webfinger client and specs; now this is much easier to maintain.

This commit is contained in:
Maxwell Salzberg 2012-01-18 01:21:28 -08:00
parent 392e317962
commit 38ad76d9c7
2 changed files with 253 additions and 193 deletions

View file

@ -2,126 +2,110 @@ require File.join(Rails.root, 'lib/hcard')
require File.join(Rails.root, 'lib/webfinger_profile')
class Webfinger
class WebfingerFailedError < RuntimeError; end
attr_accessor :host_meta_xrd, :webfinger_profile_xrd,
:webfinger_profile, :hcard, :hcard_xrd, :person,
:account, :ssl
def initialize(account)
@account = account.strip.gsub('acct:','').to_s.downcase
@ssl = true
self.account = account
self.ssl = true
Rails.logger.info("event=webfinger status=initialized target=#{account}")
end
def fetch
return person if existing_person_with_profile?
create_or_update_person_from_webfinger_profile!
end
def self.in_background(account, opts={})
Resque.enqueue(Jobs::FetchWebfinger, account)
end
def fetch
begin
person = Person.by_account_identifier(@account)
if person
if person.profile
Rails.logger.info("event=webfinger status=success route=local target=#{@account}")
return person
end
end
#everything below should be private I guess
def account=(str)
@account = str.strip.gsub('acct:','').to_s.downcase
end
profile_url = get_xrd
webfinger_profile = get_webfinger_profile(profile_url)
if person
person.assign_new_profile_from_hcard(get_hcard(webfinger_profile))
fingered_person = person
else
fingered_person = make_person_from_webfinger(webfinger_profile)
end
if fingered_person
Rails.logger.info("event=webfinger status=success route=remote target=#{@account}")
fingered_person
else
Rails.logger.info("event=webfinger status=failure route=remote target=#{@account}")
raise WebfingerFailedError.new(@account)
end
def get(url)
Rails.logger.info("Getting: #{url} for #{account}")
begin
Faraday.get(url).body
rescue Exception => e
Rails.logger.info("event=receive status=abort recipient=#{@account} reason='#{e.message}'")
nil
Rails.logger.info("Failed to fetch: #{url} for #{account}; #{e.message}")
raise e
end
end
private
def get_xrd
begin
http = Faraday.get xrd_url
def existing_person_with_profile?
cached_person.present? && cached_person.profile.present?
end
profile_url = webfinger_profile_url(http.body)
if profile_url
return profile_url
else
raise "no profile URL"
end
def cached_person
self.person ||= Person.by_account_identifier(account)
end
def create_or_update_person_from_webfinger_profile!
if person #update my profile please
person.assign_new_profile_from_hcard(self.hcard)
else
person = make_person_from_webfinger
end
Rails.logger.info("event=webfinger status=success route=remote target=#{@account}")
person
end
#this tries the xrl url with https first, then falls back to http
def host_meta_xrd
begin
get(host_meta_url)
rescue Exception => e
if @ssl
@ssl = false
if self.ssl
self.ssl = false
retry
else
raise e
raise I18n.t('webfinger.xrd_fetch_failed', :account => @account)
raise I18n.t('webfinger.xrd_fetch_failed', :account => account)
end
end
end
def get_webfinger_profile(profile_url)
begin
http = Faraday.get(profile_url)
def hcard
@hcard ||= HCard.build(hcard_xrd)
end
rescue
raise I18n.t('webfinger.fetch_failed', :profile_url => profile_url)
end
return http.body
def webfinger_profile
@webfinger_profile ||= WebfingerProfile.new(account, webfinger_profile_xrd)
end
def hcard_url
@wf_profile.hcard
self.webfinger_profile.hcard
end
def get_hcard(webfinger_profile)
unless webfinger_profile.strip == ""
@wf_profile = WebfingerProfile.new(@account, webfinger_profile)
begin
hcard = Faraday.get(hcard_url)
rescue
return I18n.t('webfinger.hcard_fetch_failed', :account => @account)
end
HCard.build hcard.body
else
nil
end
end
def make_person_from_webfinger(webfinger_profile)
card = get_hcard(webfinger_profile)
if card && @wf_profile
Person.create_from_webfinger(@wf_profile, card)
end
end
##helpers
private
def webfinger_profile_url(xrd_response)
doc = Nokogiri::XML::Document.parse(xrd_response)
def webfinger_profile_url
doc = Nokogiri::XML::Document.parse(self.host_meta_xrd)
return nil if doc.namespaces["xmlns"] != "http://docs.oasis-open.org/ns/xri/xrd-1.0"
swizzle doc.at('Link[rel=lrdd]').attribute('template').value
end
def xrd_url
domain = @account.split('@')[1]
"http#{'s' if @ssl}://#{domain}/.well-known/host-meta"
def webfinger_profile_xrd
@webfinger_profile_xrd ||= get(webfinger_profile_url)
end
def hcard_xrd
@hcard_xrd ||= get(hcard_url)
end
def make_person_from_webfinger
Person.create_from_webfinger(webfinger_profile, hcard)
end
def host_meta_url
domain = account.split('@')[1]
"http#{'s' if self.ssl}://#{domain}/.well-known/host-meta"
end
def swizzle(template)
template.gsub '{uri}', @account
template.gsub('{uri}', account)
end
end

View file

@ -4,125 +4,201 @@
require 'spec_helper'
require File.join(Rails.root, 'lib/webfinger')
describe Webfinger do
let(:host_with_port) { AppConfig.bare_pod_uri }
let(:user1) { alice }
let(:user2) { eve }
let(:account) { "foo@tom.joindiaspora.com" }
let(:person) { Factory(:person, :diaspora_handle => account) }
let(:finger) { Webfinger.new(account) }
let(:good_request) { FakeHttpRequest.new(:success) }
let(:diaspora_xrd) { File.open(File.join(Rails.root, 'spec', 'fixtures', 'host-meta.fixture.html')).read }
let(:diaspora_finger) { File.open(File.join(Rails.root, 'spec', 'fixtures', 'webfinger.fixture.html')).read }
let(:host_meta_xrd) { File.open(File.join(Rails.root, 'spec', 'fixtures', 'host-meta.fixture.html')).read }
let(:webfinger_xrd) { File.open(File.join(Rails.root, 'spec', 'fixtures', 'webfinger.fixture.html')).read }
let(:hcard_xml) { File.open(File.join(Rails.root, 'spec', 'fixtures', 'hcard.fixture.html')).read }
let(:account){'foo@bar.com'}
let(:account_in_fixtures){"alice@localhost:9887"}
let(:finger){Webfinger.new(account)}
let(:host_meta_url){"http://#{AppConfig[:pod_uri].authority}/webfinger?q="}
context 'setup' do
describe '#intialize' do
it 'sets account ' do
n = Webfinger.new("mbs348@gmail.com")
n.instance_variable_get(:@account).should_not be nil
end
it 'downcases account' do
account = "BIGBOY@Example.Org"
n = Webfinger.new(account)
n.instance_variable_get(:@account).should == account.downcase
end
it 'should set ssl as the default' do
foo = Webfinger.new(account)
foo.instance_variable_get(:@ssl).should be true
end
describe '#intialize' do
it 'sets account ' do
n = Webfinger.new("mbs348@gmail.com")
n.account.should_not be nil
end
context 'webfinger query chain processing' do
describe '#webfinger_profile_url' do
it 'parses out the webfinger template' do
finger.send(:webfinger_profile_url, diaspora_xrd).should ==
"http://#{AppConfig[:pod_uri].authority}/webfinger?q=foo@tom.joindiaspora.com"
end
it 'should return nil if not an xrd' do
finger.send(:webfinger_profile_url, '<html></html>').should be nil
end
end
describe '#xrd_url' do
it 'should return canonical host-meta url for http' do
finger.instance_variable_set(:@ssl, false)
finger.send(:xrd_url).should == "http://tom.joindiaspora.com/.well-known/host-meta"
end
it 'can return the https version' do
finger.send(:xrd_url).should == "https://tom.joindiaspora.com/.well-known/host-meta"
end
end
it "downcases account and strips whitespace, and gsub 'acct:'" do
n = Webfinger.new("acct:BIGBOY@Example.Org ")
n.account.should == 'bigboy@example.org'
end
describe '#get_xrd' do
it 'follows redirects' do
redirect_url = "http://whereami.whatisthis/host-meta"
stub_request(:get, "https://tom.joindiaspora.com/.well-known/host-meta").
to_return(:status => 302, :headers => { 'Location' => redirect_url })
stub_request(:get, redirect_url).
to_return(:status => 200, :body => diaspora_xrd)
begin
finger.send :get_xrd
rescue; end
a_request(:get, redirect_url).should have_been_made
end
it 'should set ssl as the default' do
foo = Webfinger.new(account)
foo.ssl.should be true
end
end
context 'webfingering local people' do
it 'should return a person from the database if it matches its handle' do
person.save
finger.fetch.id.should == person.id
end
describe '.in_background' do
it 'enqueues a Jobs::FetchWebfinger job' do
Resque.should_receive(:enqueue).with(Jobs::FetchWebfinger, account)
Webfinger.in_background(account)
end
it 'should fetch a diaspora webfinger and make a person for them' do
User.delete_all; Person.delete_all; Profile.delete_all
hcard_url = "http://google-1655890.com/hcard/users/29a9d5ae5169ab0b"
f = Webfinger.new("alice@#{host_with_port}")
stub_request(:get, f.send(:xrd_url)).
to_return(:status => 200, :body => diaspora_xrd, :headers => {})
stub_request(:get, f.send(:webfinger_profile_url, diaspora_xrd)).
to_return(:status => 200, :body => diaspora_finger, :headers => {})
f.should_receive(:hcard_url).and_return(hcard_url)
stub_request(:get, hcard_url).
to_return(:status => 200, :body => hcard_xml, :headers => {})
person = f.fetch
WebMock.should have_requested(:get, f.send(:xrd_url))
WebMock.should have_requested(:get, f.send(:webfinger_profile_url, diaspora_xrd))
WebMock.should have_requested(:get, hcard_url)
end
describe '#fetch' do
it 'works' do
finger = Webfinger.new(account_in_fixtures)
finger.stub(:host_meta_xrd).and_return(host_meta_xrd)
finger.stub(:hcard_xrd).and_return(hcard_xml)
finger.stub(:webfinger_profile_xrd).and_return(webfinger_xrd)
person = finger.fetch
person.should be_valid
end
it 'should retry with http if https fails' do
f = Webfinger.new("tom@tom.joindiaspora.com")
xrd_url = "://tom.joindiaspora.com/.well-known/host-meta"
stub_request(:get, "https#{xrd_url}").
to_return(:status => 503, :body => "", :headers => {})
stub_request(:get, "http#{xrd_url}").
to_return(:status => 200, :body => diaspora_xrd, :headers => {})
#Faraday::Connection.any_instance.should_receive(:get).twice.and_return(nil, diaspora_xrd)
f.send(:get_xrd)
WebMock.should have_requested(:get,"https#{xrd_url}")
WebMock.should have_requested(:get,"http#{xrd_url}")
f.instance_variable_get(:@ssl).should == false
person.should be_a Person
end
end
describe '#get' do
it 'makes a request and grabs the body' do
url ="https://bar.com/.well-known/host-meta"
stub_request(:get, url).
to_return(:status => 200, :body => host_meta_xrd)
finger.get(url).should == host_meta_xrd
end
it 'follows redirects' do
redirect_url = "http://whereami.whatisthis/host-meta"
stub_request(:get, "https://bar.com/.well-known/host-meta").
to_return(:status => 302, :headers => { 'Location' => redirect_url })
stub_request(:get, redirect_url).
to_return(:status => 200, :body => host_meta_xrd)
finger.host_meta_xrd
a_request(:get, redirect_url).should have_been_made
end
end
describe 'existing_person_with_profile?' do
it 'returns true if cached_person is present and has a profile' do
finger.should_receive(:cached_person).twice.and_return(Factory(:person))
finger.existing_person_with_profile?.should be_true
end
it 'returns false if it has no person' do
finger.stub(:cached_person).and_return false
finger.existing_person_with_profile?.should be_false
end
it 'returns false if the person has no profile' do
p = Factory(:person)
p.profile = nil
finger.stub(:cached_person).and_return(p)
finger.existing_person_with_profile?.should be_false
end
end
describe 'cached_person' do
it 'sets the person by looking up the account from Person.by_account_identifier' do
person = stub
Person.should_receive(:by_account_identifier).with(account).and_return(person)
finger.cached_person.should == person
finger.person.should == person
end
end
describe 'create_or_update_person_from_webfinger_profile!' do
context 'with a cached_person' do
it 'calls Person#assign_new_profile_from_hcard with the fetched hcard' do
finger.hcard_xrd = hcard_xml
finger.stub(:person).and_return(bob.person)
bob.person.should_receive(:assign_new_profile_from_hcard).with(finger.hcard)
finger.create_or_update_person_from_webfinger_profile!
end
end
context 'with no cached person' do
it 'sets person based on make_person_from_webfinger' do
finger.stub(:person).and_return(nil)
finger.should_receive(:make_person_from_webfinger)
finger.create_or_update_person_from_webfinger_profile!
end
end
end
describe '#host_meta_xrd' do
it 'calls #get with host_meta_url' do
finger.stub(:host_meta_url).and_return('meta')
finger.should_receive(:get).with('meta')
finger.host_meta_xrd
end
it 'should retry with ssl off a second time' do
finger.should_receive(:get).and_raise
finger.should_receive(:get)
finger.host_meta_xrd
finger.ssl.should be false
end
end
describe '#hcard' do
it 'calls HCard.build' do
finger.stub(:hcard_xrd).and_return(hcard_xml)
HCard.should_receive(:build).with(hcard_xml).and_return true
finger.hcard.should_not be_nil
end
end
describe '#webfinger_profile' do
it 'constructs a new WebfingerProfile object' do
finger.stub(:webfinger_profile_xrd).and_return(webfinger_xrd)
WebfingerProfile.should_receive(:new).with(account, webfinger_xrd)
finger.webfinger_profile
end
end
describe '#webfinger_profile_url' do
it 'returns the llrd link for a valid host meta' do
finger.stub(:host_meta_xrd).and_return(host_meta_xrd)
finger.webfinger_profile_url.should_not be_nil
end
it 'returns nil if no link is found' do
finger.stub(:host_meta_xrd).and_return(nil)
finger.webfinger_profile_url.should be_nil
end
end
describe '#webfinger_profile_xrd' do
it 'calls #get with the hcard_url' do
finger.stub(:hcard_url).and_return("url")
finger.should_receive(:get).with("url")
finger.hcard_xrd
end
end
describe '#make_person_from_webfinger' do
it 'with an hcard and a webfinger_profile, it calls Person.create_from_webfinger' do
finger.stub(:hcard).and_return("hcard")
finger.stub(:webfinger_profile).and_return("webfinger_profile")
Person.should_receive(:create_from_webfinger).with("webfinger_profile", "hcard")
finger.make_person_from_webfinger
end
end
describe '#host_meta_url' do
it 'should return canonical host-meta url for http' do
finger.ssl = false
finger.host_meta_url.should == "http://bar.com/.well-known/host-meta"
end
it 'can return the https version' do
finger.host_meta_url.should == "https://bar.com/.well-known/host-meta"
end
end
describe 'swizzle' do
it 'gsubs out {uri} for the account' do
string = "{uri} is the coolest"
finger.swizzle(string).should == "#{finger.account} is the coolest"
end
end
end