Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
470 additions
and
157 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -1,3 +1,9 @@ | |||
In work, Version 0.2 | |||
|
|||
* Change API (in work) | |||
|
|||
* Docs (in work) | |||
|
|||
Version 0.1 | Version 0.1 | ||
|
|
||
* Initial Version | * Initial Version |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -0,0 +1,192 @@ | |||
var crypto = require('crypto'); | |||
var Buffer = require('buffer').Buffer; | |||
|
|||
exports.secret = hex_hmac_sha1(Math.random(), Math.random()); | |||
|
|||
/** | |||
* Kaph stack Cookie operation. | |||
*/ | |||
Op = { | |||
DEFAULT: function() { | |||
this.cookies = new Proc(this.request, this.response); | |||
return this.next(); | |||
} | |||
}; | |||
exports.Op = Op; | |||
|
|||
/** | |||
* Cookie processor. | |||
* @param {http.ServerRequest} request | |||
* @param {http.ServerResponse} response | |||
* @returns {Proc} | |||
*/ | |||
function Proc(request, response) { | |||
this._request = request; | |||
this._response = response; | |||
} | |||
exports.Proc = Proc; | |||
/** | |||
* Set cookie. At first call decorates method `writeHead` of instance | |||
* `http.ServerResponse`. | |||
* @param {String} name | |||
* @param value Cookie value | |||
* @param {Object} options Optional options. See readme. | |||
* @param {Boolean} encrypt Encrypt cookie value | |||
*/ | |||
Proc.prototype.set = function(name, value, options, encrypt) { | |||
options = options || {}; | |||
if (!this._outgoing) { | |||
this._outgoing = {}; | |||
|
|||
// Decorate original method | |||
var _writeHead = this._response.writeHead; | |||
var COOKIE_KEY = 'Set-Cookie', slice = Array.prototype.slice; | |||
var self = this; | |||
this._response.writeHead = function() { | |||
// Honor the passed args and method signature | |||
// (see http.writeHead docs) | |||
var args = slice.call(arguments), headers = args[args.length - 1]; | |||
if (!headers || typeof (headers) != 'object') { | |||
// No header arg - create and append to args list | |||
args.push(headers = []); | |||
} | |||
|
|||
// Merge cookie values | |||
var prev = headers[COOKIE_KEY], cookies = self.deploy() || []; | |||
if (prev) cookies.push(prev); | |||
if (cookies.length > 0) | |||
headers[COOKIE_KEY] = cookies; | |||
|
|||
// Invoke original writeHead() | |||
_writeHead.apply(this, args); | |||
}; | |||
} | |||
|
|||
// determine expiration date | |||
if (options.days) { | |||
options.expires = options.expires || new Date(); | |||
options.expires.setDate(options.expires.getDate() + options.days); | |||
} | |||
|
|||
// Serve value | |||
value = (value !== null && typeof value !== 'undefined') ? | |||
value.toString() : ''; | |||
|
|||
if (encrypt) { | |||
// If value is secure - encrypt | |||
value = [value.length, encode(value), options.expires ]; | |||
var signature = hex_hmac_sha1(value.join("|"), exports.secret); | |||
value.push(signature); | |||
value = value.join('|').replace(/=/g, '*'); | |||
} | |||
|
|||
// Form cookie | |||
var cookie = name + '=' + escape(value) + ';'; | |||
options.expires && (cookie += ' expires=' + | |||
options.expires.toUTCString() + ";"); | |||
options.path && (cookie += ' path=' + options.path + ';'); | |||
options.domain && (cookie += ' domain=' + options.domain + ';'); | |||
options.secure && (cookie += ' secure;'); | |||
options.httpOnly && (cookie += ' httponly'); | |||
|
|||
this._outgoing[name] = cookie; | |||
}; | |||
|
|||
/** | |||
* Get cookie by name | |||
* @param {String} name | |||
* @param {Boolean} decrypt | |||
*/ | |||
Proc.prototype.get = function(name, decrypt) { | |||
// Parse cookies if not yet parsed | |||
if (!this._incoming) { | |||
var header = this._request.headers["cookie"] || ""; | |||
var self = this; | |||
this._incoming = {}; | |||
|
|||
header.split(";").forEach( function( cookie ) { | |||
var parts = cookie.split("="), | |||
name = (parts[0] ? parts[0].trim() : ''), | |||
value = (parts[1] ? parts[1].trim() : ''); | |||
self._incoming[name] = unescape(value); | |||
}); | |||
} | |||
|
|||
var value = this._incoming[name]; | |||
|
|||
// Decript value if needed | |||
if (decrypt && value) { | |||
var parts = value.replace(/\*/g, '=').split("|"); | |||
if (parts.length !== 4) { | |||
return; | |||
} | |||
|
|||
var len = parts[0]; | |||
value = decode(parts[1]).substr(0, len); | |||
var expires = new Date(+parts[2]); | |||
var remoteSig = parts[3]; | |||
|
|||
if ( expires < new Date ) { | |||
return; | |||
} | |||
|
|||
var localSig = hex_hmac_sha1(parts.slice(0, 3).join("|"), | |||
exports.secret); | |||
|
|||
if ( localSig !== remoteSig ) { | |||
throw new Error("invalid cookie signature: " + name); | |||
} | |||
return value; | |||
} | |||
return value; | |||
}; | |||
|
|||
/** | |||
* Clears cookie | |||
* @param {String} name | |||
*/ | |||
Proc.prototype.clear = function(name) { | |||
options = {expires: new Date( +new Date - 30 * 24 * 60 * 60 * 1000) }; | |||
this.set(name, '', options); | |||
}; | |||
|
|||
/** | |||
* Generate "Set-Cookie" header value | |||
* @returns {Array} | |||
*/ | |||
Proc.prototype.deploy = function() { | |||
if (this._outgoing) return; | |||
var stream = []; | |||
for (var k in this._outgoing) { | |||
stream.push(this._outgoing[k]); | |||
} | |||
return stream; | |||
}; | |||
|
|||
/** | |||
* Generate hash. | |||
* @param data data | |||
* @returns {String} hash | |||
*/ | |||
function hex_hmac_sha1(data, key) { | |||
var hmac = crypto.createHmac('sha1', key); | |||
hmac.update(data); | |||
return hmac.digest('hex'); | |||
} | |||
|
|||
/** | |||
* Encode data to base64 | |||
* @param data | |||
* @returns {String} | |||
*/ | |||
function encode(data) { | |||
return (new Buffer(data)).toString('base64'); | |||
} | |||
/** | |||
* Decode data from base64 | |||
* @param data | |||
* @returns {String} | |||
*/ | |||
function decode(data) { | |||
return (new Buffer(data, 'base64')).toString('utf8'); | |||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -0,0 +1,128 @@ | |||
var url = require('url'); | |||
var Buffer = require('buffer').Buffer; | |||
var crypto = require('crypto'); | |||
|
|||
/** | |||
* Kaph stack Writer operation. Just makes writer object in Handler. | |||
*/ | |||
Op = { | |||
DEFAULT: function() { | |||
this.writer = new Writer(this.request, this.response); | |||
return this.next(); | |||
} | |||
}; | |||
exports.Op = Op; | |||
|
|||
/** | |||
* | |||
* @param request | |||
* @param response | |||
* @returns {Writer} | |||
*/ | |||
function Writer(request, response) { | |||
this._request = request; | |||
this._response = response; | |||
|
|||
this._headersWritten = false; | |||
this._encoding = 'utf8'; | |||
this._statusCode = 200; | |||
this._headers = {'Content-Type': "text/html; charset=UTF-8"}; | |||
} | |||
exports.Writer = Writer; | |||
|
|||
/** | |||
* Sets response status code. | |||
* @param {Number} code HTTP Status code | |||
*/ | |||
Handler.prototype.setStatus = function(code) { | |||
if (!this.headersWritten && code in http.STATUS_CODES) | |||
this.statusCode = code; | |||
}; | |||
|
|||
/** | |||
* Set response header. | |||
* @param {String} name | |||
* @param value | |||
*/ | |||
Writer.prototype.setHeader = function(name, value) { | |||
value = value.toString(); | |||
if (value != value.replace(/[\x00-\x1f]/, " ").substring(0, 4000)) | |||
throw new Error('Unsafe header value ' + value); | |||
this._headers[name] = value; | |||
}; | |||
|
|||
/** | |||
* Write to response | |||
* @param data | |||
* @param encoding | |||
*/ | |||
Writer.prototype.write = function(data, encoding) { | |||
if (!data || data == null) return; // If no data - do nothing | |||
|
|||
if (!this._headersWritten) this._sendHeaders(); | |||
this._response.write(data, (encoding || this._encoding)); | |||
}; | |||
|
|||
/** | |||
* | |||
* @param data | |||
* @param encoding | |||
*/ | |||
Writer.prototype.end = function(data, encoding) { | |||
if (!this._headersWritten) { | |||
// if any data present - add some info in headers: | |||
if (!!data && this._statusCode == 200 | |||
&& this._request.method == 'GET') { | |||
// ETag | |||
if (!('ETag' in this._headers)) { | |||
etag = '"' + crypto.createHash("sha1"). | |||
update(data).digest("hex") + '"'; | |||
|
|||
// Check if-none-match | |||
var inm = this._request.headers["if-none-match"]; | |||
if (inm && inm.indexOf(etag) != -1) { | |||
// Not modified - just send 304 | |||
this._response.writeHead(304); | |||
this._response.end(); | |||
return; | |||
} else { | |||
this.setHeader("ETag", etag); | |||
} | |||
} | |||
|
|||
// Content-Length | |||
if (!("Content-Length" in this._headers)) { | |||
var l = Buffer.isBuffer(data) ? data.length | |||
: Buffer.byteLength(data, encoding || this._encoding); | |||
this.setHeader("Content-Length", l); | |||
} | |||
} | |||
this._sendHeaders(); | |||
} | |||
this._response.end(data, encoding || this._encoding); | |||
}; | |||
|
|||
/** | |||
* Sends a redirect to the given (optionally relative) URL. | |||
* @param redirectUrl Redirect URL | |||
* @param permanent Permanent. Default = false | |||
* @throws HttpError | |||
*/ | |||
Writer.prototype.redirect = function(redirectUrl, permanent) { | |||
permanent = permanent || false; | |||
if (this._headersWritten) { | |||
throw new Error('Cannot redirect after headers have been written'); | |||
} | |||
this.setStatus(permanent ? 301 : 302); | |||
redirectUrl = redirectUrl.replace(/[\x00-\x1f]/, ""); | |||
this.setHeader('Location', url.resolve(this.request.url, redirectUrl)); | |||
this.end(); | |||
}; | |||
|
|||
/** | |||
* Sends all headers to client. | |||
*/ | |||
Writer.prototype._sendHeaders = function() { | |||
this._headersWritten = true; | |||
this._response.writeHead(this._statusCode, this._headers); | |||
}; |
Oops, something went wrong.