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

View file

@ -4,125 +4,201 @@
require 'spec_helper' require 'spec_helper'
require File.join(Rails.root, 'lib/webfinger')
describe Webfinger do describe Webfinger do
let(:host_with_port) { AppConfig.bare_pod_uri } let(:host_meta_xrd) { File.open(File.join(Rails.root, 'spec', 'fixtures', 'host-meta.fixture.html')).read }
let(:user1) { alice } let(:webfinger_xrd) { File.open(File.join(Rails.root, 'spec', 'fixtures', 'webfinger.fixture.html')).read }
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(:hcard_xml) { File.open(File.join(Rails.root, 'spec', 'fixtures', 'hcard.fixture.html')).read } let(:hcard_xml) { File.open(File.join(Rails.root, 'spec', 'fixtures', 'hcard.fixture.html')).read }
let(:account){'foo@bar.com'}
context 'setup' do let(:account_in_fixtures){"alice@localhost:9887"}
let(:finger){Webfinger.new(account)}
let(:host_meta_url){"http://#{AppConfig[:pod_uri].authority}/webfinger?q="}
describe '#intialize' do describe '#intialize' do
it 'sets account ' do it 'sets account ' do
n = Webfinger.new("mbs348@gmail.com") n = Webfinger.new("mbs348@gmail.com")
n.instance_variable_get(:@account).should_not be nil n.account.should_not be nil
end end
it 'downcases account' do it "downcases account and strips whitespace, and gsub 'acct:'" do
account = "BIGBOY@Example.Org" n = Webfinger.new("acct:BIGBOY@Example.Org ")
n = Webfinger.new(account) n.account.should == 'bigboy@example.org'
n.instance_variable_get(:@account).should == account.downcase
end end
it 'should set ssl as the default' do it 'should set ssl as the default' do
foo = Webfinger.new(account) foo = Webfinger.new(account)
foo.instance_variable_get(:@ssl).should be true foo.ssl.should be true
end end
end end
context 'webfinger query chain processing' do describe '.in_background' do
describe '#webfinger_profile_url' do it 'enqueues a Jobs::FetchWebfinger job' do
it 'parses out the webfinger template' do Resque.should_receive(:enqueue).with(Jobs::FetchWebfinger, account)
finger.send(:webfinger_profile_url, diaspora_xrd).should == Webfinger.in_background(account)
"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
end end
describe '#xrd_url' do describe '#fetch' do
it 'should return canonical host-meta url for http' do it 'works' do
finger.instance_variable_set(:@ssl, false) finger = Webfinger.new(account_in_fixtures)
finger.send(:xrd_url).should == "http://tom.joindiaspora.com/.well-known/host-meta" 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
person.should be_a Person
end end
it 'can return the https version' do
finger.send(:xrd_url).should == "https://tom.joindiaspora.com/.well-known/host-meta"
end
end
end end
describe '#get_xrd' do 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 it 'follows redirects' do
redirect_url = "http://whereami.whatisthis/host-meta" redirect_url = "http://whereami.whatisthis/host-meta"
stub_request(:get, "https://tom.joindiaspora.com/.well-known/host-meta").
stub_request(:get, "https://bar.com/.well-known/host-meta").
to_return(:status => 302, :headers => { 'Location' => redirect_url }) to_return(:status => 302, :headers => { 'Location' => redirect_url })
stub_request(:get, redirect_url). stub_request(:get, redirect_url).
to_return(:status => 200, :body => diaspora_xrd) to_return(:status => 200, :body => host_meta_xrd)
begin
finger.send :get_xrd finger.host_meta_xrd
rescue; end
a_request(:get, redirect_url).should have_been_made a_request(:get, redirect_url).should have_been_made
end end
end end
describe 'existing_person_with_profile?' do
context 'webfingering local people' do it 'returns true if cached_person is present and has a profile' do
it 'should return a person from the database if it matches its handle' do finger.should_receive(:cached_person).twice.and_return(Factory(:person))
person.save finger.existing_person_with_profile?.should be_true
finger.fetch.id.should == person.id
end
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)
person.should be_valid
end end
it 'should retry with http if https fails' do it 'returns false if it has no person' do
f = Webfinger.new("tom@tom.joindiaspora.com") finger.stub(:cached_person).and_return false
xrd_url = "://tom.joindiaspora.com/.well-known/host-meta" finger.existing_person_with_profile?.should be_false
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
end 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
end end