├── .gitignore ├── makefile ├── package.json ├── examples └── example.js ├── README.md ├── test └── pubsubhubbub.test.js └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | REPORTER = spec 2 | test: 3 | @NODE_ENV=test ./node_modules/.bin/mocha \ 4 | --reporter $(REPORTER) \ 5 | --ui tdd 6 | 7 | test-w: 8 | @NODE_ENV=test ./node_modules/.bin/mocha \ 9 | --reporter $(REPORTER) \ 10 | --growl \ 11 | --ui tdd \ 12 | --watch 13 | 14 | .PHONY: test test-w 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pubsubhubbub", 3 | "version": "0.2.5", 4 | "description": "PubSubHubbub subscriber", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "make test" 8 | }, 9 | "homepage": "http://github.com/andris9/pubsubhubbub", 10 | "repository": { 11 | "type": "git", 12 | "url": "http://github.com/andris9/pubsubhubbub" 13 | }, 14 | "author": "Andris Reinman", 15 | "license": "MIT", 16 | "dependencies": { 17 | "request": "*" 18 | }, 19 | "devDependencies": { 20 | "mocha": "*", 21 | "chai": "*" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/example.js: -------------------------------------------------------------------------------- 1 | var pubSubHubbub = require("../index"), 2 | crypto = require("crypto"), 3 | 4 | pubsub = pubSubHubbub.createServer({ 5 | callbackUrl: "http://kreata.ee:1337", 6 | secret: "MyTopSecret" 7 | }), 8 | 9 | topic = "http://testetstetss.blogspot.com/feeds/posts/default", 10 | hub = "http://pubsubhubbub.appspot.com/"; 11 | 12 | pubsub.listen(1337); 13 | 14 | pubsub.on("denied", function(data){ 15 | console.log("Denied"); 16 | console.log(data); 17 | }); 18 | 19 | pubsub.on("subscribe", function(data){ 20 | console.log("Subscribe"); 21 | console.log(data); 22 | 23 | console.log("Subscribed "+topic+" to "+hub); 24 | }); 25 | 26 | pubsub.on("unsubscribe", function(data){ 27 | console.log("Unsubscribe"); 28 | console.log(data); 29 | 30 | console.log("Unsubscribed "+topic+" from "+hub); 31 | }); 32 | 33 | pubsub.on("error", function(error){ 34 | console.log("Error"); 35 | console.log(error); 36 | }); 37 | 38 | pubsub.on("feed", function(data){ 39 | console.log(data) 40 | console.log(data.feed.toString()); 41 | 42 | pubsub.unsubscribe(topic, hub); 43 | }); 44 | 45 | pubsub.on("listen", function(){ 46 | console.log("Server listening on port %s", pubsub.port); 47 | pubsub.subscribe(topic, hub); 48 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # README.md 2 | 3 | PubSubHubbub subscriber module. Supports both 0.3 and 0.4 hubs. 4 | 5 | **NB** Do not upgrade from v0.1.x - the API is totally different 6 | 7 | ## Install 8 | 9 | Install with npm 10 | 11 | npm install pubsubhubbub 12 | 13 | ## Usage 14 | 15 | **pubsubhubbub** starts a HTTP server on selected port. 16 | 17 | var pubSubHubbub = require("pubsubhubbub"); 18 | 19 | var pubSubSubscriber = pubSubHubbub.createServer(options); 20 | 21 | pubSubSubscriber.listen(1337); 22 | 23 | Where options includes the following properties 24 | 25 | * **port** - port to listen 26 | * **callbackUrl** Callback URL for the hub 27 | * **secret** (optional) Secret value for HMAC signatures 28 | * **maxContentSize** (optional) Maximum allowed size of the POST messages 29 | * **username** (optional) Username for HTTP Authentication 30 | * **password** (optional) Password for HTTP Authentication 31 | 32 | ## Events 33 | 34 | * **'listen'** - HTTP server has been set up and is listening for incoming connections 35 | * **'error'** (*err*) - An error has occurred 36 | * **'subscribe'** (*data*) - Subscription for a feed has been updated 37 | * **'unsubscribe'** (*data*) - Subscription for a feed has been cancelled 38 | * **'denied'** (*data*) - Subscription has been denied 39 | * **'feed'** (*data*) - Incoming notification 40 | 41 | ## API 42 | 43 | ### Listen 44 | 45 | Start listening on selected port 46 | 47 | pubSubSubscriber.listen(port) 48 | 49 | Where 50 | 51 | * **port** is the HTTP port to listen 52 | 53 | ### Subscribe 54 | 55 | Subscribe to a feed with 56 | 57 | pubSubSubscriber.subscribe(topic, hub, callback) 58 | 59 | Where 60 | 61 | * **topic** is the URL of the RSS/ATOM feed to subscribe to 62 | * **hub** is the hub for the feed 63 | * **callback** (optional) is the callback function with an error object if the subscription failed 64 | 65 | Example: 66 | 67 | var pubSubSubscriber = pubSubHubbub.createServer(options), 68 | topic = "http://testetstetss.blogspot.com/feeds/posts/default", 69 | hub = "http://pubsubhubbub.appspot.com/"; 70 | 71 | pubSubSubscriber.on("subscribe", function(data){ 72 | console.log(data.topic + " subscribed"); 73 | }); 74 | 75 | pubSubSubscriber.listen(port); 76 | 77 | pubsub.on("listen", function(){ 78 | pubSubSubscriber.subscribe(topic, hub, function(err){ 79 | if(err){ 80 | console.log("Failed subscribing"); 81 | } 82 | }); 83 | }); 84 | 85 | ### Unsubscribe 86 | 87 | Unsubscribe from a feed with 88 | 89 | pubSubSubscriber.unsubscribe(topic, hub, callback) 90 | 91 | Where 92 | 93 | * **topic** is the URL of the RSS/ATOM feed to unsubscribe from 94 | * **hub** is the hub for the feed 95 | * **callback** (optional) is the callback function with an error object if the unsubscribing failed 96 | 97 | Example: 98 | 99 | var pubSubSubscriber = pubSubHubbub.createServer(options), 100 | topic = "http://testetstetss.blogspot.com/feeds/posts/default", 101 | hub = "http://pubsubhubbub.appspot.com/"; 102 | 103 | pubSubSubscriber.on("unsubscribe", function(data){ 104 | console.log(data.topic + " unsubscribed"); 105 | }); 106 | 107 | pubSubSubscriber.listen(port); 108 | 109 | pubsub.on("listen", function(){ 110 | pubSubSubscriber.unsubscribe(topic, hub, function(err){ 111 | if(err){ 112 | console.log("Failed unsubscribing"); 113 | } 114 | }); 115 | }); 116 | 117 | ## Notifications 118 | 119 | Update notifications can be checked with the `'feed'` event. The data object is with the following structure: 120 | 121 | * **topic** - Topic URL 122 | * **hub** - Hub URL, might be undefined 123 | * **callback** - Callback URL that was used by the Hub 124 | * **feed** - Feed XML as a Buffer object 125 | * **headers** - Request headers object 126 | 127 | ## License 128 | 129 | **MIT** -------------------------------------------------------------------------------- /test/pubsubhubbub.test.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect, 2 | http = require('http'), 3 | request = require('request'), 4 | crypto = require('crypto'), 5 | pubSubHubbub = require("../index"); 6 | 7 | var pubsub = pubSubHubbub.createServer({ 8 | callbackUrl: "http://localhost:8000/callback", 9 | secret: "MyTopSecret", 10 | username: "Test", 11 | password: "P@ssw0rd" 12 | }); 13 | 14 | var topic = 'http://test.com', 15 | response_body = "This is a response.", 16 | encrypted_secret = crypto.createHmac("sha1", pubsub.secret).update(topic).digest("hex"); 17 | hub_encryption = crypto.createHmac('sha1', encrypted_secret).update(response_body).digest('hex'); 18 | 19 | var notification = function (){ 20 | var options = { 21 | url: 'http://localhost:8000', 22 | headers: { 23 | 'X-Hub-Signature': 'sha1='+hub_encryption, 24 | 'X-PubSubHubbub-Callback': 'http://localhost:8000/callback', 25 | 'hub.topic': 'http://test.com', 26 | 'link': '; rel="self", ; rel="hub"', 27 | } 28 | } 29 | return request.post(options); 30 | } 31 | 32 | describe('pubsubhubbub notification', function () { 33 | before(function () { 34 | pubsub.listen(8000); 35 | }); 36 | 37 | it('should return 400 - no topic', function (done) { 38 | var options = { 39 | url: 'http://localhost:8000', 40 | headers: { 41 | 'link': '; rel="hub"' 42 | } 43 | } 44 | request.post(options, function (err, res, body) { 45 | expect(res.statusCode).to.equal(400); 46 | done(); 47 | }); 48 | }); 49 | 50 | it('should return 403 - no X-Hub-Signature', function (done){ 51 | var options = { 52 | url: 'http://localhost:8000', 53 | headers: { 54 | 'link': '; rel="self", ; rel="hub"', 55 | } 56 | } 57 | request.post(options, function (err, res, body) { 58 | expect(res.statusCode).to.equal(403); 59 | done(); 60 | }); 61 | }); 62 | 63 | it('should return 202 - signature does not match', function (done) { 64 | var options = { 65 | url: 'http://localhost:8000', 66 | headers: { 67 | 'X-Hub-Signature': 'sha1='+hub_encryption, 68 | 'link': '; rel="self", ; rel="hub"', 69 | }, 70 | body: response_body + "potentially malicious content" 71 | } 72 | request.post(options, function (err, res, body) { 73 | expect(res.statusCode).to.equal(202); 74 | done(); 75 | }); 76 | }); 77 | 78 | it('should return 204 - sucessful request', function (done) { 79 | var options = { 80 | url: 'http://localhost:8000', 81 | headers: { 82 | 'X-Hub-Signature': 'sha1='+hub_encryption, 83 | 'link': '; rel="self", ; rel="hub"', 84 | }, 85 | body: response_body 86 | } 87 | request.post(options, function (err, res, body) { 88 | expect(res.statusCode).to.equal(204); 89 | done(); 90 | }); 91 | }); 92 | 93 | it('should emit a feed event - successful request', function (done) { 94 | var eventFired = false; 95 | var options = { 96 | url: 'http://localhost:8000', 97 | headers: { 98 | 'X-Hub-Signature': 'sha1='+hub_encryption, 99 | 'link': '; rel="self", ; rel="hub"', 100 | }, 101 | body: response_body 102 | } 103 | request.post(options, function (err, res, body) {}); 104 | 105 | pubsub.on('feed', function () { 106 | eventFired = true; 107 | }); 108 | 109 | setTimeout(function(){ 110 | expect(eventFired).to.equal(true); 111 | done(); 112 | }, 10); 113 | }); 114 | 115 | it('should not emit a feed event - signature does not match', function (done) { 116 | var eventFired = false; 117 | var options = { 118 | url: 'http://localhost:8000', 119 | headers: { 120 | 'X-Hub-Signature': 'sha1='+hub_encryption, 121 | 'link': '; rel="self", ; rel="hub"', 122 | }, 123 | body: response_body + "potentially malicious content" 124 | } 125 | request.post(options, function (err, res, body) {}); 126 | 127 | pubsub.on('feed', function () { 128 | eventFired = true; 129 | }); 130 | 131 | setTimeout( function() { 132 | expect(eventFired).to.equal(false); 133 | done(); 134 | }, 10); 135 | }); 136 | 137 | after(function () { 138 | pubsub.server.close(); 139 | }); 140 | }); 141 | 142 | suite("pubsubhubbub creation", function () { 143 | test("pubsub should exist", function () { 144 | expect(pubsub).to.exist; 145 | }); 146 | 147 | test("options passed correctly", function () { 148 | expect(pubsub.callbackUrl).to.equal("http://localhost:8000/callback"); 149 | expect(pubsub.secret).to.equal("MyTopSecret"); 150 | }); 151 | 152 | test("create authentication object", function () { 153 | expect(pubsub.auth).to.exist; 154 | expect(pubsub.auth.user).to.equal("Test"); 155 | expect(pubsub.auth.pass).to.equal("P@ssw0rd"); 156 | 157 | expect(pubsub.auth).to.eql({ 158 | 'user': 'Test', 159 | 'pass': 'P@ssw0rd', 160 | 'sendImmediately': false 161 | }); 162 | }); 163 | 164 | }); 165 | 166 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var request = require("request"), 2 | http = require("http"), 3 | urllib = require("url"), 4 | Stream = require("stream").Stream, 5 | utillib = require("util"), 6 | crypto = require("crypto"); 7 | 8 | // Expose to the world 9 | /** 10 | * Creates a PubSubHubbub subscriber service as a HTTP server. 11 | * Usage: 12 | * pubsub = createServer(options); 13 | * pubsub.listen(1337); 14 | * 15 | * @param {Object} [options] Options object 16 | * @param {String} [options.callbackUrl] Callback URL for the hub 17 | * @param {String} [options.secret] Secret value for HMAC signatures 18 | * @param {Number} [options.maxContentSize] Maximum allowed size of the POST messages 19 | * @param {String} [options.username] Username for HTTP Authentication 20 | * @param {String} [options.password] Password for HTTP Authentication 21 | * @return {Object} A PubSubHubbub server object 22 | */ 23 | module.exports.createServer = function(options){ 24 | return new PubSubHubbub(options); 25 | } 26 | 27 | /** 28 | * Create a PubSubHubbub client handler object. HTTP server is set up to listen 29 | * the responses from the hubs. 30 | * 31 | * @constructor 32 | * @param {Object} [options] Options object 33 | * @param {String} [options.callbackUrl] Callback URL for the hub 34 | * @param {String} [options.secret] Secret value for HMAC signatures 35 | * @param {Number} [options.maxContentSize] Maximum allowed size of the POST messages 36 | * @param {String} [options.username] Username for HTTP Authentication 37 | * @param {String} [options.password] Password for HTTP Authentication 38 | */ 39 | function PubSubHubbub(options){ 40 | Stream.call(this); 41 | 42 | options = options || {}; 43 | 44 | this.secret = options.secret || false; 45 | this.callbackUrl = options.callbackUrl; 46 | this.maxContentSize = options.maxContentSize || 3 * 1024 * 1024; 47 | 48 | if (options.username) { 49 | this.auth = { 50 | 'user': options.username, 51 | 'pass': options.password, 52 | 'sendImmediately': false 53 | } 54 | } 55 | } 56 | utillib.inherits(PubSubHubbub, Stream); 57 | 58 | // PUBLIC API 59 | 60 | /** 61 | * Start listening on selected port 62 | * 63 | * @param {Number} port Port number for the HTTP server 64 | */ 65 | PubSubHubbub.prototype.listen = function(port){ 66 | this.port = port; 67 | 68 | this.server = http.createServer(this._onRequest.bind(this)); 69 | this.server.on("error", this._onError.bind(this)); 70 | this.server.on("listening", this._onListening.bind(this)); 71 | 72 | this.server.listen(this.port); 73 | } 74 | 75 | /** 76 | * Subsribe for a topic at selected hub 77 | * 78 | * @param {String} topic Atom or RSS feed URL 79 | * @param {String} hub Hub URL 80 | * @param {String} [callbackUrl] Define callback url for the hub, do not use the default 81 | * @param {Function} [callback] Callback function, might not be very useful 82 | */ 83 | PubSubHubbub.prototype.subscribe = function(topic, hub, callbackUrl, callback){ 84 | this.setSubscription("subscribe", topic, hub, callbackUrl, callback); 85 | } 86 | 87 | /** 88 | * Subsribe a topic at selected hub 89 | * 90 | * @param {String} topic Atom or RSS feed URL 91 | * @param {String} hub Hub URL 92 | * @param {String} [callbackUrl] Define callback url for the hub, do not use the default 93 | * @param {Function} [callback] Callback function, might not be very useful 94 | */ 95 | PubSubHubbub.prototype.unsubscribe = function(topic, hub, callbackUrl, callback){ 96 | this.setSubscription("unsubscribe", topic, hub, callbackUrl, callback); 97 | } 98 | 99 | /** 100 | * Subsribe or unsubscribe a topic at selected hub 101 | * 102 | * @param {String} mode Either "subscribe" or "unsubscribe" 103 | * @param {String} topic Atom or RSS feed URL 104 | * @param {String} hub Hub URL 105 | * @param {String} [callbackUrl] Define callback url for the hub, do not use the default 106 | * @param {Function} [callback] Callback function, might not be very useful 107 | */ 108 | PubSubHubbub.prototype.setSubscription = function(mode, topic, hub, callbackUrl, callback){ 109 | 110 | if(!callback && typeof callbackUrl == "function"){ 111 | callback = callbackUrl; 112 | callbackUrl = undefined; 113 | } 114 | 115 | // by default the topic url is added as a GET parameter to the callback url 116 | callbackUrl = callbackUrl || this.callbackUrl + 117 | (this.callbackUrl.replace(/^https?:\/\//i, "").match(/\//)?"":"/") + 118 | (this.callbackUrl.match(/\?/)?"&":"?") + 119 | "topic="+encodeURIComponent(topic)+ 120 | "&hub="+encodeURIComponent(hub); 121 | 122 | var form = { 123 | "hub.callback": callbackUrl, 124 | "hub.mode": mode, 125 | "hub.topic": topic, 126 | "hub.verify": "async" 127 | }, 128 | 129 | postParams = { 130 | url: hub, 131 | form: form, 132 | encoding: "utf-8" 133 | }; 134 | 135 | if (this.auth) { 136 | postParams.auth = this.auth; 137 | } 138 | 139 | if(this.secret){ 140 | // do not use the original secret but a generated one 141 | form["hub.secret"] = crypto.createHmac("sha1", this.secret).update(topic).digest("hex"); 142 | } 143 | 144 | request.post(postParams, function(error, response, responseBody){ 145 | 146 | if(error){ 147 | if(callback){ 148 | return callback(error); 149 | }else{ 150 | return this.emit("denied", {topic: topic, error: error}); 151 | } 152 | } 153 | 154 | if(response.statusCode != 202 && response.statusCode != 204){ 155 | var err = new Error("Invalid response status " + response.statusCode); 156 | err.responseBody = (responseBody || "").toString(); 157 | if(callback){ 158 | return callback(err); 159 | }else{ 160 | return this.emit("denied", {topic: topic, error: err}); 161 | } 162 | } 163 | 164 | return callback && callback(null, topic); 165 | }); 166 | } 167 | 168 | // PRIVATE API 169 | 170 | /** 171 | * Request handler. Will be fired when a client (hub) opens a connection to the server 172 | * 173 | * @event 174 | * @param {Object} req HTTP Request object 175 | * @param {Object} res HTTP Response object 176 | */ 177 | PubSubHubbub.prototype._onRequest = function(req, res){ 178 | switch(req.method){ 179 | case "GET": return this._onGetRequest(req, res); 180 | case "POST": return this._onPostRequest(req, res); 181 | default: 182 | return this._sendError(req, res, 405, "Method Not Allowed"); 183 | } 184 | } 185 | 186 | /** 187 | * Error event handler for the HTTP server 188 | * 189 | * @event 190 | * @param {Error} error Error object 191 | */ 192 | PubSubHubbub.prototype._onError = function(error){ 193 | if(error.syscall == "listen"){ 194 | error.message = "Failed to start listening on port " + this.port + " ("+error.code+")"; 195 | this.emit("error", error); 196 | }else{ 197 | this.emit("error", error); 198 | } 199 | } 200 | 201 | /** 202 | * Will be fired when HTTP server has successfully started listening on the selected port 203 | * 204 | * @event 205 | */ 206 | PubSubHubbub.prototype._onListening = function(){ 207 | this.emit("listen"); 208 | } 209 | 210 | /** 211 | * GET request handler for the HTTP server. This should be called when the server 212 | * tries to verify the intent of the subscriber. 213 | * 214 | * @param {Object} req HTTP Request object 215 | * @param {Object} res HTTP Response object 216 | */ 217 | PubSubHubbub.prototype._onGetRequest = function(req, res){ 218 | var params = urllib.parse(req.url, true, true), 219 | data; 220 | 221 | // Does not seem to be a valid PubSubHubbub request 222 | if(!params.query["hub.topic"] || !params.query['hub.mode']){ 223 | return this._sendError(req, res, 400, "Bad Request"); 224 | } 225 | 226 | switch(params.query['hub.mode']){ 227 | case "denied": 228 | res.writeHead(200, {'Content-Type': 'text/plain'}); 229 | data = {topic: params.query["hub.topic"], hub: params.query.hub}; 230 | res.end(params.query['hub.challenge'] || "ok"); 231 | break; 232 | case "subscribe": 233 | case "unsubscribe": 234 | res.writeHead(200, {'Content-Type': 'text/plain'}); 235 | res.end(params.query['hub.challenge']); 236 | data = { 237 | lease: Number(params.query["hub.lease_seconds"] || 0) + Math.round(Date.now()/1000), 238 | topic: params.query["hub.topic"], 239 | hub: params.query.hub 240 | }; 241 | break; 242 | default: 243 | // Not a valid mode 244 | return this._sendError(req, res, 403, "Forbidden"); 245 | } 246 | 247 | // Emit subscription information 248 | this.emit(params.query["hub.mode"], data); 249 | } 250 | 251 | /** 252 | * POST request handler. Should be called when the hub tries to notify the subscriber 253 | * with new data 254 | * 255 | * @param {Object} req HTTP Request object 256 | * @param {Object} res HTTP Response object 257 | */ 258 | PubSubHubbub.prototype._onPostRequest = function(req, res){ 259 | var bodyChunks = [], 260 | params = urllib.parse(req.url, true, true), 261 | topic = params && params.query && params.query.topic, 262 | hub = params && params.query && params.query.hub, 263 | bodyLen = 0, 264 | tooLarge = false, 265 | signatureParts, algo, signature, hmac; 266 | 267 | // v0.4 hubs have a link header that includes both the topic url and hub url 268 | (req.headers && req.headers.link || ""). 269 | replace(/<([^>]+)>\s*(?:;\s*rel=['"]([^'"]+)['"])?/gi, function(o, url, rel){ 270 | switch((rel || "").toLowerCase()){ 271 | case "self": 272 | topic = url; 273 | break; 274 | case "hub": 275 | hub = url; 276 | break; 277 | } 278 | }); 279 | 280 | if(!topic){ 281 | return this._sendError(req, res, 400, "Bad Request"); 282 | } 283 | 284 | // Hub must notify with signature header if secret specified. 285 | if(this.secret && !req.headers['x-hub-signature']){ 286 | return this._sendError(req, res, 403, "Forbidden"); 287 | } 288 | 289 | if(this.secret){ 290 | signatureParts = req.headers['x-hub-signature'].split("="); 291 | algo = (signatureParts.shift() || "").toLowerCase(); 292 | signature = (signatureParts.pop() || "").toLowerCase(); 293 | 294 | try{ 295 | hmac = crypto.createHmac(algo, crypto.createHmac("sha1", this.secret).update(topic).digest("hex")); 296 | }catch(E){ 297 | return this._sendError(req, res, 403, "Forbidden"); 298 | } 299 | } 300 | 301 | req.on("data", (function(chunk){ 302 | if(!chunk || !chunk.length || tooLarge){ 303 | return; 304 | } 305 | 306 | if(bodyLen + chunk.length <= this.maxContentSize){ 307 | bodyChunks.push(chunk); 308 | bodyLen += chunk.length; 309 | if(this.secret){ 310 | hmac.update(chunk); 311 | } 312 | }else{ 313 | tooLarge = true; 314 | } 315 | 316 | chunk = null; 317 | }).bind(this)); 318 | 319 | req.on("end", (function(){ 320 | if(tooLarge){ 321 | return this._sendError(req, res, 413, "Request Entity Too Large"); 322 | } 323 | 324 | // Must return 2xx code even if signature doesn't match. 325 | if(this.secret && hmac.digest("hex").toLowerCase() != signature){ 326 | res.writeHead(202, {'Content-Type': 'text/plain; charset=utf-8'}); 327 | return res.end(); 328 | } 329 | 330 | res.writeHead(204, {'Content-Type': 'text/plain; charset=utf-8'}); 331 | res.end(); 332 | 333 | this.emit("feed", { 334 | topic: topic, 335 | hub: hub, 336 | callback: "http://" + req.headers.host + req.url, 337 | feed: Buffer.concat(bodyChunks, bodyLen), 338 | headers: req.headers 339 | }); 340 | 341 | }).bind(this)); 342 | } 343 | 344 | /** 345 | * Generates and sends an error message as the response for a HTTP request 346 | * 347 | * @param {Object} req HTTP Request object 348 | * @param {Object} res HTTP Response object 349 | * @param {Number} code HTTP response status 350 | * @param {String} message Error message to display 351 | */ 352 | PubSubHubbub.prototype._sendError = function(req, res, code, message){ 353 | res.writeHead(code, {"Content-Type": "text/html"}); 354 | res.end("\n"+ 355 | "\n"+ 356 | " \n"+ 357 | " \n"+ 358 | " " + code + " " + message + "\n"+ 359 | " \n"+ 360 | " \n"+ 361 | "

" + code + " " + message + "

\n"+ 362 | " \n"+ 363 | ""); 364 | } --------------------------------------------------------------------------------