UNPKG

elsewhere

Version:

A node project that aims to replicate the functionality of the Google Social Graph API

407 lines (357 loc) 12.2 kB
// index.js // // main module for Webfinger // // Copyright 2012, StatusNet Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. var Step = require("step"), http = require("http"), https = require("https"), xml2js = require("xml2js"), url = require("url"); var JSONTYPE = "application/json", XRDTYPE = "application/xrd+xml"; var xrd2jrd = function(str, callback) { var getProperty = function(obj, Property) { var k, v; if (Property.hasOwnProperty("@")) { if (Property["@"].hasOwnProperty("type")) { k = Property["@"]["type"]; } if (Property["@"].hasOwnProperty("xsi:nil") && Property["@"]["xsi:nil"] == "true") { obj[k] = null; } else if (Property.hasOwnProperty("#")) { obj[k] = Property["#"]; } else { // TODO: log this } } }, getTitle = function(obj, Title) { var k = "default"; if (typeof(Title) == "string") { obj[k] = Title; } else { if (Title.hasOwnProperty("@")) { if (Title["@"].hasOwnProperty("xml:lang")) { k = Title["@"]["xml:lang"]; } } if (Title.hasOwnProperty("#")) { obj[k] = Title["#"]; } } }, getLink = function(Link) { var prop, i, l, k, v, Property, Title; l = {}; if (Link.hasOwnProperty("@")) { for (prop in Link["@"]) { if (Link["@"].hasOwnProperty(prop)) { l[prop] = Link["@"][prop]; } } } if (Link.hasOwnProperty("Property")) { // groan l.properties = {}; if (Array.isArray(Link.Property)) { for (i = 0; i < Link.Property.length; i++) { getProperty(l.properties, Link.Property[i]); } } else { getProperty(l.properties, Link.Property); } } if (Link.hasOwnProperty("Title")) { l.titles = {}; if (Array.isArray(Link.Title)) { for (i = 0; i < Link.Title.length; i++) { getTitle(l.titles, Link.Title[i]); } } else { getTitle(l.titles, Link.Title); } } return l; }; Step( function() { var parser = new xml2js.Parser(); parser.parseString(str, this); }, function(err, doc) { var Link, jrd = {}, i, prop, l; if (err) { callback(err, null); } else { // XXX: Booooooo! This is bletcherous. if (doc.hasOwnProperty("Subject")) { if (Array.isArray(doc.Subject)) { jrd.subject = doc.Subject[0]; } else { jrd.subject = doc.Subject; } } if (doc.hasOwnProperty("Expires")) { if (Array.isArray(doc.Expires)) { jrd.expires = doc.Expires[0]; } else { jrd.expires = doc.Expires; } } if (doc.hasOwnProperty("Alias")) { if (Array.isArray(doc.Alias)) { jrd.aliases = doc.Alias; } else { jrd.aliases = [doc.Alias]; } } if (doc.hasOwnProperty("Property")) { jrd.properties = {}; if (Array.isArray(doc.Property)) { for (i = 0; i < doc.Property.length; i++) { getProperty(jrd.properties, doc.Property[i]); } } else { getProperty(jrd.properties, doc.Property); } } if (doc.hasOwnProperty("Link")) { Link = doc.Link; jrd.links = []; if (Array.isArray(Link)) { for (i = 0; i < Link.length; i++) { jrd.links.push(getLink(Link[i])); } } else { jrd.links.push(getLink(Link)); } } callback(null, jrd); } } ); }; var jrd = function(body, callback) { var result; try { result = JSON.parse(body); callback(null, result); } catch (err) { callback(err, null); } }; var request = function(module, options, parsers, callback) { var types = [], prop; for (prop in parsers) { if (parsers.hasOwnProperty(prop)) { types.push(prop); } } var req = module.request(options, function(res) { var body = ""; res.setEncoding('utf8'); res.on('data', function (chunk) { body = body + chunk; }); res.on("error", function(err) { callback(err, null); }); res.on("end", function() { var ct, matched, parser; if (res.statusCode > 300 && res.statusCode < 400 && res.headers.location) { options = url.parse(res.headers.location); request(((options.protocol == "https:") ? https : http), options, parsers, callback); return; } else if (res.statusCode !== 200) { callback(new Error("Bad response code: " + res.statusCode + ":" + body), null); return; } if (!res.headers["content-type"]) { callback(new Error("No Content-Type header"), null); return; } ct = res.headers["content-type"]; matched = types.filter(function(type) { return (ct.substr(0, type.length) == type); }); if (matched.length == 0) { callback(new Error("Content-Type '"+ct+"' does not match any expected types: "+types.join(",")), null); return; } parser = parsers[matched]; parser(body, callback); }); }); req.on('error', function(err) { callback(err, null); }); req.end(); }; var httpHostMeta = function(address, callback) { var options = { hostname: address, port: 80, path: "/.well-known/host-meta", method: "GET", headers: { accept: "application/json, application/xrd+xml; q=0.5" } }; request(http, options, {"application/json": jrd, "application/xrd+xml": xrd2jrd}, callback); }; var httpHostMetaJSON = function(address, callback) { var options = { hostname: address, port: 80, path: "/.well-known/host-meta.json", method: "GET" }; request(http, options, {"application/json": jrd}, callback); }; var httpsHostMeta = function(address, callback) { var options = { hostname: address, port: 443, path: "/.well-known/host-meta", method: "GET", headers: { accept: "application/json, application/xrd+xml; q=0.5" } }; request(https, options, {"application/json": jrd, "application/xrd+xml": xrd2jrd}, callback); }; var httpsHostMetaJSON = function(address, callback) { var options = { hostname: address, port: 443, path: "/.well-known/host-meta.json", method: "GET" }; request(https, options, {"application/json": jrd}, callback); }; var hostmeta = function(address, callback) { Step( function() { httpsHostMetaJSON(address, this); }, function(err, jrd) { if (!err) { callback(null, jrd); } else { httpsHostMeta(address, this); } }, function(err, jrd) { if (!err) { callback(null, jrd); } else { httpHostMetaJSON(address, this); } }, function(err, jrd) { if (!err) { callback(null, jrd); } else { httpHostMeta(address, this); } }, function(err, jrd) { if (!err) { callback(null, jrd); } else { callback(new Error("Unable to get host-meta or host-meta.json"), null); } } ); }; var template = function(tmpl, address, parsers, callback) { var getme = tmpl.replace("{uri}", encodeURIComponent(address)), options = url.parse(getme); request(((options.protocol == "https:") ? https : http), options, parsers, callback); }; var webfinger = function(address, callback) { var parts, username, hostname; if (address.indexOf("@") === -1) { callback(new Error(address + " doesn't look like a webfinger address"), null); return; } parts = address.split("@", 2); if (parts.length !== 2) { callback(new Error(address + " doesn't look like a webfinger address"), null); return; } username = parts[0]; hostname = parts[1]; Step( function() { hostmeta(hostname, this); }, function(err, hm) { var lrdds, json, xrd; if (err) throw err; if (!hm.hasOwnProperty("links")) { throw new Error("No links in host-meta"); } // First, get the lrdd ones lrdds = hm.links.filter(function(link) { return (link.hasOwnProperty("rel") && link.rel == "lrdd" && link.hasOwnProperty("template")); }); if (!lrdds || lrdds.length === 0) { throw new Error("No lrdd links with templates in host-meta"); } // Try JSON ones first json = lrdds.filter(function(link) { return (link.hasOwnProperty("type") && link.type == JSONTYPE); }); if (json && json.length > 0) { template(json[0].template, address, {"application/json": jrd}, this); return; } // Try explicitly XRD ones second xrd = lrdds.filter(function(link) { return (link.hasOwnProperty("type") && link.type == XRDTYPE); }); if (xrd && xrd.length > 0) { template(xrd[0].template, address, {"application/xrd+xml": xrd2jrd}, this); return; } // Try implicitly XRD ones third xrd = lrdds.filter(function(link) { return (!link.hasOwnProperty("type")); }); if (xrd && xrd.length > 0) { template(xrd[0].template, address, {"application/xrd+xml": xrd2jrd}, this); return; } // Otherwise, give up throw new Error("No lrdd links with templates and acceptable type in host-meta"); }, callback ); }; var discover = function(address, callback) { if (address.indexOf("@") !== -1) { webfinger(address, callback); } else { hostmeta(address, callback); } }; exports.xrd2jrd = xrd2jrd; exports.webfinger = webfinger; exports.hostmeta = hostmeta; exports.discover = discover;