use host-meta xml generator from raven24's gem
This commit is contained in:
parent
a0c4021c23
commit
0fe6c4dc27
12 changed files with 434 additions and 15 deletions
|
|
@ -2,6 +2,7 @@ PATH
|
|||
remote: .
|
||||
specs:
|
||||
diaspora_federation (0.0.1)
|
||||
nokogiri (~> 1.6.6.2)
|
||||
rails (~> 4.2.2)
|
||||
|
||||
GEM
|
||||
|
|
|
|||
3
LICENSE
3
LICENSE
|
|
@ -13,3 +13,6 @@ GNU Affero General Public License for more details.
|
|||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Some parts are based on an older federation gem from Florian Staudacher:
|
||||
https://github.com/Raven24/diaspora-federation
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ require_dependency "diaspora_federation/application_controller"
|
|||
module DiasporaFederation
|
||||
class WebfingerController < ApplicationController
|
||||
def host_meta
|
||||
render "host_meta", content_type: "application/xrd+xml"
|
||||
doc = WebFinger::HostMeta.from_base_url(DiasporaFederation.server_uri.to_s)
|
||||
render body: doc.to_xml, content_type: "application/xrd+xml"
|
||||
end
|
||||
|
||||
##
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
|
||||
|
||||
<!-- Resource-specific Information -->
|
||||
|
||||
<Link rel="lrdd"
|
||||
type="application/xrd+xml"
|
||||
template="<%= DiasporaFederation.server_uri.to_s %>webfinger?q={uri}" />
|
||||
|
||||
</XRD>
|
||||
|
|
@ -18,4 +18,5 @@ Gem::Specification.new do |s|
|
|||
s.test_files = Dir["test/**/*"]
|
||||
|
||||
s.add_dependency "rails", "~> 4.2.2"
|
||||
s.add_dependency "nokogiri", "~> 1.6.6.2"
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
require "diaspora_federation/engine"
|
||||
require "diaspora_federation/logging"
|
||||
|
||||
require "diaspora_federation/webfinger"
|
||||
|
||||
##
|
||||
# diaspora* federation rails engine
|
||||
module DiasporaFederation
|
||||
|
|
@ -73,7 +75,6 @@ module DiasporaFederation
|
|||
end
|
||||
end
|
||||
|
||||
##
|
||||
# raised, if the engine is not configured correctly
|
||||
class ConfigurationError < RuntimeError
|
||||
end
|
||||
|
|
|
|||
11
lib/diaspora_federation/webfinger.rb
Normal file
11
lib/diaspora_federation/webfinger.rb
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
module DiasporaFederation
|
||||
##
|
||||
# This module provides the namespace for the various classes implementing
|
||||
# WebFinger and other protocols used for metadata discovery on remote servers
|
||||
# in the Diaspora* network.
|
||||
module WebFinger
|
||||
end
|
||||
end
|
||||
|
||||
require "diaspora_federation/webfinger/xrd_document"
|
||||
require "diaspora_federation/webfinger/host_meta"
|
||||
104
lib/diaspora_federation/webfinger/host_meta.rb
Normal file
104
lib/diaspora_federation/webfinger/host_meta.rb
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
|
||||
module DiasporaFederation
|
||||
module WebFinger
|
||||
##
|
||||
# Generates and parses Host Meta documents.
|
||||
#
|
||||
# This is a minimal implementation of the standard, only to the degree of what
|
||||
# is used for the purposes of the Diaspora* protocol. (e.g. WebFinger)
|
||||
#
|
||||
# @example Creating a Host Meta document
|
||||
# doc = HostMeta.from_base_url("https://pod.example.tld/")
|
||||
# doc.to_xml
|
||||
#
|
||||
# @example Parsing a Host Meta document
|
||||
# doc = HostMeta.from_xml(xml_string)
|
||||
# webfinger_tpl = doc.webfinger_template_url
|
||||
#
|
||||
# @see http://tools.ietf.org/html/rfc6415 RFC 6415: "Web Host Metadata"
|
||||
# @see XrdDocument
|
||||
class HostMeta
|
||||
private_class_method :new
|
||||
|
||||
# URL fragment to append to the base URL
|
||||
WEBFINGER_SUFFIX = "webfinger?q={uri}"
|
||||
|
||||
##
|
||||
# Returns the WebFinger URL that was used to build this instance (either from
|
||||
# xml or by giving a base URL).
|
||||
# @return [String] WebFinger template URL
|
||||
def webfinger_template_url
|
||||
@webfinger_url
|
||||
end
|
||||
|
||||
##
|
||||
# Produces the XML string for the Host Meta instance with a +Link+ element
|
||||
# containing the +webfinger_url+.
|
||||
# @return [String] XML string
|
||||
def to_xml
|
||||
doc = XrdDocument.new
|
||||
doc.links << {rel: "lrdd",
|
||||
type: "application/xrd+xml",
|
||||
template: @webfinger_url}
|
||||
doc.to_xml
|
||||
end
|
||||
|
||||
##
|
||||
# Builds a new HostMeta instance and constructs the WebFinger URL from the
|
||||
# given base URL by appending HostMeta::WEBFINGER_SUFFIX.
|
||||
# @return [HostMeta]
|
||||
# @raise [InvalidData] if the webfinger url is malformed
|
||||
def self.from_base_url(base_url)
|
||||
raise ArgumentError, "base_url is not a String" unless base_url.instance_of?(String)
|
||||
|
||||
base_url += "/" unless base_url.end_with?("/")
|
||||
webfinger_url = base_url + WEBFINGER_SUFFIX
|
||||
raise InvalidData, "invalid webfinger url: #{webfinger_url}" unless webfinger_url_valid?(webfinger_url)
|
||||
|
||||
hm = allocate
|
||||
hm.instance_variable_set(:@webfinger_url, webfinger_url)
|
||||
hm
|
||||
end
|
||||
|
||||
##
|
||||
# Reads the given Host Meta XML document string and populates the
|
||||
# +webfinger_url+.
|
||||
# @param [String] hostmeta_xml Host Meta XML string
|
||||
# @raise [InvalidData] if the xml or the webfinger url is malformed
|
||||
def self.from_xml(hostmeta_xml)
|
||||
data = XrdDocument.xml_data(hostmeta_xml)
|
||||
raise InvalidData, "received an invalid xml" unless data.key?(:links)
|
||||
|
||||
webfinger_url = webfinger_url_from_xrd(data)
|
||||
raise InvalidData, "invalid webfinger url: #{webfinger_url}" unless webfinger_url_valid?(webfinger_url)
|
||||
|
||||
hm = allocate
|
||||
hm.instance_variable_set(:@webfinger_url, webfinger_url)
|
||||
hm
|
||||
end
|
||||
|
||||
##
|
||||
# Applies some basic sanity-checking to the given URL
|
||||
# @param [String] url validation subject
|
||||
# @return [Boolean] validation result
|
||||
def self.webfinger_url_valid?(url)
|
||||
!url.nil? && url.instance_of?(String) && url =~ %r{^https?:\/\/.*\{uri\}}i
|
||||
end
|
||||
private_class_method :webfinger_url_valid?
|
||||
|
||||
##
|
||||
# Gets the webfinger url from an XRD data structure
|
||||
# @param [Hash] extracted data
|
||||
# @return [String] webfinger url
|
||||
def self.webfinger_url_from_xrd(data)
|
||||
link = data[:links].find {|l| (l[:rel] == "lrdd" && l[:type] == "application/xrd+xml") }
|
||||
return link[:template] unless link.nil?
|
||||
end
|
||||
private_class_method :webfinger_url_from_xrd
|
||||
|
||||
# Raised, if the +webfinger_url+ is missing or malformed
|
||||
class InvalidData < RuntimeError
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
151
lib/diaspora_federation/webfinger/xrd_document.rb
Normal file
151
lib/diaspora_federation/webfinger/xrd_document.rb
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
module DiasporaFederation
|
||||
module WebFinger
|
||||
##
|
||||
# This class implements basic handling of XRD documents as far as it is
|
||||
# necessary in the context of the protocols used with Diaspora* federation.
|
||||
#
|
||||
# @note {http://tools.ietf.org/html/rfc6415 RFC 6415} recommends that servers
|
||||
# should also offer the JRD format in addition to the XRD representation.
|
||||
# Implementing +XrdDocument#to_json+ and +XrdDocument.json_data+ should
|
||||
# be almost trivial due to the simplicity of the format and the way the data
|
||||
# is stored internally already. See
|
||||
# {http://tools.ietf.org/html/rfc6415#appendix-A RFC 6415, Appendix A}
|
||||
# for a description of the JSON format.
|
||||
#
|
||||
# @example Creating a XrdDocument
|
||||
# doc = XrdDocument.new
|
||||
# doc.expires = DateTime.new(2020, 1, 15, 0, 0, 1)
|
||||
# doc.subject = "http://example.tld/articles/11""
|
||||
# doc.aliases << "http://example.tld/cool_article"
|
||||
# doc.aliases << "http://example.tld/authors/2/articles/3"
|
||||
# doc.properties["http://x.example.tld/ns/version"] = "1.3"
|
||||
# doc.links << { rel: "author", type: "text/html", href: "http://example.tld/authors/2" }
|
||||
# doc.links << { rel: "copyright", template: "http://example.tld/copyright?id={uri}" }
|
||||
#
|
||||
# doc.to_xml
|
||||
#
|
||||
# @example Parsing a XrdDocument
|
||||
# data = XrdDocument.xml_data(xml_string)
|
||||
#
|
||||
# @see http://docs.oasis-open.org/xri/xrd/v1.0/xrd-1.0.html Extensible Resource Descriptor (XRD) Version 1.0
|
||||
class XrdDocument
|
||||
# xml namespace url
|
||||
XMLNS = "http://docs.oasis-open.org/ns/xri/xrd-1.0"
|
||||
|
||||
# +Link+ element attributes
|
||||
LINK_ATTRS = %i(rel type href template)
|
||||
|
||||
# format string for datetime (+Expires+ element)
|
||||
DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
|
||||
|
||||
attr_writer :expires, :subject
|
||||
|
||||
# @return [Array<String>] list of alias URIs
|
||||
attr_reader :aliases
|
||||
|
||||
# @return [Hash<String => mixed>] list of properties. Hash key represents the
|
||||
# +type+ attribute, and the value is the element content
|
||||
attr_reader :properties
|
||||
|
||||
# @return [Array<Hash<attr => val>>] list of +Link+ element hashes. Each
|
||||
# hash contains the attributesa and their associated values for the +Link+
|
||||
# element.
|
||||
attr_reader :links
|
||||
|
||||
def initialize
|
||||
@aliases = []
|
||||
@links = []
|
||||
@properties = {}
|
||||
end
|
||||
|
||||
##
|
||||
# Generates an XML document from the current instance and returns it as string
|
||||
# @return [String] XML document
|
||||
def to_xml
|
||||
builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
|
||||
xml.XRD("xmlns" => XMLNS) {
|
||||
if !@expires.nil? && @expires.instance_of?(DateTime)
|
||||
xml.Expires(@expires.strftime(DATETIME_FORMAT))
|
||||
end
|
||||
|
||||
xml.Subject(@subject) if !@subject.nil? && !@subject.empty?
|
||||
|
||||
@aliases.each do |a|
|
||||
next if !a.instance_of?(String) || a.empty?
|
||||
xml.Alias(a.to_s)
|
||||
end
|
||||
|
||||
@properties.each do |type, val|
|
||||
xml.Property(val.to_s, type: type)
|
||||
end
|
||||
|
||||
@links.each do |l|
|
||||
attrs = {}
|
||||
LINK_ATTRS.each do |attr|
|
||||
attrs[attr.to_s] = l[attr] if l.key?(attr)
|
||||
end
|
||||
xml.Link(attrs)
|
||||
end
|
||||
}
|
||||
end
|
||||
builder.to_xml
|
||||
end
|
||||
|
||||
##
|
||||
# Parse the XRD document from the given string and create a hash containing
|
||||
# the extracted data.
|
||||
#
|
||||
# Small bonus: the hash structure that comes out of this method is the same
|
||||
# as the one used to produce a JRD (JSON Resource Descriptor) or parsing it.
|
||||
#
|
||||
# @param [String] xrd_doc XML string
|
||||
# @return [Hash] extracted data
|
||||
# @raise [InvalidDocument] if the XRD is malformed
|
||||
def self.xml_data(xrd_doc)
|
||||
raise ArgumentError unless xrd_doc.instance_of?(String)
|
||||
|
||||
doc = Nokogiri::XML::Document.parse(xrd_doc)
|
||||
raise InvalidDocument, "Not an XRD document" if !doc.root || doc.root.name != "XRD"
|
||||
|
||||
data = {}
|
||||
ns = {xrd: XMLNS}
|
||||
|
||||
exp_elem = doc.at_xpath("xrd:XRD/xrd:Expires", ns)
|
||||
unless exp_elem.nil?
|
||||
data[:expires] = DateTime.strptime(exp_elem.content, DATETIME_FORMAT)
|
||||
end
|
||||
|
||||
subj_elem = doc.at_xpath("xrd:XRD/xrd:Subject", ns)
|
||||
data[:subject] = subj_elem.content unless subj_elem.nil?
|
||||
|
||||
aliases = []
|
||||
doc.xpath("xrd:XRD/xrd:Alias", ns).each do |node|
|
||||
aliases << node.content
|
||||
end
|
||||
data[:aliases] = aliases unless aliases.empty?
|
||||
|
||||
properties = {}
|
||||
doc.xpath("xrd:XRD/xrd:Property", ns).each do |node|
|
||||
properties[node[:type]] = node.children.empty? ? nil : node.content
|
||||
end
|
||||
data[:properties] = properties unless properties.empty?
|
||||
|
||||
links = []
|
||||
doc.xpath("xrd:XRD/xrd:Link", ns).each do |node|
|
||||
link = {}
|
||||
LINK_ATTRS.each do |attr|
|
||||
link[attr] = node[attr.to_s] if node.key?(attr.to_s)
|
||||
end
|
||||
links << link
|
||||
end
|
||||
data[:links] = links unless links.empty?
|
||||
|
||||
data
|
||||
end
|
||||
|
||||
# Raised, if the XML structure is invalid
|
||||
class InvalidDocument < RuntimeError
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -3,22 +3,29 @@ module DiasporaFederation
|
|||
routes { DiasporaFederation::Engine.routes }
|
||||
|
||||
describe "#host_meta" do
|
||||
before do
|
||||
DiasporaFederation.server_uri = URI("http://localhost:3000/")
|
||||
end
|
||||
|
||||
it "succeeds" do
|
||||
get :host_meta
|
||||
expect(response).to be_success
|
||||
end
|
||||
|
||||
it "contains the webfinger-template" do
|
||||
DiasporaFederation.server_uri = "http://localhost:3000/"
|
||||
get :host_meta
|
||||
expect(response.body).to include "template=\"http://localhost:3000/webfinger?q={uri}\""
|
||||
end
|
||||
|
||||
it "renders the host_meta template" do
|
||||
it "returns a application/xrd+xml" do
|
||||
get :host_meta
|
||||
expect(response).to render_template("host_meta")
|
||||
expect(response.header["Content-Type"]).to include "application/xrd+xml"
|
||||
end
|
||||
|
||||
it "calls WebFinger::HostMeta.from_base_url with the base url" do
|
||||
expect(WebFinger::HostMeta).to receive(:from_base_url).with("http://localhost:3000/").and_call_original
|
||||
get :host_meta
|
||||
end
|
||||
end
|
||||
|
||||
describe "#legacy_webfinger" do
|
||||
|
|
|
|||
78
spec/lib/webfinger/host_meta_spec.rb
Normal file
78
spec/lib/webfinger/host_meta_spec.rb
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
module DiasporaFederation
|
||||
describe WebFinger::HostMeta do
|
||||
base_url = "https://pod.example.tld/"
|
||||
xml = <<-XML
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
|
||||
<Link rel="lrdd" type="application/xrd+xml" template="#{base_url}webfinger?q={uri}"/>
|
||||
</XRD>
|
||||
XML
|
||||
|
||||
it "must not create blank instances" do
|
||||
expect { WebFinger::HostMeta.new }.to raise_error(NoMethodError)
|
||||
end
|
||||
|
||||
context "#to_xml" do
|
||||
it "creates a nice XML document" do
|
||||
hm = WebFinger::HostMeta.from_base_url(base_url)
|
||||
expect(hm.to_xml).to eq(xml)
|
||||
end
|
||||
|
||||
it "appends a '/' if necessary" do
|
||||
hm = WebFinger::HostMeta.from_base_url("https://pod.example.tld")
|
||||
expect(hm.to_xml).to eq(xml)
|
||||
end
|
||||
|
||||
it "fails if the base_url was omitted" do
|
||||
expect { WebFinger::HostMeta.from_base_url("") }.to raise_error(WebFinger::HostMeta::InvalidData)
|
||||
end
|
||||
end
|
||||
|
||||
context "#webfinger_template_url" do
|
||||
it "parses its own output" do
|
||||
hm = WebFinger::HostMeta.from_xml(xml)
|
||||
expect(hm.webfinger_template_url).to eq("#{base_url}webfinger?q={uri}")
|
||||
end
|
||||
|
||||
it "also reads old-style XML" do
|
||||
historic_xml = <<-XML
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
|
||||
|
||||
<!-- Resource-specific Information -->
|
||||
|
||||
<Link rel="lrdd"
|
||||
type="application/xrd+xml"
|
||||
template="#{base_url}webfinger?q={uri}" />
|
||||
|
||||
</XRD>
|
||||
XML
|
||||
hm = WebFinger::HostMeta.from_xml(historic_xml)
|
||||
expect(hm.webfinger_template_url).to eq("#{base_url}webfinger?q={uri}")
|
||||
end
|
||||
|
||||
it "fails if the document does not contain a webfinger url" do
|
||||
invalid_xml = <<XML
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
|
||||
</XRD>
|
||||
XML
|
||||
expect { WebFinger::HostMeta.from_xml(invalid_xml) }.to raise_error(WebFinger::HostMeta::InvalidData)
|
||||
end
|
||||
|
||||
it "fails if the document contains a malformed webfinger url" do
|
||||
invalid_xml = <<XML
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
|
||||
<Link rel="lrdd" type="application/xrd+xml" template="#{base_url}webfinger?q="/>
|
||||
</XRD>
|
||||
XML
|
||||
expect { WebFinger::HostMeta.from_xml(invalid_xml) }.to raise_error(WebFinger::HostMeta::InvalidData)
|
||||
end
|
||||
|
||||
it "fails if the document is invalid" do
|
||||
expect { WebFinger::HostMeta.from_xml("") }.to raise_error(WebFinger::XrdDocument::InvalidDocument)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
71
spec/lib/webfinger/xrd_document_spec.rb
Normal file
71
spec/lib/webfinger/xrd_document_spec.rb
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
module DiasporaFederation
|
||||
describe WebFinger::XrdDocument do
|
||||
xml = <<XML
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
|
||||
<Expires>2010-01-30T09:30:00Z</Expires>
|
||||
<Subject>http://blog.example.com/article/id/314</Subject>
|
||||
<Alias>http://blog.example.com/cool_new_thing</Alias>
|
||||
<Alias>http://blog.example.com/steve/article/7</Alias>
|
||||
<Property type="http://blgx.example.net/ns/version">1.3</Property>
|
||||
<Property type="http://blgx.example.net/ns/ext"/>
|
||||
<Link rel="author" type="text/html" href="http://blog.example.com/author/steve"/>
|
||||
<Link rel="author" href="http://example.com/author/john"/>
|
||||
<Link rel="copyright" template="http://example.com/copyright?id={uri}"/>
|
||||
</XRD>
|
||||
XML
|
||||
|
||||
data = {
|
||||
subject: "http://blog.example.com/article/id/314",
|
||||
expires: DateTime.parse("2010-01-30T09:30:00Z"),
|
||||
aliases: %w(
|
||||
http://blog.example.com/cool_new_thing
|
||||
http://blog.example.com/steve/article/7
|
||||
),
|
||||
properties: {
|
||||
"http://blgx.example.net/ns/version" => "1.3",
|
||||
"http://blgx.example.net/ns/ext" => nil
|
||||
},
|
||||
links: [
|
||||
{
|
||||
rel: "author",
|
||||
type: "text/html",
|
||||
href: "http://blog.example.com/author/steve"
|
||||
},
|
||||
{
|
||||
rel: "author",
|
||||
href: "http://example.com/author/john"
|
||||
},
|
||||
{
|
||||
rel: "copyright",
|
||||
template: "http://example.com/copyright?id={uri}"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
it "creates the xml document" do
|
||||
doc = WebFinger::XrdDocument.new
|
||||
doc.expires = data[:expires]
|
||||
doc.subject = data[:subject]
|
||||
|
||||
data[:aliases].each do |a|
|
||||
doc.aliases << a
|
||||
end
|
||||
|
||||
data[:properties].each do |t, v|
|
||||
doc.properties[t] = v
|
||||
end
|
||||
|
||||
data[:links].each do |h|
|
||||
doc.links << h
|
||||
end
|
||||
|
||||
expect(doc.to_xml).to eq(xml)
|
||||
end
|
||||
|
||||
it "reads the xml document" do
|
||||
doc = WebFinger::XrdDocument.xml_data(xml)
|
||||
expect(doc).to eq(data)
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Reference in a new issue