Skip to content
Browse files

New API

  • Loading branch information...
1 parent c6e9741 commit af92818becd028557f08c42c0a1fcaff9ae106ee @akaspin committed Nov 10, 2010
Showing with 470 additions and 157 deletions.
  1. +6 −0 ChangeLog
  2. +0 −8 README.md
  3. +192 −0 chain/cookie.js
  4. +128 −0 chain/writer.js
  5. +40 −149 http.js
  6. +104 −0 readme.md
View
6 ChangeLog
@@ -1,3 +1,9 @@
+In work, Version 0.2
+
+* Change API (in work)
+
+* Docs (in work)
+
Version 0.1
* Initial Version
View
8 README.md
@@ -1,8 +0,0 @@
-# kaph*
-
-*kaph* is loose-coupled set of tools for handle requests under node.js.
-
-*In the Phoenician alphabet letter "kaph" indicates palm.
-
-## Design
-
View
192 chain/cookie.js
@@ -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');
+}
View
128 chain/writer.js
@@ -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);
+};
View
189 http.js
@@ -1,214 +1,105 @@
var inherits = require('util').inherits;
var http = require('http');
-var url = require('url');
-var Buffer = require('buffer').Buffer;
-var crypto = require('crypto');
/**
* Handler for HTTP Request
* @param {http.ServerRequest} request
* @param {http.ServerResponse} response
- * @param {Array} stack Stack of operations. Each operation has next
- * interface: {
- * 'Http method* || DEFAULT': function() { ... },
- * 'ERROR': {String} function(code, message) { ... }
- * }
+ * @param {Array} chain chain of operations. See readme.
* @param {Array} args Optional arguments
* @returns {Handler}
*/
-function HttpHandler(request, response, stack, args) {
+function Handler(request, response, chain, args) {
this.request = request;
this.response = response;
- this.args = args || [];
+ this._args = args || [];
- this.stack = stack;
- this.level = -1; // Level in stack of operations
- this.logger = console;
- this.headersWritten = false;
+ this._chain = chain;
+ this._level = -1; // Level in chain of operations
- // Initial setup
- this.encoding = 'utf8';
- this.statusCode = 200;
- this.headers = {'Content-Type': "text/html; charset=UTF-8"};
+ this.logger = console; // default logger
};
-exports.HttpHandler = HttpHandler;
+exports.Handler = Handler;
/**
- * Executes next operation in stack. In operation selects the current
+ * Executes next operation in chain. In operation selects the current
* request method ("GET", "POST" etc.) or "DEFAULT".
*
* If operation throws error - handles it. To generate a human readable error
* message uses method "ERROR" of operation.
*/
-HttpHandler.prototype.next = function() {
- this.level++;
- var op = this.stack[this.level];
+Handler.prototype.next = function() {
+ this._level++;
+ var op = this.chain[this._level];
try {
var meth = op[this.request.method] || op['DEFAULT'];
- meth.apply(this, this.args);
+ if (!meth) throw new HandlerError(405, 'Operation hasn\'t methods ' +
+ this.request.method + ' or DEFAULT.');
+ return meth.apply(this, this._args);
} catch (e) {
var errGen = op['ERROR'] || ERROR;
this._handleError(e, errGen);
}
};
-/**
- * Sets response status code.
- * @param {Number} code HTTP Status code
- */
-HttpHandler.prototype.setStatus = function(code) {
- if (!this.headersWritten && code in http.STATUS_CODES)
- this.statusCode = code;
-};
-
-/**
- * Set header.
- * @param {String} name
- * @param value
- */
-HttpHandler.prototype.setHeader = function(name, value) {
- var value = value.toString();
- var safe = value.replace(/[\x00-\x1f]/, " ").substring(0, 4000);
- if (safe != value) throw new HttpError('Unsafe header value ' + value);
- this.headers[name] = value;
-};
-
-HttpHandler.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
- */
-HttpHandler.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);
+Handler.prototype.error = function(code, reason) {
+ throw new HandlerError(code, reason);
};
/**
- * Sends a redirect to the given (optionally relative) URL.
- * @param redirectUrl Redirect URL
- * @param permanent Permanent. Default = false
- * @throws HttpError
- */
-HttpHandler.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. If headers already written - send trailers.
- */
-HttpHandler.prototype._sendHeaders = function() {
- this.headersWritten = true;
- this.response.writeHead(this.statusCode, this.headers);
-};
-
-/**
- * Handle error
+ * Handle error.
* @param e error
- * @param errGen Optional error message generator
+ * @param errGen Error message generator
*/
-HttpHandler.prototype._handleError = function(e, errGen) {
- errGen = errGen || ERROR;
- var consoleMessage = '';
+Handler.prototype._handleError = function(e, errGen) {
+ var summary = this.request.method + " " + this.request.url +
+ '(level ' + this.level + '):';
var code = 0;
var message = '';
- if (e instanceof HttpError) {
- consoleMessage = e.code in http.STATUS_CODES ?
- this._summary() + " " + e : "Bad HTTP status code " + e.code;
+ if (e instanceof HandlerError) {
+ message = e.code in http.STATUS_CODES ?
+ summary + " " + e :
+ summary + "Bad HTTP status code " + e.code;
code = e.code;
- message = e.message;
} else {
- consoleMessage = "Uncaught exception in " + this._summary() + "\n" +
+ message = "Uncaught exception in " + summary + "\n" +
(e.stack || e);
code = 500;
- message = (e.stack || e);
}
- this.logger.error(consoleMessage);
+ this.logger.error(message);
- if (!this.headersWritten) {
- this.statusCode = code;
- this.end(errGen(code, message));
- }
-};
-
-/**
- * Get request summary.
- * @returns {String}
- */
-HttpHandler.prototype._summary = function() {
- return this.request.method + " " + this.request.url;
+ this.response.writeHead(code,
+ {'Content-Type': "text/html; charset=UTF-8"});
+ this.response.end(errGen(code, message));
};
/**
- * Default function for genarate error html body.
+ * Default function for generate error html body.
* @param {Number} code
* @param {String} message
* @returns {String}
*/
function ERROR(code, message) {
- var msg = code + ": " + message;
- return "<html><title>" + msg + "</title>" +
- "<body>" + msg + "</body></html>";
+ return "<html><title>" + message + "</title>" +
+ "<body><code>" + message + "</code></body></html>";
};
exports.ERROR = ERROR;
/**
* HTTP Error
* @constructor
* @extends Error
- * @param code Status code
- * @param reason Reason
+ * @param code Optional status code. By default is "500"
+ * @param reason Optional reason message
*/
-function HttpError(code, reason) {
- //Error.call(this);
- this.name = "HttpError";
+function HandlerError(code, reason) {
+ this.name = "HandlerError";
this.code = code || 500;
this.reason = reason || http.STATUS_CODES[this.code] || "Not implemented";
- this.message = this.code + " " + this.reason;
+ this.message = this.code + ": " + this.reason;
Error.captureStackTrace(this);
}
-inherits(HttpError, Error);
-exports.HttpError = HttpError;
+inherits(HandlerError, Error);
+exports.HandlerError = HandlerError;
View
104 readme.md
@@ -0,0 +1,104 @@
+# Kaph*
+
+*Kaph* is loose-coupled set of tools for handle requests under
+[node.js](http://nodejs.org). It's not framework.
+
+*In the Phoenician alphabet letter "kaph" indicates palm.
+
+## Design
+
+Design of *kaph* was inspired by
+[Connect](http://github.com/senchalabs/Connect),
+[Tornado](http://www.tornadoweb.org/) and many others. But unlike them, it was
+designed to make all components as independent from each other without a large
+overhead.
+
+## Usage
+
+To handle request *Kaph* executes *chain* of defined operations.
+
+ var http = require('http');
+ var HttpHandler = require('kaph/http').Handler;
+
+ // Make some operations
+ OpA = {
+ DEFAULT: function() {
+ this.response.writeHead(200,
+ {'Content-Type': "text/html; charset=UTF-8"});
+ this.next();
+ }
+ };
+ OpB = {
+ GET: function(arg) {
+ this.response.end('OpB ' + arg);
+ },
+ ERROR: function(code, message) {
+ return code + ' ' + message;
+ }
+ };
+
+ var chain = [OpA, OpB]; // Our chain
+ var arg = ['Some arg']; // Optional arguments
+
+ http.createServer(function(request, response) {
+ // Just make new kaph handler and call the method "next"
+ (new HttpHandler(request, response, chain, arg)).next();
+ }).listen(9080);
+
+*Kaph* handler receives four arguments:
+
+* `request` is standart *node.js* `http.ServerRequest`
+* `response` is standart *node.js* `http.ServerResponse`
+* `chain` `Array` of operations.
+* `args` Optional `Array` of arguments.
+
+`request` and `response` may not be instances of *node.js* `http` module. They
+simply must have the same behavior. Handler never interfere with their
+implementation. It use only `request.method` property to choose operation
+method (see below) as well as `response`'s `writeHead` and `end` methods
+on exceptions.
+
+Now about `chain`. It's just `Array` of `Objects`. On each iteration *kaph*
+handler invokes operation methods by following these rules:
+
+* If operation has method with name matching `request.method`, it performed as
+ a native method of handler (yes `apply`) with optional arguments.
+* If operation hasn't named method, handler performs method with name
+ `DEFAULT`.
+
+To invoke next operation in chain you must call `next` method of handler.
+
+## Error handling
+
+*Kaph* trying to handle all throwed exceptions without crash entire server. On
+exception *kaph* handler ends request, logs error and try to send it to client.
+To give a better description of exception you can throw new `kaph.HandlerError`
+with two arguments: status `code` and optional `message`. Another way to throw
+exception is handler's `error` method with same arguments.
+
+By default *kaph* handler generates client error message with own `kaph.ERROR`
+function. You can replace it with method `ERROR` of operation. `ERROR` method
+also takes two `code` and `message` arguments and returns `String`.
+
+## Handler properties
+
+Because handler performs the methods of operations as own, it's implementation
+is important. Each instance has next properties:
+
+* `request` ... well, you understand :)
+* `response` ... well, it is also clear :)
+* `logger` is just *node.js* `console`.
+
+## Bundled operations
+
+As stated in the beginning, *kaph* is not framework. But in directory `chain`
+you find a few things that can make life much more pleasant:
+
+* cookie - work with cookies.
+* writer - facade for working with `http.ServerResponse`
+
+## How about router?
+
+I'm use my own [daleth](https://github.com/akaspin/daleth)
+
+

0 comments on commit af92818

Please sign in to comment.
Something went wrong with that request. Please try again.