-
Notifications
You must be signed in to change notification settings - Fork 47
/
Copy paththtp.js
220 lines (184 loc) · 6.56 KB
/
thtp.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
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);
}