telehash
Version:
A telehash library for node and browserify
221 lines (184 loc) • 6.71 kB
JavaScript
var urllib = require('url');
var httplib = require('http');
var streamlib = require('stream');
var lob = require('lob-enc');
var hashname = require('hashname');
var util = require("util");
var THTP = require('./thtp.class');
// implements https://github.com/telehash/telehash.org/blob/v3/v3/channels/thtp.md
exports.name = 'thtp';
function sanitizeheaders(headers){
delete headers[":path"];
delete headers[":method"];
return headers;
}
exports.mesh = function(mesh, cbMesh)
{
var ext = {open:{}};
ext.link = function(link, cbLink)
{
/** proxy an existing node http request and response pair to this link over thtp.
* @memberOf TLink
* @param {httpIncomingMessage} request - typically generated from node's http server
* @param {httpResponseObject} response - typically generated from node's http server
* @return {ChannelStream} proxied response
*/
link.proxy = function(req, res)
{
// create the thtp request json
var json = {};
if(typeof req.headers == 'object') Object.keys(req.headers).forEach(function(header){
json[header.toLowerCase()] = req.headers[header];
});
json[':method'] = (req.method || 'GET').toUpperCase();
// convenience pattern
if(req.url)
{
var url = urllib.parse(req.url);
json[':path'] = url.path;
}else{
json[':path'] = (req.path || '/');
}
var packet = lob.encode(json, false);
// create the channel request
var open = {json:{type:'thtp'}};
open.json.seq = 1; // always reliable
open.body = packet.slice(0,1000); // send as much of the headers as we can
var channel = link.x.channel(open);
// create a stream to encode the http->thtp
var sencode = mesh.streamize(channel);
// create a stream to decode the thtp->http
var sdecode = lob.stream(function(packet, cbStream){
// mimic http://nodejs.org/api/http.html#http_http_incomingmessage
sdecode.statusCode = parseInt(packet.json[':status'])||500;
sdecode.reasonPhrase = packet.json[':reason']||'';
delete packet.json[':status'];
delete packet.json[':reason'];
sdecode.headers = packet.json;
// direct response two ways depending on args
if(typeof res == 'object')
{
res.writeHead(sdecode.statusCode, packet.json);
sdecode.pipe(res);
}else if(typeof res == 'function'){
res(sdecode); // handler must set up stream piping
}else{
return cbStream('no result handler');
}
cbStream();
}).on('error', function(err){
log.error('got thtp error',err);
})
// any response is decoded
sencode.pipe(sdecode);
// finish sending the open
channel.send(open);
// if more header data, send it too
if(packet.length > 1000) sencode.write(packet.slice(1000));
// auto-pipe in any request body
if(typeof req.pipe == 'function') req.pipe(sencode);
return sencode;
}
/** create a thtp request just like http://nodejs.org/api/http.html#http_http_request_options_callback
* @memberOf TLink
* @param {object} options - see node docs
* @param {function} callback - see node docs
* @return {ChannelStream} http style response stream
*/
link.request = function(options, cbRequest)
{
// allow string url as the only arg
if(typeof options == 'string')
options = urllib.parse(options);
options.method = options.method || "GET";
// TODO, handle friendly body/json options like the request module?
var proxy = link.proxy(options, function(response){
if(cbRequest)
cbRequest(response);
cbRequest = false;
});
proxy.on('error', function(err){
if(cbRequest)
cbRequest(err);
cbRequest = false;
});
// friendly
if(options.method.toUpperCase() == 'GET')
proxy.end();
return proxy;
}
cbLink();
}
/** make a thtp GET request to a url where the hashname is the hostname
* @memberOf Mesh
* @param {string} req - url: http://[hashname]/[path]
* @param {function} callback - see node docs
* @return {ChannelStream} http style response stream
*/
mesh.request = function(req, cbRequest)
{
if(typeof req == 'string')
req = urllib.parse(req);
if(!hashname.isHashname(req.hostname))
return cbRequest('invalid hashname',req.hostname);
return mesh.link(req.hostname).request(req, cbRequest);
}
var mPaths = {};
mesh.match = function(path, cbMatch)
{
mPaths[path] = cbMatch;
}
/** begin accepting incoming thtp requests, either to proxy to a remote http server, or directly into a local server
* @memberOf Mesh
* @param {httpServer|string} options - either a httpserver or a url denoting the host and port to proxy to.
*/
mesh.proxy = function(options)
{
// provide a url to directly proxy to
if(typeof options == 'string')
{
mesh._proxy = httplib.createServer();
var to = urllib.parse(options);
if(to.hostname == '0.0.0.0') to.hostname = '127.0.0.1';
mesh._proxy.on('request', function(req, res){
var opt = {host:to.hostname,port:to.port,method:req.headers[":method"],path:req.headers[":path"],headers:sanitizeheaders(req.headers)};
req.pipe(httplib.request(opt, function(pres){
pres.pipe(res)
}));
});
}else{
// local http server given as argument
mesh._proxy = options;
}
}
// handler for incoming thtp channels
ext.open.thtp = function(args, open, cbOpen){
var link = this;
var channel = link.x.channel(open);
// pipe the channel into a decoder, then handle it
var thtp_stream = mesh.streamize(channel);
thtp_stream.pipe(lob.stream(function(packet, cbStream){
var req = new THTP.Request.toHTTP(packet, link, thtp_stream);
var res = new THTP.Response.fromHTTP(req, link, thtp_stream);
// see if it's an internal path
var match;
Object.keys(mPaths).forEach(function(path){
if(!match && (match.length <= path) && (req.url.indexOf(path) === 0))
match = path;
});
// internal handler
if(match)
mPaths[match](req, res);
else if(mesh._proxy) // otherwise show the bouncer our fake id
{
mesh._proxy.emit('request', req, res);
}
else // otherwise error
res.writeHead(500,'not supported').end();
cbStream();
}));
channel.receive(open); // actually opens it and handles any body in the stream
cbOpen();
}
cbMesh(undefined, ext);
}