Comparing version 0.0.1 to 0.0.2
{ | ||
"name": "web", | ||
"version": "0.0.1", | ||
"version": "0.0.2", | ||
"description": "A small and fast web/http library for nodejs. (replaces the built-in http module)", | ||
@@ -5,0 +5,0 @@ "main": "web.js", |
127
README.md
@@ -54,5 +54,5 @@ # Web | ||
// request.method is "GET", "POST", "PUT", etc.. | ||
// request.url is { pathname: "/foo/bar", query: "and=stuff" ...} | ||
// request is still the request stream, but I'm thinking of moving | ||
// it to a .body property. | ||
// request.url is the raw request path "/foo/bar?and=stuff" | ||
// request.headers is the raw array of request headers. | ||
// request.body is the request body as a stream. | ||
@@ -72,1 +72,122 @@ // respond(code, headers, body) is a function | ||
## Built-in Middleware System | ||
Unlike the `http` module, `web` makes it trivial to stack app layers. There is no need for a library because all that's needed is a simple function. Any function that implements the `(request, respond)` interface is a valid web application. | ||
### Basic Logger Layer | ||
Suppose you wanted to add a layer that logged all request and the response code the app gave them. | ||
This can be done simple as: | ||
```js | ||
function logger(app) { | ||
// Any per-layer startup logic would go here. | ||
// We only need the app closure reference, so there is nothing else to do | ||
return function(req, res) { | ||
// Per request logic during the inward path would go here. Since | ||
// we want to wait till the response code is generated, there is nothing | ||
// to do. | ||
// Forward to the next layer inward. | ||
app(req, function (code, headers, body) { | ||
// Here we've intercepted the response function and can do stuff on the | ||
// way back out of the layers. We want to log the request and response. | ||
console.log(req.method + " " + req.url.path + " " + code); | ||
// Forward to the layers outward. | ||
res(code, headers, body); | ||
}); | ||
}; | ||
} | ||
// Then to use this layer, we just wrap out app. | ||
app = logger(app); | ||
``` | ||
As you can see, there are places to do logic at several steps in a request and server lifetime. | ||
## Request Object | ||
The request object contains the following properties. | ||
### req.method | ||
This is the HTTP method in the request. It can have values like "GET", "POST", "PUT", "DELETE", and any other value the node http parser supports. | ||
### req.headers | ||
This is a hash of the headers with keys lowercases for easy access. | ||
It can be used as: | ||
```js | ||
if (req.headers["content-type"] === "application/json") { ... } | ||
``` | ||
### req.url | ||
This is the result of node's url.parse on the http request url. | ||
`req.url.path` contains the original raw string if desired. | ||
`req.url.pathname` is the path alone without query strings. | ||
### req.versionMajor, req.versionMinor | ||
These two properties tell you the version of the http request. They are usually either (1, 0) or (1, 1) for HTTP 1.0 and HTTP 1.1. | ||
### req.shouldKeepAlive | ||
This is a hint set by node's http parser to tell the middleware if it should | ||
keepalive the connection. | ||
### req.upgrade | ||
This is another hint set by node's http parser. It's true for upgrade request like websocket connections. | ||
### req.body | ||
This property is set by the web library. It's a readable node stream for the request body. | ||
### req.rawHeaders | ||
This is a raw array of alternating key/value pairs. For example, the headers from a curl request look like: | ||
```js | ||
req.rawHeaders = [ | ||
'User-Agent', 'curl/7.26.0', | ||
'Host', 'localhost:8080', | ||
'Accept', '*/*' ] | ||
``` | ||
## Post response processing. | ||
The built-in response function does a bit of post-processing after your app is | ||
done to help your app be a proper http server. These post-processing filters can be configured in the socketHandler function's options argument. | ||
### options.autoDate = true | ||
This options adds a `Date` header with the current date as required by the HTTP spec if your response does not have a `Date` header. | ||
### options.autoServer = "node.js " + process.version | ||
Adds a `Server` header with the running version of node if you don't have a `Server` header. Set to some falsy value to disable or set to a new string to replace. | ||
### options.autoContentLength = true | ||
If your body is a known size (not streaming) and you leave out the `Content-Length` header, this will add one for you. | ||
### options.autoChunked = true | ||
If you leave out a `Content-Length` header and your body is streaming, it will replace your stream with a chunked encoded stream and set the proper `Transfer-Encoding: chunked` header for you. | ||
### options.autoConnection = true | ||
This will try to detect what the `Connection` header should be. Usually either `close` or `keep-alive`. Also it will update `req.shouldKeepAlive` which controls the tcp level connection behavior. | ||
@@ -1,40 +0,12 @@ | ||
var middle = require('./middle'); | ||
var fs = require('fs'); | ||
function app(req, res) { | ||
var isHead; | ||
var method = req.method; | ||
if (method === "HEAD") { | ||
method = "GET"; | ||
isHead = true; | ||
if (req.method === "GET" && req.url.path === "/") { | ||
res(200, { "Content-Type": "text/plain" }, "Hello World\n"); | ||
} | ||
if (!method === "GET") return res(404, {}, ""); | ||
// fs.open(__filename, "r", function (err, fd) { | ||
// if (err) throw err; | ||
// fs.fstat(fd, function (err, stat) { | ||
// if (err) throw err; | ||
// var input; | ||
// if (isHead) { | ||
// fs.close(fd); | ||
// } | ||
// else { | ||
// input = fs.createReadStream(null, {fd:fd}); | ||
// } | ||
// res(200, { | ||
// "ETag": '"' + stat.ino.toString(36) + "-" + stat.size.toString(36) + "-" + stat.mtime.valueOf().toString(36) + '"', | ||
// "Content-Type": "application/javascript", | ||
// "Content-Length": stat.size | ||
// }, input); | ||
// }); | ||
// }); | ||
res(200, { | ||
"Content-Type": "text/plain", | ||
}, "Hello World\n"); | ||
else { | ||
res(404, {}, ""); | ||
} | ||
} | ||
app = middle.autoHeaders(app); | ||
app = middle.log(app); | ||
var server = require('net').createServer(require('./web').socketHandler(app)); | ||
@@ -41,0 +13,0 @@ server.listen(8080, function () { |
144
web.js
var HTTPParser = process.binding("http_parser").HTTPParser; | ||
var Stream = require('stream').Stream; | ||
var urlParse = require('url').parse; | ||
@@ -59,3 +60,16 @@ var STATUS_CODES = { | ||
exports.socketHandler = function (app) { | ||
var defaults = { | ||
autoDate: true, | ||
autoServer: "node.js " + process.version, | ||
autoContentLength: true, | ||
autoChunked: true, | ||
autoConnection: true, | ||
}; | ||
exports.socketHandler = function (app, options) { | ||
// Mix the options with the default config. | ||
var config = Object.create(defaults); | ||
for (var key in options) { | ||
config[key] = options[key]; | ||
} | ||
return function (client) { | ||
@@ -65,40 +79,112 @@ var parser = new HTTPParser(HTTPParser.REQUEST); | ||
parser.onHeadersComplete = function (info) { | ||
info.__proto__ = Stream.prototype; | ||
Stream.call(info); | ||
req = info; | ||
req.readable = true; | ||
app(req, function (statusCode, headers, body) { | ||
var reasonPhrase = STATUS_CODES[statusCode]; | ||
if (!reasonPhrase) { | ||
throw new Error("Invalid response code " + statusCode); | ||
function res(statusCode, headers, body) { | ||
var hasContentLength, hasTransferEncoding, hasDate, hasServer; | ||
for (var key in headers) { | ||
switch (key.toLowerCase()) { | ||
case "date": hasDate = true; continue; | ||
case "server": hasServer = true; continue; | ||
case "content-length": hasContentLength = true; continue; | ||
case "transfer-encoding": hasTransferEncoding = true; continue; | ||
} | ||
var head = "HTTP/1.1 " + statusCode + " " + reasonPhrase + "\r\n"; | ||
for (var key in headers) { | ||
head += key + ": " + headers[key] + "\r\n"; | ||
} | ||
if (!hasDate && config.autoDate) { | ||
headers["Date"] = (new Date).toUTCString(); | ||
} | ||
if (!hasServer && config.autoServer) { | ||
headers["Server"] = config.autoServer; | ||
} | ||
var isStreaming = body && typeof body === "object" && typeof body.pipe === "function"; | ||
if (body && !hasContentLength && !hasTransferEncoding) { | ||
if (!isStreaming && config.autoContentLength) { | ||
body += ""; | ||
headers["Content-Length"] = Buffer.byteLength(body); | ||
hasContentLength = true; | ||
} | ||
head += "\r\n"; | ||
else if (config.autoChunked) { | ||
headers["Transfer-Encoding"] = "chunked"; | ||
hasTransferEncoding = true; | ||
var originalBody = body; | ||
body = new Stream(); | ||
body.readable = true; | ||
var isStreaming = body && typeof body === "object" && typeof body.pipe === "function"; | ||
originalBody.on("data", function (chunk) { | ||
if (Buffer.isBuffer(chunk)) { | ||
body.emit("data", chunk.length.toString(16).toUpperCase() + "\r\n"); | ||
body.emit("data", chunk); | ||
body.emit("data", "\r\n"); | ||
return; | ||
} | ||
var length = Buffer.byteLength(chunk); | ||
body.emit("data", toString(16).toUpperCase() + "\r\n" + chunk + "\r\n"); | ||
}); | ||
if (body && !isStreaming) head += body; | ||
originalBody.on("end", function () { | ||
body.emit("data", "0\r\n\r\n\r\n"); | ||
body.emit("end") | ||
}); | ||
} | ||
} | ||
client.write(head); | ||
if (!isStreaming) { | ||
return done() | ||
if (config.autoConnection) { | ||
if (req.shouldKeepAlive && (hasContentLength || hasTransferEncoding || statusCode == 304)) { | ||
headers["Connection"] = "keep-alive" | ||
} | ||
else { | ||
headers["Connection"] = "close" | ||
req.shouldKeepAlive = false | ||
} | ||
} | ||
body.pipe(client); | ||
body.on("end", done); | ||
var reasonPhrase = STATUS_CODES[statusCode]; | ||
if (!reasonPhrase) { | ||
throw new Error("Invalid response code " + statusCode); | ||
} | ||
var head = "HTTP/1.1 " + statusCode + " " + reasonPhrase + "\r\n"; | ||
for (var key in headers) { | ||
head += key + ": " + headers[key] + "\r\n"; | ||
} | ||
head += "\r\n"; | ||
}); | ||
if (body && !isStreaming) head += body; | ||
client.write(head); | ||
if (!isStreaming) { | ||
return done() | ||
} | ||
body.pipe(client); | ||
body.on("end", done); | ||
} | ||
function done() { | ||
if (req.shouldKeepAlive) { | ||
parser.reinitialize(HTTPParser.REQUEST); | ||
} | ||
else { | ||
client.end(); | ||
} | ||
} | ||
parser.onHeadersComplete = function (info) { | ||
info.body = new Stream(); | ||
info.body.readable = true; | ||
req = info; | ||
var rawHeaders = req.rawHeaders = req.headers; | ||
var headers = req.headers = {}; | ||
for (var i = 0, l = rawHeaders.length; i < l; i += 2) { | ||
headers[rawHeaders[i].toLowerCase()] = rawHeaders[i + 1]; | ||
} | ||
req.url = urlParse(req.url); | ||
app(req, res); | ||
} | ||
parser.onBody = function (buf, start, len) { | ||
req.emit("data", buf.slide(start, len)); | ||
req.body.emit("data", buf.slide(start, len)); | ||
}; | ||
parser.onMessageComplete = function () { | ||
req.emit("end"); | ||
req.body.emit("end"); | ||
}; | ||
@@ -115,11 +201,3 @@ | ||
function done() { | ||
if (req.shouldKeepAlive) { | ||
parser.reinitialize(HTTPParser.REQUEST); | ||
} | ||
else { | ||
client.end(); | ||
} | ||
} | ||
}; | ||
}; |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
14309
192
0
4
192
2