Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial commit

  • Loading branch information...
commit b9fd401fc765b3b5275c5378b41cca7df6a26d3a 0 parents
Michael Jackson authored
Showing with 3,800 additions and 0 deletions.
  1. +2 −0  .gitignore
  2. +58 −0 README
  3. +114 −0 SPEC
  4. +3 −0  bin/linkup
  5. +163 −0 lib/link.js
  6. +98 −0 lib/link/builder.js
  7. +36 −0 lib/link/commonlogger.js
  8. +23 −0 lib/link/contentlength.js
  9. +17 −0 lib/link/contenttype.js
  10. +35 −0 lib/link/input.js
  11. +171 −0 lib/link/lint.js
  12. +33 −0 lib/link/methodoverride.js
  13. +614 −0 lib/link/mime.js
  14. +118 −0 lib/link/mock.js
  15. +529 −0 lib/link/multipart.js
  16. +29 −0 lib/link/querystring.js
  17. +451 −0 lib/link/request.js
  18. +86 −0 lib/link/response.js
  19. +149 −0 lib/link/session/cookie.js
  20. +35 −0 lib/link/static.js
  21. +107 −0 lib/link/urlmap.js
  22. +145 −0 lib/link/utils.js
  23. +8 −0 package.json
  24. BIN  test/_files/binary
  25. +6 −0 test/_files/content_type_no_filename
  26. +7 −0 test/_files/filename_and_modification_param
  27. +6 −0 test/_files/filename_with_escaped_quotes
  28. +6 −0 test/_files/filename_with_percent_escaped_quotes
  29. +6 −0 test/_files/filename_with_unescaped_quotes
  30. +7 −0 test/_files/filename_with_unescaped_quotes_and_modification_param
  31. +21 −0 test/_files/mixed_files
  32. +9 −0 test/_files/none
  33. +15 −0 test/_files/text
  34. +6 −0 test/_files/text_ie
  35. +32 −0 test/_files/webkit
  36. +118 −0 test/link_test.js
  37. +34 −0 test/mime_test.js
  38. +44 −0 test/mock_test.js
  39. +266 −0 test/multipart_test.js
  40. +47 −0 test/querystring_test.js
  41. +117 −0 test/request_test.js
  42. +29 −0 test/utils_test.js
2  .gitignore
@@ -0,0 +1,2 @@
+.DS_Store
+node_modules
58 README
@@ -0,0 +1,58 @@
+Link is a modular web server interface for Node.js. Using Link, web developers
+can build highly performant servers in a powerful, modular style that encourages
+readability and clean separation of responsibility. The core Link distribution
+consists of three things:
+
+ - A specification (see SPEC) for building applications and middleware
+ - A library (see lib) with many useful utilities and middleware to aid
+ developers in the common tasks of building applications that conform to the
+ specification
+ - An executable (see bin/linkup) for running Link applications from the
+ command line
+
+= Installation
+
+Using npm:
+
+ $ npm install link
+
+= Usage
+
+Link reduces the complex task of building a web application to the simplest
+terms possible. A Link application is simply a JavaScript function that takes
+two arguments: the environment and a callback. The environment is a JavaScript
+object that contains information about the incoming request such as the request
+method that was used, any parameters that were sent, etc. When the application
+is ready to send the response to the client, it simply calls the callback with
+the response status code, the headers, and the response body.
+
+ function (env, callback) {
+ callback(200, {
+ "Content-Type": "text/plain",
+ "Content-Length": "11"
+ }, "hello world");
+ }
+
+= Tests
+
+To run the tests, first install vows:
+
+ $ npm install vows
+
+Run all tests with:
+
+ $ ./node_modules/.bin/vows test/*_test.js
+
+Otherwise, run the tests for a specific module with:
+
+ $ ./node_modules/.bin/vows test/utils_test.js
+
+= Credits
+
+Link was inspired by similar efforts in the Python and Ruby communities, namely
+WSGI and Rack. It borrows many code patterns from these libraries, as well as
+the JSGI project. Link's multipart parser is based on the fast parser in the
+node-formidable project.
+
+My sincere thanks to the authors of each of these libraries for the excellent
+work they've done and graciously shared.
114 SPEC
@@ -0,0 +1,114 @@
+This specification aims to formalize the Link protocol for building web
+applications using the Node.js JavaScript platform. You can (and should) use
+the lint middleware to enforce it. When you develop middleware, be sure to add
+a lint before and after to catch all mistakes.
+
+= Link Applications
+
+A Link application is a JavaScript function that takes exactly two arguments:
+the *environment* and a *callback*.
+
+== The Environment
+
+The environment is a JavaScript object that includes CGI-like properties. It
+must include the following properties except when they would be empty, but see
+below.
+
+ - protocol The protocol used in the request (i.e. "http:" or
+ "https:"). This variable may never be an empty string and
+ is always required.
+ - protocolVersion The version of the protocol used in the request. This
+ variable may never be an empty string and is always
+ required.
+ - requestMethod The request method (e.g. "GET" or "POST"). This cannot
+ ever be an empty string, and is always required.
+ - serverName, When combined with scriptName and pathInfo these
+ serverPort variables may be used to reconstruct the original
+ request URL. Note, however, that if httpHost is present,
+ it should be used in preference to serverName. These
+ variables can never be empty strings, and are always
+ required.
+ - scriptName The initial portion of the request URL's "path" that
+ corresponds to the application, so that it knows its
+ virtual "location". This may be an empty string, if the
+ application corresponds to the "root" of the server.
+ - pathInfo The remainder of the request URL's "path", designating
+ the virtual "location" of the target resource within the
+ application. This may be an empty string if the request
+ URL targets the root of the application and does not
+ have a trailing slash. This value may be percent-encoded
+ when originating from a URL.
+ - queryString The portion of the request URL that follows the "?", if
+ any. May be an empty string, but is always required.
+ - http* Variables corresponding to the client-supplied HTTP
+ request headers (i.e. variables whose names begin with
+ http). The presence or absence of these variables should
+ correspond with the presence or absence of the
+ appropriate HTTP header in the request. The remainder of
+ the property name will be the camel-cased version of the
+ original header name (e.g. "httpAccept" and
+ "httpUserAgent").
+
+The environment must not contain the properties httpContentType or
+httpContentLength (use contentType and contentLength instead).
+
+In addition to these, the environment must include the following Link-specific
+variables:
+
+ - link.version The current version of the Link library.
+ - link.input An EventEmitter of data contained in the request body.
+ - link.error A writable Stream for error output.
+ - link.session A JavaScript object containing session data.
+
+There are the following restrictions:
+
+ - protocol must be either "http:" or "https:".
+ - requestMethod must be a valid token.
+ - scriptName, if not empty, should start with a "/".
+ - pathInfo, if not empty, should start with a "/".
+ - Both scriptName and pathInfo must be set. pathInfo should be "/" if
+ scriptName is empty. scriptName should never be "/" but instead be empty.
+ - contentLength, if given, must consist of digits only.
+ - link.version must be an array of integers [major, minor, patch].
+
+The application is free to modify the environment. Property names must contain
+at least one dot and should be prefixed uniquely. The prefix "link" is reserved
+for use within the Link core distribution and other accepted specifications and
+is not available for use elsewhere.
+
+== The Callback
+
+The callback is used to issue a response to the client and must be called with
+exactly three arguments: the response *status*, HTTP *headers*, and *body*.
+
+=== The Status
+
+The status must be an HTTP status code as a Number.
+
+=== The Headers
+
+The headers must be a JavaScript object whose properties are the names of HTTP
+headers in their canonical form (i.e. "Content-Type" instead of "content-type").
+Header names may contain only letters, digits, "-", and "_" and must start with
+a letter and must not end with a "-" or "_". If more than one value for a header
+is required, the value for that property must be an array.
+
+=== The Content-Type
+
+There must be a Content-Type header, except for when the status is 1xx, 204, or
+304, in which case there must be none given.
+
+=== The Content-Length
+
+There must not be a Content-Length header when the status is 1xx, 204, or 304.
+
+=== The Body
+
+The body must be either a string or a readable Stream. If it is a Stream, the
+response will be pumped through to the client.
+
+= Credits
+
+Some parts of this specification are adopted from PEP333: Python Web Server
+Gateway Interface v1.0 (http://www.python.org/dev/peps/pep-0333/) and the Rack
+specification (http://rack.rubyforge.org/doc/files/SPEC.html).
3  bin/linkup
@@ -0,0 +1,3 @@
+#!/usr/bin/env node
+
+
163 lib/link.js
@@ -0,0 +1,163 @@
+var util = require("util"),
+ http = require("http"),
+ https = require("https"),
+ url = require("url"),
+ EventEmitter = require("events").EventEmitter,
+ utils = require("./link/utils"),
+ Input = require("./link/input");
+
+/**
+ * The current version of Link as [major, minor, patch].
+ */
+exports.version = [0, 1, 0];
+
+exports.createServer = createServer;
+exports.envFor = envFor;
+
+/**
+ * Creates an HTTP server for the given +app+. If +opts+ are supplied, they are
+ * used as the options for an HTTPS server. The +app+ should be a valid Link app
+ * (see the SPEC), or an object that has a #toApp function that returns an app.
+ */
+function createServer(app, opts) {
+ var server;
+
+ if (typeof opts === "object") {
+ server = https.createServer(opts);
+ } else {
+ server = http.createServer();
+ }
+
+ // The app may be any object that has a toApp method (e.g. a Builder).
+ if (typeof app.toApp === "function") {
+ app = app.toApp();
+ }
+
+ if (typeof app !== "function") {
+ throw new Error("App must be a function");
+ }
+
+ server.on("request", function handleRequest(req, res) {
+ var addr = server.address();
+ var uri = url.parse(req.url);
+
+ uri.protocol = (server instanceof https.Server) ? "https:" : "http:";
+ uri.hostname = process.env.SERVER_NAME || addr.address;
+ uri.port = (parseInt(process.env.SERVER_PORT, 10) || addr.port).toString(10);
+
+ var env = envFor(uri, {
+ protocolVersion: req.httpVersion,
+ method: req.method,
+ headers: req.headers,
+ input: req,
+ error: process.stderr
+ });
+
+ // Call the app.
+ app(env, function handleResponse(status, headers, body) {
+ res.writeHead(status, headers);
+
+ if (body instanceof EventEmitter) {
+ util.pump(body, res);
+ } else {
+ res.end(body);
+ }
+ });
+ });
+
+ return server;
+}
+
+/**
+ * Creates an environment object for the given +uri+, which should be an object
+ * with any of the following properties:
+ *
+ * - protocol
+ * - hostname
+ * - port
+ * - pathname
+ * - query
+ *
+ * The +opts+ parameter should be an object with any of the following
+ * properties:
+ *
+ * - protocolVersion The HTTP protocol version
+ * - method The request method (e.g. "GET" or "POST")
+ * - headers An object of HTTP headers
+ * - input The input stream of the request body
+ * - error The error stream
+ */
+function envFor(uri, opts) {
+ uri = uri || {};
+ opts = opts || {};
+
+ if (!(opts.input instanceof EventEmitter)) {
+ throw new Error("Input must be an EventEmitter");
+ }
+
+ var env = {};
+
+ env.protocolVersion = opts.protocolVersion || "1.0";
+ env.requestMethod = (opts.method || "GET").toUpperCase();
+
+ env.protocol = uri.protocol || "http:";
+ env.serverName = uri.hostname || "";
+ env.serverPort = uri.port || (env.protocol == "https:" ? "443": "80");
+ env.scriptName = "";
+ env.pathInfo = uri.pathname || "";
+ env.queryString = uri.query || "";
+
+ if (opts.headers) {
+ // Add http* properties for HTTP headers.
+ var propName;
+ for (var headerName in opts.headers) {
+ propName = utils.httpPropertyName(headerName);
+ env[propName] = opts.headers[headerName];
+ }
+ }
+
+ // Set contentType and contentLength.
+ env.contentType = env.httpContentType || "";
+ delete env.httpContentType;
+ env.contentLength = (parseInt(env.httpContentLength, 10) || 0).toString(10);
+ delete env.httpContentLength;
+
+ env["link.version"] = exports.version;
+ env["link.input"] = new Input(opts.input);
+ env["link.error"] = opts.error || process.stderr;
+
+ return env;
+}
+
+// Setup dynamic loading of package contents, accessible by property name.
+
+var propPaths = {
+ // Constructors
+ "Builder": "link/builder",
+ "Input": "link/input",
+ "Request": "link/request",
+ "Response": "link/response",
+ "UploadedFile": "link/uploadedfile",
+ // Other
+ "commonLogger": "link/commonlogger",
+ "contentLength": "link/contentlength",
+ "contentType": "link/contenttype",
+ "lint": "link/lint",
+ "methodOverride": "link/methodoverride",
+ "mime": "link/mime",
+ "mock": "link/mock",
+ "multipart": "link/multipart",
+ "querystring": "link/querystring",
+ "sessionCookie": "link/session/cookie",
+ "static": "link/static",
+ "urlMap": "link/urlmap",
+ "utils": "link/utils"
+};
+
+for (var propertyName in propPaths) {
+ exports.__defineGetter__(propertyName, (function (path) {
+ return function () {
+ return require("./" + path);
+ }
+ })(propPaths[propertyName]));
+}
98 lib/link/builder.js
@@ -0,0 +1,98 @@
+var urlMap = require("./urlmap");
+
+module.exports = Builder;
+
+/**
+ * Provides a convenient interface for iteratively constructing a Link
+ * application fronted by various middleware, with the ability to easily nest
+ * more builders at a certain path prefix.
+ *
+ * var link = require("link");
+ * var builder = new link.Builder;
+ *
+ * builder.use(link.contentLength);
+ * builder.use(link.contentType, "text/plain");
+ *
+ * builder.map("/files", function (builder) {
+ * builder.use(link.static, "/var/www/public");
+ * builder.run(function (env, callback) {
+ * callback(404, {}, "File not found: " + env.pathInfo);
+ * });
+ * });
+ *
+ * builder.run(function (env, callback) {
+ * callback(200, {"Content-Type": "text/html"}, "<p>Hello world!</p>");
+ * });
+ *
+ * var server = link.createServer(builder);
+ * server.listen(80);
+ */
+function Builder(app) {
+ this._use = [];
+ this._map = null;
+
+ if (app) {
+ this.run(app);
+ }
+}
+
+Builder.prototype.use = function use(middleware) {
+ var args = Array.prototype.slice.call(arguments, 1);
+
+ if (this._map) {
+ var mapping = this._map;
+ this._map = null;
+
+ this._use.push(function (app) {
+ return generateMap(app, mapping);
+ });
+ }
+
+ this._use.push(function (app) {
+ args.unshift(app);
+ return middleware.apply(this, args);
+ });
+}
+
+Builder.prototype.map = function map(path, callback) {
+ if (!this._map) {
+ this._map = {};
+ }
+
+ this._map[path] = callback;
+}
+
+Builder.prototype.run = function run(app) {
+ this._app = app;
+}
+
+/**
+ * Compiles this object to a callable app.
+ */
+Builder.prototype.toApp = function toApp() {
+ var app = this._map ? generateMap(this._app, this._map) : this._app;
+
+ if (!app) {
+ throw new Error("Missing call to run or map");
+ }
+
+ var i = this._use.length;
+ while (i) {
+ app = this._use[--i](app);
+ }
+
+ return app;
+}
+
+function generateMap(app, mapping) {
+ var map = app ? {"/": app} : {}
+
+ var builder;
+ for (var path in mapping) {
+ builder = new Builder(app);
+ mapping[path](builder);
+ map[path] = builder.toApp();
+ }
+
+ return urlMap(map);
+}
36 lib/link/commonlogger.js
@@ -0,0 +1,36 @@
+var Request = require("./request"),
+ utils = require("./utils");
+
+/**
+ * A middleware that logs the request to the given +stream+ on its way out,
+ * similar to the Apache web server.
+ */
+module.exports = function (app, stream) {
+ stream = stream || process.stderr;
+
+ // Common Log Format: http://httpd.apache.org/docs/1.3/logs.html#common
+ // LogFormat "%h %l %u %t \"%r\" %>s %b" common
+ // 127.0.0.1 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 200 2326
+
+ return function commonLogger(env, callback) {
+ app(env, function (status, headers, body) {
+ var req = new Request(env);
+
+ var host = env.httpXForwardedFor || "-",
+ id = "-",
+ user = "-",
+ timestamp = "[" + utils.strftime(new Date, "%d/%b/%Y:%H:%M:%S %Z") + "]",
+ reqline = '"' + req.method + " " + req.fullPath + " HTTP/" + env.protocolVersion + '"',
+ length = parseInt(headers["Content-Length"], 10) || 0;
+
+ if (!length && typeof body === "string") {
+ length = body.length;
+ }
+
+ var entry = [host, id, user, timestamp, reqline, String(status), length].join(" ");
+ stream.write(entry + "\n");
+
+ callback(status, headers, body);
+ });
+ }
+}
23 lib/link/contentlength.js
@@ -0,0 +1,23 @@
+var utils = require("./utils");
+
+/**
+ * A middleware that sets the Content-Length header if it is missing on string
+ * bodies. Does not work for Stream bodies.
+ */
+module.exports = function (app) {
+ return function contentLength(env, callback) {
+ app(env, function (status, headers, body) {
+ if (utils.STATUS_WITH_NO_ENTITY_BODY.indexOf(status) === -1 &&
+ !headers["Content-Length"] &&
+ !headers["Transfer-Encoding"]) {
+ if (typeof body.length !== "undefined") {
+ headers["Content-Length"] = String(body.length);
+ } else {
+ env["link.error"].write("Cannot set Content-Length for body with no length\n");
+ }
+ }
+
+ callback(status, headers, body);
+ });
+ }
+}
17 lib/link/contenttype.js
@@ -0,0 +1,17 @@
+/**
+ * A middleware that sets a default Content-Type header in case one hasn't
+ * already been set in a downstream app.
+ */
+module.exports = function (app, defaultType) {
+ defaultType = defaultType || "text/html";
+
+ return function contentType(env, callback) {
+ app(env, function (status, headers, body) {
+ if (!headers["Content-Type"]) {
+ headers["Content-Type"] = defaultType;
+ }
+
+ callback(status, headers, body);
+ });
+ }
+}
35 lib/link/input.js
@@ -0,0 +1,35 @@
+var util = require("util"),
+ EventEmitter = require("events").EventEmitter;
+
+module.exports = Input;
+
+/**
+ * A simple stream wrapper for request input.
+ */
+function Input(stream) {
+ EventEmitter.call(this);
+ this._stream = stream;
+ var self = this;
+
+ stream.on("data", function (chunk) {
+ self.emit("data", chunk);
+ });
+
+ stream.on("end", function () {
+ self.emit("end");
+ });
+}
+
+util.inherits(Input, EventEmitter);
+
+Input.prototype.setEncoding = function setEncoding(encoding) {
+ return this._stream.setEncoding(encoding);
+}
+
+Input.prototype.pause = function pause() {
+ return this._stream.pause();
+}
+
+Input.prototype.resume = function resume() {
+ return this._stream.resume();
+}
171 lib/link/lint.js
@@ -0,0 +1,171 @@
+var Stream = require("stream").Stream,
+ EventEmitter = require("events").EventEmitter,
+ utils = require("./utils");
+
+/**
+ * A middleware that checks the environment and callback from upstream and the
+ * status, headers, and body from downstream for conformance with the Link
+ * specification. See SPEC for more information.
+ */
+module.exports = function (app) {
+ return function lint(env, callback) {
+ assert(arguments.length === 2, "App must be called with exactly two arguments: environment and callback");
+
+ checkEnv(env);
+ checkCallback(callback);
+
+ app(env, function (status, headers, body) {
+ assert(arguments.length === 3, "Callback must be called with exactly three arguments: status, headers, and body");
+
+ checkStatus(status);
+ checkHeaders(headers);
+ checkBody(body);
+
+ checkContentType(status, headers);
+ checkContentLength(status, headers, body);
+
+ callback(status, headers, body);
+ });
+ }
+}
+
+function assert(condition, message) {
+ if (!condition) {
+ throw new Error(message);
+ }
+}
+
+function checkEnv(env) {
+ assert(typeof env === "object", "Environment must be an object");
+
+ var cgiProperties = [
+ "protocolVersion",
+ "requestMethod",
+ "scriptName",
+ "pathInfo",
+ "queryString",
+ "serverName",
+ "serverPort"
+ ];
+
+ cgiProperties.forEach(function (property) {
+ assert(property in env, 'Environment missing required property "' + property + '"');
+ assert(typeof env[property] === "string", 'Property "' + property + '" must be a String');
+ });
+
+ // The environment must not contain the properties httpContentType or
+ // httpContentLength (use contentType and contentLength instead).
+ assert(typeof env["httpContentType"] === "undefined", 'Environment must not contain property "httpContentType", use "contentType" instead');
+ assert(typeof env["httpContentLength"] === "undefined", 'Environment must not contain property "httpContentLength", use "contentLength" instead');
+
+ var linkProperties = [
+ "link.version",
+ "link.urlScheme",
+ "link.input",
+ "link.error"
+ ];
+
+ linkProperties.forEach(function (property) {
+ assert(property in env, 'Environment missing required property "' + property + '"');
+ });
+
+ // - link.input An emitter of data contained in the request body.
+ assert(env["link.input"] instanceof EventEmitter, "link.input must be an EventEmitter");
+
+ // - link.error A writable stream for error output.
+ assert(env["link.error"] instanceof Stream, "link.error must be a Stream");
+ assert(env["link.error"].writable, "link.error must be writable");
+
+ // - link.session A JavaScript object containing session data.
+ var session = env["link.session"];
+ if (session) {
+ assert(typeof env === "object", "Session must be an object");
+ }
+
+ // - link.version must be an array of integers [major, minor, patch].
+ assert(Array.isArray(env["link.version"]), "link.version must be an array");
+ assert(env["link.version"].length === 3, "link.version must contain three values (major, minor, patch)");
+ env["link.version"].forEach(function (n, i) {
+ assert(typeof n === "number", "Index " + i + " of link.version must be a number");
+ });
+
+ // - link.urlScheme must be either "http" or "https".
+ assert(["http", "https"].indexOf(env["link.urlScheme"]) !== -1, 'link.urlScheme must be either "http" or "https"');
+
+ // - contentLength, if given, must consist of digits only.
+ if (env.contentLength) {
+ assert(typeof env.contentLength === "string", "contentLength must be a string");
+ assert((/^\d+$/).test(env.contentLength), "contentLength must consist of digits only");
+ }
+
+ // - requestMethod must be a valid token.
+ assert((/^[0-9A-Za-z!\#$%&'*+.^_`|~-]+$/).test(env.requestMethod), "Request method must be a valid token");
+
+ // - scriptName, if not empty, should start with a "/".
+ // - pathInfo, if not empty, should start with a "/".
+ // - Both scriptName and pathInfo must be set. pathInfo should be "/" if
+ // scriptName is empty. scriptName should never be "/" but instead be empty.
+ if (env.scriptName !== "") {
+ assert(env.scriptName.charAt(0) === "/", 'scriptName must start with a "/"');
+ assert(env.scriptName !== "/", 'scriptName cannot be "/", make it empty and pathInfo "/"');
+ }
+ if (env.pathInfo !== "") {
+ assert(env.pathInfo.charAt(0) === "/", 'pathInfo must start with a "/"');
+ }
+}
+
+function checkCallback(callback) {
+ // The callback is used to issue a response to the client and must be called with
+ // exactly three arguments: the response *status*, the HTTP *headers*, and the
+ // *body*.
+ assert(typeof callback === "function", "Callback must be a function");
+ assert(callback.length === 3, "Callback must accept three arguments");
+}
+
+function checkStatus(status) {
+ // The status must be an HTTP status code as a Number.
+ assert(typeof status === "number", "Status must be a number");
+}
+
+function checkHeaders(headers) {
+ // The headers must be a JavaScript object whose properties are the names of HTTP
+ // headers in their canonical form (i.e. "Content-Type" instead of "content-type").
+ // Header names may contain only letters, digits, -, and _ and must start with a
+ // letter and must not end with a - or _. If more than one value for a header is
+ // required, the value for that property must be an array.
+ assert(typeof headers === "object", "Headers must be an object");
+
+ for (var headerName in headers) {
+ assert(typeof headers[headerName] === "string", 'Value for header "' + headerName + '" must be a string');
+ assert((/^[0-9A-Za-z_-]+$/).test(headerName), 'Invalid HTTP header name "' + headerName + '"');
+ assert((/^[A-Za-z]/).test(headerName), "Header name must start with a letter");
+ assert(!(/[_-]$/).test(headerName), 'Header name must not end with a "_" or "-"');
+ }
+}
+
+function checkBody(body) {
+ // The body must be either a string or a readable Stream. If it is a Stream, the
+ // response will be pumped through to the client.
+ assert(typeof body === "string" || body instanceof Stream, "Body must be a String or Stream");
+
+ if (body instanceof Stream) {
+ assert(body.readable, "Stream body must be readable");
+ }
+}
+
+function checkContentType(status, headers) {
+ // There must be a Content-Type header, except for when the status is 1xx, 204, or
+ // 304, in which case there must be none given.
+ if (utils.STATUS_WITH_NO_ENTITY_BODY.indexOf(status) === -1) {
+ assert("Content-Type" in headers, "Missing Content-Type header");
+ } else {
+ assert(!("Content-Type" in headers), "Content-Type header given for respons with no entity body");
+ }
+}
+
+function checkContentLength(status, headers, body) {
+ // There must not be a Content-Length header when the status is 1xx, 204, or 304.
+ if (utils.STATUS_WITH_NO_ENTITY_BODY.indexOf(status) !== -1) {
+ assert(!("Content-Length" in headers), "Content-Length header given for respons with no entity body");
+ }
+}
33 lib/link/methodoverride.js
@@ -0,0 +1,33 @@
+var utils = require("./utils"),
+ Request = require("./request");
+
+/**
+ * A middleware that may be used for HTTP clients who do not natively support
+ * PUT, DELETE, etc. to coerce the environment's requestMethod to instead use
+ * the one specified in a request parameter or HTTP header.
+ */
+module.exports = function (app, paramName, headerName) {
+ paramName = paramName || "_method";
+ headerName = headerName || "X-Http-Method-Override";
+
+ var propName = utils.httpPropertyName(headerName);
+
+ return function methodOverride(env, callback) {
+ if (env.requestMethod === "POST") {
+ var req = new Request(env);
+
+ req.params(function (err, params) {
+ var method = params[paramName] || env[propName];
+
+ if (method) {
+ env["link.methodOverride.originalMethod"] = env.requestMethod;
+ env.requestMethod = method.toUpperCase();
+ }
+
+ app(env, callback);
+ });
+ } else {
+ app(env, callback);
+ }
+ }
+}
614 lib/link/mime.js
@@ -0,0 +1,614 @@
+exports.type = type;
+
+function type(ext, fallback) {
+ return exports.types[ext] || fallback || "application/octet-stream";
+}
+
+/**
+ * A map of all known file extensions to their MIME type. Copied from Rack.
+ */
+exports.types = {
+ ".123" : "application/vnd.lotus-1-2-3",
+ ".3dml" : "text/vnd.in3d.3dml",
+ ".3g2" : "video/3gpp2",
+ ".3gp" : "video/3gpp",
+ ".a" : "application/octet-stream",
+ ".acc" : "application/vnd.americandynamics.acc",
+ ".ace" : "application/x-ace-compressed",
+ ".acu" : "application/vnd.acucobol",
+ ".aep" : "application/vnd.audiograph",
+ ".afp" : "application/vnd.ibm.modcap",
+ ".ai" : "application/postscript",
+ ".aif" : "audio/x-aiff",
+ ".aiff" : "audio/x-aiff",
+ ".ami" : "application/vnd.amiga.ami",
+ ".apr" : "application/vnd.lotus-approach",
+ ".asc" : "application/pgp-signature",
+ ".asf" : "video/x-ms-asf",
+ ".asm" : "text/x-asm",
+ ".aso" : "application/vnd.accpac.simply.aso",
+ ".asx" : "video/x-ms-asf",
+ ".atc" : "application/vnd.acucorp",
+ ".atom" : "application/atom+xml",
+ ".atomcat" : "application/atomcat+xml",
+ ".atomsvc" : "application/atomsvc+xml",
+ ".atx" : "application/vnd.antix.game-component",
+ ".au" : "audio/basic",
+ ".avi" : "video/x-msvideo",
+ ".bat" : "application/x-msdownload",
+ ".bcpio" : "application/x-bcpio",
+ ".bdm" : "application/vnd.syncml.dm+wbxml",
+ ".bh2" : "application/vnd.fujitsu.oasysprs",
+ ".bin" : "application/octet-stream",
+ ".bmi" : "application/vnd.bmi",
+ ".bmp" : "image/bmp",
+ ".box" : "application/vnd.previewsystems.box",
+ ".btif" : "image/prs.btif",
+ ".bz" : "application/x-bzip",
+ ".bz2" : "application/x-bzip2",
+ ".c" : "text/x-c",
+ ".c4g" : "application/vnd.clonk.c4group",
+ ".cab" : "application/vnd.ms-cab-compressed",
+ ".cc" : "text/x-c",
+ ".ccxml" : "application/ccxml+xml",
+ ".cdbcmsg" : "application/vnd.contact.cmsg",
+ ".cdkey" : "application/vnd.mediastation.cdkey",
+ ".cdx" : "chemical/x-cdx",
+ ".cdxml" : "application/vnd.chemdraw+xml",
+ ".cdy" : "application/vnd.cinderella",
+ ".cer" : "application/pkix-cert",
+ ".cgm" : "image/cgm",
+ ".chat" : "application/x-chat",
+ ".chm" : "application/vnd.ms-htmlhelp",
+ ".chrt" : "application/vnd.kde.kchart",
+ ".cif" : "chemical/x-cif",
+ ".cii" : "application/vnd.anser-web-certificate-issue-initiation",
+ ".cil" : "application/vnd.ms-artgalry",
+ ".cla" : "application/vnd.claymore",
+ ".class" : "application/octet-stream",
+ ".clkk" : "application/vnd.crick.clicker.keyboard",
+ ".clkp" : "application/vnd.crick.clicker.palette",
+ ".clkt" : "application/vnd.crick.clicker.template",
+ ".clkw" : "application/vnd.crick.clicker.wordbank",
+ ".clkx" : "application/vnd.crick.clicker",
+ ".clp" : "application/x-msclip",
+ ".cmc" : "application/vnd.cosmocaller",
+ ".cmdf" : "chemical/x-cmdf",
+ ".cml" : "chemical/x-cml",
+ ".cmp" : "application/vnd.yellowriver-custom-menu",
+ ".cmx" : "image/x-cmx",
+ ".com" : "application/x-msdownload",
+ ".conf" : "text/plain",
+ ".cpio" : "application/x-cpio",
+ ".cpp" : "text/x-c",
+ ".cpt" : "application/mac-compactpro",
+ ".crd" : "application/x-mscardfile",
+ ".crl" : "application/pkix-crl",
+ ".crt" : "application/x-x509-ca-cert",
+ ".csh" : "application/x-csh",
+ ".csml" : "chemical/x-csml",
+ ".csp" : "application/vnd.commonspace",
+ ".css" : "text/css",
+ ".csv" : "text/csv",
+ ".curl" : "application/vnd.curl",
+ ".cww" : "application/prs.cww",
+ ".cxx" : "text/x-c",
+ ".daf" : "application/vnd.mobius.daf",
+ ".davmount" : "application/davmount+xml",
+ ".dcr" : "application/x-director",
+ ".dd2" : "application/vnd.oma.dd2+xml",
+ ".ddd" : "application/vnd.fujixerox.ddd",
+ ".deb" : "application/x-debian-package",
+ ".der" : "application/x-x509-ca-cert",
+ ".dfac" : "application/vnd.dreamfactory",
+ ".diff" : "text/x-diff",
+ ".dis" : "application/vnd.mobius.dis",
+ ".djv" : "image/vnd.djvu",
+ ".djvu" : "image/vnd.djvu",
+ ".dll" : "application/x-msdownload",
+ ".dmg" : "application/octet-stream",
+ ".dna" : "application/vnd.dna",
+ ".doc" : "application/msword",
+ ".dot" : "application/msword",
+ ".dp" : "application/vnd.osgi.dp",
+ ".dpg" : "application/vnd.dpgraph",
+ ".dsc" : "text/prs.lines.tag",
+ ".dtd" : "application/xml-dtd",
+ ".dts" : "audio/vnd.dts",
+ ".dtshd" : "audio/vnd.dts.hd",
+ ".dv" : "video/x-dv",
+ ".dvi" : "application/x-dvi",
+ ".dwf" : "model/vnd.dwf",
+ ".dwg" : "image/vnd.dwg",
+ ".dxf" : "image/vnd.dxf",
+ ".dxp" : "application/vnd.spotfire.dxp",
+ ".ear" : "application/java-archive",
+ ".ecelp4800" : "audio/vnd.nuera.ecelp4800",
+ ".ecelp7470" : "audio/vnd.nuera.ecelp7470",
+ ".ecelp9600" : "audio/vnd.nuera.ecelp9600",
+ ".ecma" : "application/ecmascript",
+ ".edm" : "application/vnd.novadigm.edm",
+ ".edx" : "application/vnd.novadigm.edx",
+ ".efif" : "application/vnd.picsel",
+ ".ei6" : "application/vnd.pg.osasli",
+ ".eml" : "message/rfc822",
+ ".eol" : "audio/vnd.digital-winds",
+ ".eot" : "application/vnd.ms-fontobject",
+ ".eps" : "application/postscript",
+ ".es3" : "application/vnd.eszigno3+xml",
+ ".esf" : "application/vnd.epson.esf",
+ ".etx" : "text/x-setext",
+ ".exe" : "application/x-msdownload",
+ ".ext" : "application/vnd.novadigm.ext",
+ ".ez" : "application/andrew-inset",
+ ".ez2" : "application/vnd.ezpix-album",
+ ".ez3" : "application/vnd.ezpix-package",
+ ".f" : "text/x-fortran",
+ ".f77" : "text/x-fortran",
+ ".f90" : "text/x-fortran",
+ ".fbs" : "image/vnd.fastbidsheet",
+ ".fdf" : "application/vnd.fdf",
+ ".fe_launch" : "application/vnd.denovo.fcselayout-link",
+ ".fg5" : "application/vnd.fujitsu.oasysgp",
+ ".fli" : "video/x-fli",
+ ".flo" : "application/vnd.micrografx.flo",
+ ".flv" : "video/x-flv",
+ ".flw" : "application/vnd.kde.kivio",
+ ".flx" : "text/vnd.fmi.flexstor",
+ ".fly" : "text/vnd.fly",
+ ".fm" : "application/vnd.framemaker",
+ ".fnc" : "application/vnd.frogans.fnc",
+ ".for" : "text/x-fortran",
+ ".fpx" : "image/vnd.fpx",
+ ".fsc" : "application/vnd.fsc.weblaunch",
+ ".fst" : "image/vnd.fst",
+ ".ftc" : "application/vnd.fluxtime.clip",
+ ".fti" : "application/vnd.anser-web-funds-transfer-initiation",
+ ".fvt" : "video/vnd.fvt",
+ ".fzs" : "application/vnd.fuzzysheet",
+ ".g3" : "image/g3fax",
+ ".gac" : "application/vnd.groove-account",
+ ".gdl" : "model/vnd.gdl",
+ ".gem" : "application/octet-stream",
+ ".gemspec" : "text/x-script.ruby",
+ ".ghf" : "application/vnd.groove-help",
+ ".gif" : "image/gif",
+ ".gim" : "application/vnd.groove-identity-message",
+ ".gmx" : "application/vnd.gmx",
+ ".gph" : "application/vnd.flographit",
+ ".gqf" : "application/vnd.grafeq",
+ ".gram" : "application/srgs",
+ ".grv" : "application/vnd.groove-injector",
+ ".grxml" : "application/srgs+xml",
+ ".gtar" : "application/x-gtar",
+ ".gtm" : "application/vnd.groove-tool-message",
+ ".gtw" : "model/vnd.gtw",
+ ".gv" : "text/vnd.graphviz",
+ ".gz" : "application/x-gzip",
+ ".h" : "text/x-c",
+ ".h261" : "video/h261",
+ ".h263" : "video/h263",
+ ".h264" : "video/h264",
+ ".hbci" : "application/vnd.hbci",
+ ".hdf" : "application/x-hdf",
+ ".hh" : "text/x-c",
+ ".hlp" : "application/winhlp",
+ ".hpgl" : "application/vnd.hp-hpgl",
+ ".hpid" : "application/vnd.hp-hpid",
+ ".hps" : "application/vnd.hp-hps",
+ ".hqx" : "application/mac-binhex40",
+ ".htc" : "text/x-component",
+ ".htke" : "application/vnd.kenameaapp",
+ ".htm" : "text/html",
+ ".html" : "text/html",
+ ".hvd" : "application/vnd.yamaha.hv-dic",
+ ".hvp" : "application/vnd.yamaha.hv-voice",
+ ".hvs" : "application/vnd.yamaha.hv-script",
+ ".icc" : "application/vnd.iccprofile",
+ ".ice" : "x-conference/x-cooltalk",
+ ".ico" : "image/vnd.microsoft.icon",
+ ".ics" : "text/calendar",
+ ".ief" : "image/ief",
+ ".ifb" : "text/calendar",
+ ".ifm" : "application/vnd.shana.informed.formdata",
+ ".igl" : "application/vnd.igloader",
+ ".igs" : "model/iges",
+ ".igx" : "application/vnd.micrografx.igx",
+ ".iif" : "application/vnd.shana.informed.interchange",
+ ".imp" : "application/vnd.accpac.simply.imp",
+ ".ims" : "application/vnd.ms-ims",
+ ".ipk" : "application/vnd.shana.informed.package",
+ ".irm" : "application/vnd.ibm.rights-management",
+ ".irp" : "application/vnd.irepository.package+xml",
+ ".iso" : "application/octet-stream",
+ ".itp" : "application/vnd.shana.informed.formtemplate",
+ ".ivp" : "application/vnd.immervision-ivp",
+ ".ivu" : "application/vnd.immervision-ivu",
+ ".jad" : "text/vnd.sun.j2me.app-descriptor",
+ ".jam" : "application/vnd.jam",
+ ".jar" : "application/java-archive",
+ ".java" : "text/x-java-source",
+ ".jisp" : "application/vnd.jisp",
+ ".jlt" : "application/vnd.hp-jlyt",
+ ".jnlp" : "application/x-java-jnlp-file",
+ ".joda" : "application/vnd.joost.joda-archive",
+ ".jp2" : "image/jp2",
+ ".jpeg" : "image/jpeg",
+ ".jpg" : "image/jpeg",
+ ".jpgv" : "video/jpeg",
+ ".jpm" : "video/jpm",
+ ".js" : "application/javascript",
+ ".json" : "application/json",
+ ".karbon" : "application/vnd.kde.karbon",
+ ".kfo" : "application/vnd.kde.kformula",
+ ".kia" : "application/vnd.kidspiration",
+ ".kml" : "application/vnd.google-earth.kml+xml",
+ ".kmz" : "application/vnd.google-earth.kmz",
+ ".kne" : "application/vnd.kinar",
+ ".kon" : "application/vnd.kde.kontour",
+ ".kpr" : "application/vnd.kde.kpresenter",
+ ".ksp" : "application/vnd.kde.kspread",
+ ".ktz" : "application/vnd.kahootz",
+ ".kwd" : "application/vnd.kde.kword",
+ ".latex" : "application/x-latex",
+ ".lbd" : "application/vnd.llamagraphics.life-balance.desktop",
+ ".lbe" : "application/vnd.llamagraphics.life-balance.exchange+xml",
+ ".les" : "application/vnd.hhe.lesson-player",
+ ".link66" : "application/vnd.route66.link66+xml",
+ ".log" : "text/plain",
+ ".lostxml" : "application/lost+xml",
+ ".lrm" : "application/vnd.ms-lrm",
+ ".ltf" : "application/vnd.frogans.ltf",
+ ".lvp" : "audio/vnd.lucent.voice",
+ ".lwp" : "application/vnd.lotus-wordpro",
+ ".m3u" : "audio/x-mpegurl",
+ ".m4a" : "audio/mp4a-latm",
+ ".m4v" : "video/mp4",
+ ".ma" : "application/mathematica",
+ ".mag" : "application/vnd.ecowin.chart",
+ ".man" : "text/troff",
+ ".manifest" : "text/cache-manifest",
+ ".mathml" : "application/mathml+xml",
+ ".mbk" : "application/vnd.mobius.mbk",
+ ".mbox" : "application/mbox",
+ ".mc1" : "application/vnd.medcalcdata",
+ ".mcd" : "application/vnd.mcd",
+ ".mdb" : "application/x-msaccess",
+ ".mdi" : "image/vnd.ms-modi",
+ ".mdoc" : "text/troff",
+ ".me" : "text/troff",
+ ".mfm" : "application/vnd.mfmp",
+ ".mgz" : "application/vnd.proteus.magazine",
+ ".mid" : "audio/midi",
+ ".midi" : "audio/midi",
+ ".mif" : "application/vnd.mif",
+ ".mime" : "message/rfc822",
+ ".mj2" : "video/mj2",
+ ".mlp" : "application/vnd.dolby.mlp",
+ ".mmd" : "application/vnd.chipnuts.karaoke-mmd",
+ ".mmf" : "application/vnd.smaf",
+ ".mml" : "application/mathml+xml",
+ ".mmr" : "image/vnd.fujixerox.edmics-mmr",
+ ".mng" : "video/x-mng",
+ ".mny" : "application/x-msmoney",
+ ".mov" : "video/quicktime",
+ ".movie" : "video/x-sgi-movie",
+ ".mp3" : "audio/mpeg",
+ ".mp4" : "video/mp4",
+ ".mp4a" : "audio/mp4",
+ ".mp4s" : "application/mp4",
+ ".mp4v" : "video/mp4",
+ ".mpc" : "application/vnd.mophun.certificate",
+ ".mpeg" : "video/mpeg",
+ ".mpg" : "video/mpeg",
+ ".mpga" : "audio/mpeg",
+ ".mpkg" : "application/vnd.apple.installer+xml",
+ ".mpm" : "application/vnd.blueice.multipass",
+ ".mpn" : "application/vnd.mophun.application",
+ ".mpp" : "application/vnd.ms-project",
+ ".mpy" : "application/vnd.ibm.minipay",
+ ".mqy" : "application/vnd.mobius.mqy",
+ ".mrc" : "application/marc",
+ ".ms" : "text/troff",
+ ".mscml" : "application/mediaservercontrol+xml",
+ ".mseq" : "application/vnd.mseq",
+ ".msf" : "application/vnd.epson.msf",
+ ".msh" : "model/mesh",
+ ".msi" : "application/x-msdownload",
+ ".msl" : "application/vnd.mobius.msl",
+ ".msty" : "application/vnd.muvee.style",
+ ".mts" : "model/vnd.mts",
+ ".mus" : "application/vnd.musician",
+ ".mvb" : "application/x-msmediaview",
+ ".mwf" : "application/vnd.mfer",
+ ".mxf" : "application/mxf",
+ ".mxl" : "application/vnd.recordare.musicxml",
+ ".mxml" : "application/xv+xml",
+ ".mxs" : "application/vnd.triscape.mxs",
+ ".mxu" : "video/vnd.mpegurl",
+ ".n" : "application/vnd.nokia.n-gage.symbian.install",
+ ".nc" : "application/x-netcdf",
+ ".ngdat" : "application/vnd.nokia.n-gage.data",
+ ".nlu" : "application/vnd.neurolanguage.nlu",
+ ".nml" : "application/vnd.enliven",
+ ".nnd" : "application/vnd.noblenet-directory",
+ ".nns" : "application/vnd.noblenet-sealer",
+ ".nnw" : "application/vnd.noblenet-web",
+ ".npx" : "image/vnd.net-fpx",
+ ".nsf" : "application/vnd.lotus-notes",
+ ".oa2" : "application/vnd.fujitsu.oasys2",
+ ".oa3" : "application/vnd.fujitsu.oasys3",
+ ".oas" : "application/vnd.fujitsu.oasys",
+ ".obd" : "application/x-msbinder",
+ ".oda" : "application/oda",
+ ".odc" : "application/vnd.oasis.opendocument.chart",
+ ".odf" : "application/vnd.oasis.opendocument.formula",
+ ".odg" : "application/vnd.oasis.opendocument.graphics",
+ ".odi" : "application/vnd.oasis.opendocument.image",
+ ".odp" : "application/vnd.oasis.opendocument.presentation",
+ ".ods" : "application/vnd.oasis.opendocument.spreadsheet",
+ ".odt" : "application/vnd.oasis.opendocument.text",
+ ".oga" : "audio/ogg",
+ ".ogg" : "application/ogg",
+ ".ogv" : "video/ogg",
+ ".ogx" : "application/ogg",
+ ".org" : "application/vnd.lotus-organizer",
+ ".otc" : "application/vnd.oasis.opendocument.chart-template",
+ ".otf" : "application/vnd.oasis.opendocument.formula-template",
+ ".otg" : "application/vnd.oasis.opendocument.graphics-template",
+ ".oth" : "application/vnd.oasis.opendocument.text-web",
+ ".oti" : "application/vnd.oasis.opendocument.image-template",
+ ".otm" : "application/vnd.oasis.opendocument.text-master",
+ ".ots" : "application/vnd.oasis.opendocument.spreadsheet-template",
+ ".ott" : "application/vnd.oasis.opendocument.text-template",
+ ".oxt" : "application/vnd.openofficeorg.extension",
+ ".p" : "text/x-pascal",
+ ".p10" : "application/pkcs10",
+ ".p12" : "application/x-pkcs12",
+ ".p7b" : "application/x-pkcs7-certificates",
+ ".p7m" : "application/pkcs7-mime",
+ ".p7r" : "application/x-pkcs7-certreqresp",
+ ".p7s" : "application/pkcs7-signature",
+ ".pas" : "text/x-pascal",
+ ".pbd" : "application/vnd.powerbuilder6",
+ ".pbm" : "image/x-portable-bitmap",
+ ".pcl" : "application/vnd.hp-pcl",
+ ".pclxl" : "application/vnd.hp-pclxl",
+ ".pcx" : "image/x-pcx",
+ ".pdb" : "chemical/x-pdb",
+ ".pdf" : "application/pdf",
+ ".pem" : "application/x-x509-ca-cert",
+ ".pfr" : "application/font-tdpfr",
+ ".pgm" : "image/x-portable-graymap",
+ ".pgn" : "application/x-chess-pgn",
+ ".pgp" : "application/pgp-encrypted",
+ ".pic" : "image/x-pict",
+ ".pict" : "image/pict",
+ ".pkg" : "application/octet-stream",
+ ".pki" : "application/pkixcmp",
+ ".pkipath" : "application/pkix-pkipath",
+ ".pl" : "text/x-script.perl",
+ ".plb" : "application/vnd.3gpp.pic-bw-large",
+ ".plc" : "application/vnd.mobius.plc",
+ ".plf" : "application/vnd.pocketlearn",
+ ".pls" : "application/pls+xml",
+ ".pm" : "text/x-script.perl-module",
+ ".pml" : "application/vnd.ctc-posml",
+ ".png" : "image/png",
+ ".pnm" : "image/x-portable-anymap",
+ ".pntg" : "image/x-macpaint",
+ ".portpkg" : "application/vnd.macports.portpkg",
+ ".ppd" : "application/vnd.cups-ppd",
+ ".ppm" : "image/x-portable-pixmap",
+ ".pps" : "application/vnd.ms-powerpoint",
+ ".ppt" : "application/vnd.ms-powerpoint",
+ ".prc" : "application/vnd.palm",
+ ".pre" : "application/vnd.lotus-freelance",
+ ".prf" : "application/pics-rules",
+ ".ps" : "application/postscript",
+ ".psb" : "application/vnd.3gpp.pic-bw-small",
+ ".psd" : "image/vnd.adobe.photoshop",
+ ".ptid" : "application/vnd.pvi.ptid1",
+ ".pub" : "application/x-mspublisher",
+ ".pvb" : "application/vnd.3gpp.pic-bw-var",
+ ".pwn" : "application/vnd.3m.post-it-notes",
+ ".py" : "text/x-script.python",
+ ".pya" : "audio/vnd.ms-playready.media.pya",
+ ".pyv" : "video/vnd.ms-playready.media.pyv",
+ ".qam" : "application/vnd.epson.quickanime",
+ ".qbo" : "application/vnd.intu.qbo",
+ ".qfx" : "application/vnd.intu.qfx",
+ ".qps" : "application/vnd.publishare-delta-tree",
+ ".qt" : "video/quicktime",
+ ".qtif" : "image/x-quicktime",
+ ".qxd" : "application/vnd.quark.quarkxpress",
+ ".ra" : "audio/x-pn-realaudio",
+ ".rake" : "text/x-script.ruby",
+ ".ram" : "audio/x-pn-realaudio",
+ ".rar" : "application/x-rar-compressed",
+ ".ras" : "image/x-cmu-raster",
+ ".rb" : "text/x-script.ruby",
+ ".rcprofile" : "application/vnd.ipunplugged.rcprofile",
+ ".rdf" : "application/rdf+xml",
+ ".rdz" : "application/vnd.data-vision.rdz",
+ ".rep" : "application/vnd.businessobjects",
+ ".rgb" : "image/x-rgb",
+ ".rif" : "application/reginfo+xml",
+ ".rl" : "application/resource-lists+xml",
+ ".rlc" : "image/vnd.fujixerox.edmics-rlc",
+ ".rld" : "application/resource-lists-diff+xml",
+ ".rm" : "application/vnd.rn-realmedia",
+ ".rmp" : "audio/x-pn-realaudio-plugin",
+ ".rms" : "application/vnd.jcp.javame.midlet-rms",
+ ".rnc" : "application/relax-ng-compact-syntax",
+ ".roff" : "text/troff",
+ ".rpm" : "application/x-redhat-package-manager",
+ ".rpss" : "application/vnd.nokia.radio-presets",
+ ".rpst" : "application/vnd.nokia.radio-preset",
+ ".rq" : "application/sparql-query",
+ ".rs" : "application/rls-services+xml",
+ ".rsd" : "application/rsd+xml",
+ ".rss" : "application/rss+xml",
+ ".rtf" : "application/rtf",
+ ".rtx" : "text/richtext",
+ ".ru" : "text/x-script.ruby",
+ ".s" : "text/x-asm",
+ ".saf" : "application/vnd.yamaha.smaf-audio",
+ ".sbml" : "application/sbml+xml",
+ ".sc" : "application/vnd.ibm.secure-container",
+ ".scd" : "application/x-msschedule",
+ ".scm" : "application/vnd.lotus-screencam",
+ ".scq" : "application/scvp-cv-request",
+ ".scs" : "application/scvp-cv-response",
+ ".sdkm" : "application/vnd.solent.sdkm+xml",
+ ".sdp" : "application/sdp",
+ ".see" : "application/vnd.seemail",
+ ".sema" : "application/vnd.sema",
+ ".semd" : "application/vnd.semd",
+ ".semf" : "application/vnd.semf",
+ ".setpay" : "application/set-payment-initiation",
+ ".setreg" : "application/set-registration-initiation",
+ ".sfd" : "application/vnd.hydrostatix.sof-data",
+ ".sfs" : "application/vnd.spotfire.sfs",
+ ".sgm" : "text/sgml",
+ ".sgml" : "text/sgml",
+ ".sh" : "application/x-sh",
+ ".shar" : "application/x-shar",
+ ".shf" : "application/shf+xml",
+ ".sig" : "application/pgp-signature",
+ ".sit" : "application/x-stuffit",
+ ".sitx" : "application/x-stuffitx",
+ ".skp" : "application/vnd.koan",
+ ".slt" : "application/vnd.epson.salt",
+ ".smi" : "application/smil+xml",
+ ".snd" : "audio/basic",
+ ".so" : "application/octet-stream",
+ ".spf" : "application/vnd.yamaha.smaf-phrase",
+ ".spl" : "application/x-futuresplash",
+ ".spot" : "text/vnd.in3d.spot",
+ ".spp" : "application/scvp-vp-response",
+ ".spq" : "application/scvp-vp-request",
+ ".src" : "application/x-wais-source",
+ ".srx" : "application/sparql-results+xml",
+ ".sse" : "application/vnd.kodak-descriptor",
+ ".ssf" : "application/vnd.epson.ssf",
+ ".ssml" : "application/ssml+xml",
+ ".stf" : "application/vnd.wt.stf",
+ ".stk" : "application/hyperstudio",
+ ".str" : "application/vnd.pg.format",
+ ".sus" : "application/vnd.sus-calendar",
+ ".sv4cpio" : "application/x-sv4cpio",
+ ".sv4crc" : "application/x-sv4crc",
+ ".svd" : "application/vnd.svd",
+ ".svg" : "image/svg+xml",
+ ".svgz" : "image/svg+xml",
+ ".swf" : "application/x-shockwave-flash",
+ ".swi" : "application/vnd.arastra.swi",
+ ".t" : "text/troff",
+ ".tao" : "application/vnd.tao.intent-module-archive",
+ ".tar" : "application/x-tar",
+ ".tbz" : "application/x-bzip-compressed-tar",
+ ".tcap" : "application/vnd.3gpp2.tcap",
+ ".tcl" : "application/x-tcl",
+ ".tex" : "application/x-tex",
+ ".texi" : "application/x-texinfo",
+ ".texinfo" : "application/x-texinfo",
+ ".text" : "text/plain",
+ ".tif" : "image/tiff",
+ ".tiff" : "image/tiff",
+ ".tmo" : "application/vnd.tmobile-livetv",
+ ".torrent" : "application/x-bittorrent",
+ ".tpl" : "application/vnd.groove-tool-template",
+ ".tpt" : "application/vnd.trid.tpt",
+ ".tr" : "text/troff",
+ ".tra" : "application/vnd.trueapp",
+ ".trm" : "application/x-msterminal",
+ ".tsv" : "text/tab-separated-values",
+ ".ttf" : "application/octet-stream",
+ ".twd" : "application/vnd.simtech-mindmapper",
+ ".txd" : "application/vnd.genomatix.tuxedo",
+ ".txf" : "application/vnd.mobius.txf",
+ ".txt" : "text/plain",
+ ".ufd" : "application/vnd.ufdl",
+ ".umj" : "application/vnd.umajin",
+ ".unityweb" : "application/vnd.unity",
+ ".uoml" : "application/vnd.uoml+xml",
+ ".uri" : "text/uri-list",
+ ".ustar" : "application/x-ustar",
+ ".utz" : "application/vnd.uiq.theme",
+ ".uu" : "text/x-uuencode",
+ ".vcd" : "application/x-cdlink",
+ ".vcf" : "text/x-vcard",
+ ".vcg" : "application/vnd.groove-vcard",
+ ".vcs" : "text/x-vcalendar",
+ ".vcx" : "application/vnd.vcx",
+ ".vis" : "application/vnd.visionary",
+ ".viv" : "video/vnd.vivo",
+ ".vrml" : "model/vrml",
+ ".vsd" : "application/vnd.visio",
+ ".vsf" : "application/vnd.vsf",
+ ".vtu" : "model/vnd.vtu",
+ ".vxml" : "application/voicexml+xml",
+ ".war" : "application/java-archive",
+ ".wav" : "audio/x-wav",
+ ".wax" : "audio/x-ms-wax",
+ ".wbmp" : "image/vnd.wap.wbmp",
+ ".wbs" : "application/vnd.criticaltools.wbs+xml",
+ ".wbxml" : "application/vnd.wap.wbxml",
+ ".webm" : "video/webm",
+ ".wm" : "video/x-ms-wm",
+ ".wma" : "audio/x-ms-wma",
+ ".wmd" : "application/x-ms-wmd",
+ ".wmf" : "application/x-msmetafile",
+ ".wml" : "text/vnd.wap.wml",
+ ".wmlc" : "application/vnd.wap.wmlc",
+ ".wmls" : "text/vnd.wap.wmlscript",
+ ".wmlsc" : "application/vnd.wap.wmlscriptc",
+ ".wmv" : "video/x-ms-wmv",
+ ".wmx" : "video/x-ms-wmx",
+ ".wmz" : "application/x-ms-wmz",
+ ".woff" : "application/octet-stream",
+ ".wpd" : "application/vnd.wordperfect",
+ ".wpl" : "application/vnd.ms-wpl",
+ ".wps" : "application/vnd.ms-works",
+ ".wqd" : "application/vnd.wqd",
+ ".wri" : "application/x-mswrite",
+ ".wrl" : "model/vrml",
+ ".wsdl" : "application/wsdl+xml",
+ ".wspolicy" : "application/wspolicy+xml",
+ ".wtb" : "application/vnd.webturbo",
+ ".wvx" : "video/x-ms-wvx",
+ ".x3d" : "application/vnd.hzn-3d-crossword",
+ ".xar" : "application/vnd.xara",
+ ".xbd" : "application/vnd.fujixerox.docuworks.binder",
+ ".xbm" : "image/x-xbitmap",
+ ".xdm" : "application/vnd.syncml.dm+xml",
+ ".xdp" : "application/vnd.adobe.xdp+xml",
+ ".xdw" : "application/vnd.fujixerox.docuworks",
+ ".xenc" : "application/xenc+xml",
+ ".xer" : "application/patch-ops-error+xml",
+ ".xfdf" : "application/vnd.adobe.xfdf",
+ ".xfdl" : "application/vnd.xfdl",
+ ".xhtml" : "application/xhtml+xml",
+ ".xif" : "image/vnd.xiff",
+ ".xls" : "application/vnd.ms-excel",
+ ".xml" : "application/xml",
+ ".xo" : "application/vnd.olpc-sugar",
+ ".xop" : "application/xop+xml",
+ ".xpm" : "image/x-xpixmap",
+ ".xpr" : "application/vnd.is-xpr",
+ ".xps" : "application/vnd.ms-xpsdocument",
+ ".xpw" : "application/vnd.intercon.formnet",
+ ".xsl" : "application/xml",
+ ".xslt" : "application/xslt+xml",
+ ".xsm" : "application/vnd.syncml+xml",
+ ".xspf" : "application/xspf+xml",
+ ".xul" : "application/vnd.mozilla.xul+xml",
+ ".xwd" : "image/x-xwindowdump",
+ ".xyz" : "chemical/x-xyz",
+ ".yaml" : "text/yaml",
+ ".yml" : "text/yaml",
+ ".zaz" : "application/vnd.zzazz.deck+xml",
+ ".zip" : "application/zip",
+ ".zmm" : "application/vnd.handheld-entertainment+xml",
+}
118 lib/link/mock.js
@@ -0,0 +1,118 @@
+var util = require("util"),
+ url = require("url"),
+ EventEmitter = require("events").EventEmitter,
+ link = require("./../link"),
+ lint = require("./lint");
+
+exports.empty = empty;
+exports.request = request;
+exports.envFor = envFor;
+exports.Stream = MockStream;
+
+function empty(env, callback) {
+ callback(empty.status, empty.headers, empty.body);
+}
+
+empty.status = 200;
+empty.headers = {"Content-Type": "text/plain", "Content-Length": "0"};
+empty.body = "";
+
+/**
+ * Calls the given +callback+ with the result of sending a mock request to the
+ * given +app+. Creates the environment to use from the given +uri+ and +opts+.
+ * Set opts.lint +true+ to wrap the +app+ in a lint middleware.
+ */
+function request(uri, opts, app, callback) {
+ uri = uri || {};
+ opts = opts || {};
+ app = app || exports.empty;
+ callback = callback || function (status, headers, body) {};
+
+ if (opts.lint) {
+ app = lint(app);
+ delete opts.lint;
+ }
+
+ app(envFor(uri, opts), callback);
+}
+
+/**
+ * A wrapper for +link.envFor+ that provides the following:
+ *
+ * - allows a String to be given as +uri+
+ * - allows a String to be provided as +opts.input+
+ */
+function envFor(uri, opts) {
+ uri = uri || {};
+ opts = opts || {};
+
+ if (typeof uri == "string") {
+ uri = url.parse(uri);
+ }
+
+ // Wrap String inputs in a MockStream.
+ var input;
+ if (opts.input) {
+ if (opts.input instanceof EventEmitter) {
+ input = opts.input;
+ } else if (typeof opts.input === "string") {
+ input = new MockStream(opts.input);
+ } else {
+ throw new Error("Input must be an EventEmitter or String");
+ }
+ } else {
+ input = new MockStream;
+ }
+
+ opts.input = input;
+
+ return link.envFor(uri, opts);
+}
+
+/**
+ * Imitates a stream that emits the given +string+.
+ */
+function MockStream(string) {
+ EventEmitter.call(this);
+
+ string = string || "";
+
+ this.encoding = null;
+ this.wait = false;
+
+ this.readable = true;
+ this.writable = false;
+
+ var buffer = new Buffer(string),
+ self = this;
+
+ process.nextTick(function () {
+ if (self.wait) {
+ process.nextTick(arguments.callee);
+ return;
+ }
+
+ if (self.encoding) {
+ self.emit("data", buffer.toString(self.encoding));
+ } else {
+ self.emit("data", buffer);
+ }
+
+ self.emit("end");
+ self.readable = false;
+ });
+}
+
+util.inherits(MockStream, EventEmitter);
+
+MockStream.prototype.setEncoding = function (encoding) {
+ this.encoding = encoding;
+}
+
+MockStream.prototype.pause = function () {
+ this.wait = true;
+}
+
+MockStream.prototype.resume = function () {
+ this.wait = false;
+}
529 lib/link/multipart.js
@@ -0,0 +1,529 @@
+var util = require("util"),
+ path = require("path"),
+ fs = require("fs"),
+ EventEmitter = require("events").EventEmitter;
+
+exports.Parser = Parser;
+exports.Part = Part;
+exports.File = File;
+
+// This parser is modified from the one in the node-formidable
+// project, MIT licensed.
+
+var s = 0,
+ S =
+ { START: s++,
+ START_BOUNDARY: s++,
+ HEADER_FIELD_START: s++,
+ HEADER_FIELD: s++,
+ HEADER_VALUE_START: s++,
+ HEADER_VALUE: s++,
+ HEADER_VALUE_ALMOST_DONE: s++,
+ HEADERS_ALMOST_DONE: s++,
+ PART_DATA_START: s++,
+ PART_DATA: s++,
+ PART_END: s++,
+ END: s++,
+ },
+
+ f = 1,
+ F =
+ { PART_BOUNDARY: f,
+ LAST_BOUNDARY: f *= 2,
+ },
+
+ LF = 10,
+ CR = 13,
+ SPACE = 32,
+ HYPHEN = 45,
+ COLON = 58,
+ A = 97, // lower-case "a"
+ Z = 122; // lower-case "z"
+
+for (var s in S) {
+ exports[s] = S[s];
+}
+
+function Parser(boundary, tmpdir, prefix) {
+ this.boundary = new Buffer(boundary.length + 4);
+ this.boundary.write("\r\n--", "ascii", 0);
+ this.boundary.write(boundary, "ascii", 4);
+
+ this.tmpdir = tmpdir || "/tmp";
+ this.prefix = prefix || "";
+
+ if (path.existsSync(this.tmpdir)) {
+ var stats = fs.statSync(this.tmpdir);
+ if (!stats.isDirectory()) {
+ throw new Error('"' + this.tmpdir + '" is not a directory')
+ }
+ } else {
+ throw new Error('Directory "' + this.tmpdir + '" does not exist');
+ }
+
+ this.lookBehind = new Buffer(this.boundary.length + 8);
+ this.state = S.START;
+
+ this.boundaryChars = {};
+ var i = this.boundary.length;
+ while (i) {
+ this.boundaryChars[this.boundary[--i]] = true;
+ }
+
+ this.index = null;
+ this.flags = 0;
+
+ this._done = false; // Are we done with writes?
+ this._flushing = 0; // How many files are still being flushed to disk?
+}
+
+Parser.prototype.tmpfile = function tmpfile() {
+ var name = this.prefix;
+
+ for (var i = 0; i < 32; ++i) {
+ name += Math.round(Math.random() * 16).toString(16);
+ }
+
+ return path.join(this.tmpdir, name);
+}
+
+Parser.prototype.write = function write(buffer) {
+ var self = this,
+ bufferLength = buffer.length,
+ prevIndex = this.index,
+ index = this.index,
+ state = this.state,
+ flags = this.flags,
+ lookBehind = this.lookBehind,
+ boundary = this.boundary,
+ boundaryChars = this.boundaryChars,
+ boundaryLength = boundary.length,
+ boundaryEnd = boundaryLength - 1,
+ c,
+ cl;
+
+ for (var i = 0; i < bufferLength; ++i) {
+ c = buffer[i];
+
+ switch (state) {
+ case S.START:
+ index = 0;
+ state = S.START_BOUNDARY;
+ // fall through
+ case S.START_BOUNDARY:
+ if (index == boundaryLength - 2) {
+ if (c != CR) {
+ return i;
+ }
+ index++;
+ break;
+ } else if (index == boundaryLength - 1) {
+ if (c != LF) {
+ return i;
+ }
+ index = 0;
+ this._callback("partBegin");
+ state = S.HEADER_FIELD_START;
+ break;
+ }
+
+ if (c != boundary[index + 2]) {
+ return i;
+ }
+ index++;
+ break;
+ case S.HEADER_FIELD_START:
+ state = S.HEADER_FIELD;
+ this._mark("headerName", i);
+ index = 0;
+ // fall through
+ case S.HEADER_FIELD:
+ if (c == CR) {
+ this._clear("headerName");
+ state = S.HEADERS_ALMOST_DONE;
+ break;
+ }
+
+ index++;
+ if (c == HYPHEN) {
+ break;
+ }
+
+ if (c == COLON) {
+ if (index == 1) {
+ // empty header field
+ return i;
+ }
+ this._dataCallback("headerName", buffer, true, i);
+ state = S.HEADER_VALUE_START;
+ break;
+ }
+
+ cl = c | 0x20; // lower-case
+ if (cl < A || cl > Z) {
+ return i;
+ }
+ break;
+ case S.HEADER_VALUE_START:
+ if (c == SPACE) {
+ break;
+ }
+ this._mark("headerValue", i);
+ state = S.HEADER_VALUE;
+ // fall through
+ case S.HEADER_VALUE:
+ if (c == CR) {
+ this._dataCallback("headerValue", buffer, true, i);
+ this._callback("headerEnd");
+ state = S.HEADER_VALUE_ALMOST_DONE;
+ }
+ break;
+ case S.HEADER_VALUE_ALMOST_DONE:
+ if (c != LF) {
+ return i;
+ }
+ state = S.HEADER_FIELD_START;
+ break;
+ case S.HEADERS_ALMOST_DONE:
+ if (c != LF) {
+ return i;
+ }
+ this._callback("headersEnd");
+ state = S.PART_DATA_START;
+ break;
+ case S.PART_DATA_START:
+ state = S.PART_DATA
+ this._mark("partData", i);
+ // fall through
+ case S.PART_DATA:
+ prevIndex = index;
+
+ if (index == 0) {
+ // boyer-moore derrived algorithm to safely skip non-boundary data
+ i += boundaryEnd;
+ while (i < bufferLength && !(buffer[i] in boundaryChars)) {
+ i += boundaryLength;
+ }
+ i -= boundaryEnd;
+ c = buffer[i];
+ }
+
+ if (index < boundaryLength) {
+ if (boundary[index] == c) {
+ if (index == 0) {
+ this._dataCallback("partData", buffer, true, i);
+ }
+ index++;
+ } else {
+ index = 0;
+ }
+ } else if (index == boundaryLength) {
+ index++;
+ if (c == CR) {
+ // CR = part boundary
+ flags |= F.PART_BOUNDARY;
+ } else if (c == HYPHEN) {
+ // HYPHEN = end boundary
+ flags |= F.LAST_BOUNDARY;
+ } else {
+ index = 0;
+ }
+ } else if (index - 1 == boundaryLength) {
+ if (flags & F.PART_BOUNDARY) {
+ index = 0;
+ if (c == LF) {
+ // unset the PART_BOUNDARY flag
+ flags &= ~F.PART_BOUNDARY;
+ this._callback("partEnd");
+ this._callback("partBegin");
+ state = S.HEADER_FIELD_START;
+ break;
+ }
+ } else if (flags & F.LAST_BOUNDARY) {
+ if (c == HYPHEN) {
+ this._callback("partEnd");
+ // this._callback("end");
+ state = S.END;
+ } else {
+ index = 0;
+ }
+ } else {
+ index = 0;
+ }
+ }
+
+ if (index > 0) {
+ // when matching a possible boundary, keep a lookBehind
+ // reference in case it turns out to be a false lead
+ lookBehind[index - 1] = c;
+ } else if (prevIndex > 0) {
+ // if our boundary turned out to be rubbish, the captured
+ // lookBehind belongs to partData
+ this._callback("partData", lookBehind, 0, prevIndex);
+ prevIndex = 0;
+ this._mark("partData", i);
+
+ // reconsider the current character even so it interrupted the
+ // sequence it could be the beginning of a new sequence
+ i--;
+ }
+
+ break;
+ case S.END:
+ break;
+ default:
+ return i;
+ }
+ }
+
+ this._dataCallback("headerName", buffer);
+ this._dataCallback("headerValue", buffer);
+ this._dataCallback("partData", buffer);
+
+ this.index = index;
+ this.state = state;
+ this.flags = flags;
+
+ return bufferLength;
+}
+
+Parser.prototype._mark = function mark(name, i) {
+ this[name + "Mark"] = i;
+}
+
+Parser.prototype._clear = function clear(name) {
+ delete this[name + "Mark"];
+}
+
+Parser.prototype._callback = function callback(name, buffer, start, end) {
+ if (start !== undefined && start === end) {
+ return;
+ }
+
+ var prop = "on" + name.substr(0, 1).toUpperCase() + name.substr(1);
+ if (prop in this) {
+ this[prop](buffer, start, end);
+ }
+}
+
+Parser.prototype._dataCallback = function dataCallback(name, buffer, clear, i) {
+ var prop = name + "Mark";
+ if (prop in this) {
+ if (!clear) {
+ this._callback(name, buffer, this[prop], buffer.length);
+ this[prop] = 0;
+ } else {
+ this._callback(name, buffer, this[prop], i);
+ delete this[prop];
+ }
+ }
+}
+
+Parser.prototype.onPartBegin = function onPartBegin() {
+ this._part = new Part;
+ this._headerName = "";
+ this._headerValue = "";
+}
+
+Parser.prototype.onHeaderName = function onHeaderName(buffer, start, end) {
+ this._headerName += buffer.toString("utf8", start, end);
+}
+
+Parser.prototype.onHeaderValue = function onHeaderValue(buffer, start, end) {
+ this._headerValue += buffer.toString("utf8", start, end);
+}
+
+Parser.prototype.onHeaderEnd = function onHeaderEnd() {
+ var headerName = this._headerName.toLowerCase();
+ this._part.headers[headerName] = this._headerValue;
+ this._headerName = "";
+ this._headerValue = "";
+}
+
+Parser.prototype.onHeadersEnd = function onHeadersEnd() {
+ this.onPart(this._part);
+}
+
+Parser.prototype.onPartData = function onPartData(buffer, start, end) {
+ this._part.write(buffer.slice(start, end));
+}
+
+Parser.prototype.onPartEnd = function onPartEnd() {
+ this._part.end();
+}
+
+Parser.prototype.end = function end() {
+ if (this.state !== S.END) {
+ throw new Error("Stream ended unexpectedly (state: " + this.state + ")");
+ }
+
+ this._done = true;
+ this._maybeEnd();
+}
+
+Parser.prototype.onPart = function onPart(part) {
+ var filename = part.filename,
+ self = this;
+
+ if (filename) {
+ this._flushing += 1;
+
+ var file = new File(this.tmpfile(), part.type, filename);
+
+ this.onFile(file);
+
+ file.on("end", function () {
+ self.onParam(part.name, file);
+ self._flushing -= 1;
+ self._maybeEnd();
+ });
+
+ part.on("data", function (buffer) {
+ file.write(buffer);
+ });
+
+ part.on("end", function () {
+ file.end();
+ });
+ } else {
+ var content = "";
+
+ part.on("data", function (buffer) {
+ content += buffer.toString("utf8");
+ });
+
+ part.on("end", function () {
+ self.onParam(part.name, content);
+ });
+ }
+}
+
+Parser.prototype._maybeEnd = function maybeEnd() {
+ // Make sure that we're both done with the input (i.e. Parser.end was
+ // called) AND that we're done flushing all files to disk.
+ if (!this._done || this._flushing) {
+ return;
+ }
+
+ this.onEnd();
+}
+
+Parser.prototype.onFile = function onFile(file) {}
+Parser.prototype.onParam = function onParam(name, value) {}
+Parser.prototype.onEnd = function onEnd() {}
+
+
+/**
+ * A container class for data pertaining to one part of a multipart message.
+ */
+function Part() {
+ this.headers = {};
+}
+
+util.inherits(Part, EventEmitter);
+
+/**
+ * Returns the name of this part.
+ */
+Part.prototype.__defineGetter__("name", function name() {
+ var disposition = this.headers["content-disposition"],
+ match;
+
+ if (disposition && (match = disposition.match(/name="([^"]+)"/i))) {
+ return match[1];
+ }
+
+ return this.headers["content-id"] || null;
+});
+
+/**
+ * Returns the filename of this part if it originated from a file upload.
+ */
+Part.prototype.__defineGetter__("filename", function filename() {
+ var disposition = this.headers["content-disposition"];
+
+ if (disposition) {
+ var match = disposition.match(/filename="([^;]*)"/i),
+ filename;
+
+ if (match) {
+ filename = decodeURIComponent(match[1].replace(/\\"/g, '"'));
+ } else {
+ // Match unquoted filename.
+ match = disposition.match(/filename=([^;]+)/i);
+ if (match) {
+ filename = decodeURIComponent(match[1]);
+ }
+ }
+
+ if (filename) {
+ // Take the last part of the filename. This handles full Windows
+ // paths given by IE (and possibly other dumb clients).
+ return filename.substr(filename.lastIndexOf("\\") + 1);
+ }
+ }
+
+ return null;
+});
+
+/**
+ * Returns the Content-Type of this part.
+ */
+Part.prototype.__defineGetter__("type", function type() {
+ return this.headers["content-type"] || null;
+});
+
+Part.prototype.write = function write(buffer) {
+ this.emit("data", buffer);
+}
+
+Part.prototype.end = function end() {
+ this.emit("end");
+}
+
+
+/**
+ * A container class for data pertaining to a file upload stored on disk.
+ * Constructor parameters are:
+ *
+ * - path The full path to the temporary file on disk
+ * - type The Content-Type of the file
+ * - name The name of the original file
+ */
+function File(path, type, name) {
+ this.path = path;
+ this.type = type;
+ this.name = name;
+ this.size = 0;
+}
+
+util.inherits(File, EventEmitter);
+
+/**
+ * Returns the media type of the file, which is the content type with any extra
+ * parameters stripped (e.g. "text/plain;charset=utf-8" becomes "text/plain").
+ */
+File.prototype.__defineGetter__("mediaType", function mediaType() {
+ return (this.type || "").split(/\s*[;,]\s*/)[0].toLowerCase();
+});
+
+File.prototype.write = function write(buffer) {
+ if (!this._writeStream) {
+ this._writeStream = fs.createWriteStream(this.path);
+ }
+
+ this.emit("write");
+
+ var self = this;
+ this._writeStream.write(buffer, function () {
+ self.size += buffer.length;
+ self.emit("progress", self.size);
+ });
+}
+
+File.prototype.end = function end() {
+ var self = this;
+ this._writeStream.end(function () {
+ self.emit("end");
+ });
+}
29 lib/link/querystring.js
@@ -0,0 +1,29 @@
+var qs = require("querystring");
+
+// TODO: Make this a streaming parser, instead of buffering.
+
+exports.Parser = Parser;
+
+function Parser(sep, eq) {
+ this.body = "";
+ this.sep = sep;
+ this.eq = eq;
+}
+
+Parser.prototype.write = function write(buffer) {
+ this.body += buffer.toString("ascii");
+ return buffer.length;
+}
+
+Parser.prototype.end = function end() {
+ var params = qs.parse(this.body, this.sep, this.eq);
+
+ for (var param in params) {
+ this.onParam(param, params[param]);
+ }
+
+ this.onEnd();
+}
+
+Parser.prototype.onParam = function onParam(name, value) {}
+Parser.prototype.onEnd = function onEnd() {}
451 lib/link/request.js
@@ -0,0 +1,451 @@
+var path = require("path"),
+ multipart = require("./multipart"),
+ querystring = require("./querystring"),
+ utils = require("./utils");
+
+module.exports = Request;
+
+/**
+ * Provides a convenient interface to the application environment and various
+ * properties of the request. This class is stateless, and directly modifies
+ * the +env+ provided in the constructor.
+ */
+function Request(env) {
+ this.env = env;
+}
+
+/**
+ * The set of form-data media types. Requests that indicate one of these media
+ * types were most likely made using an HTML form.
+ */
+Request.FORM_DATA_MEDIA_TYPES = [
+ "application/x-www-form-urlencoded",
+ "multipart/form-data"
+];
+
+/**
+ * The set of parseable media types. Requests that indicate one of these media
+ * types should contain multipart data.
+ */
+Request.PARSEABLE_DATA_MEDIA_TYPES = [
+ "multipart/related",
+ "multipart/mixed"
+];
+
+Request.prototype.__defineGetter__("protocolVersion", function version() {
+ return this.env.protocolVersion;
+});
+
+Request.prototype.__defineGetter__("method", function method() {
+ return this.env.requestMethod;
+});
+
+Request.prototype.__defineGetter__("scriptName", function scriptName() {
+ return this.env.scriptName;
+});
+
+Request.prototype.__defineSetter__("scriptName", function setScriptName(value) {
+ this.env.scriptName = value;
+});
+
+Request.prototype.__defineGetter__("pathInfo", function pathInfo() {
+ return this.env.pathInfo;
+});
+
+Request.prototype.__defineSetter__("pathInfo", function setPathInfo(value) {
+ this.env.pathInfo = value;
+});
+
+/**
+ * The media type (type/subtype) portion of the Content-Type header without any
+ * media type parameters. e.g., when Content-Type is "text/plain;charset=utf-8",
+ * the mediaType is "text/plain".
+ *
+ * For more information on the use of media types in HTTP, see:
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
+ */
+Request.prototype.__defineGetter__("mediaType", function mediaType() {
+ var contentType = this.env.contentType;
+ return contentType && contentType.split(/\s*[;,]\s*/)[0].toLowerCase();
+});
+
+Request.prototype.__defineGetter__("contentLength", function contentLength() {
+ return this.env.contentLength;
+});
+
+Request.prototype.__defineGetter__("contentType", function contentType() {
+ var type = this.env.contentType;
+ return (type && type !== "") ? type : null;
+});
+
+Request.prototype.__defineGetter__("userAgent", function userAgent() {
+ return this.env.httpUserAgent;
+});
+
+/**
+ * The URL scheme used in the request (i.e. "http" or "https").
+ */
+Request.prototype.__defineGetter__("scheme", function scheme() {
+ if (this.env.httpXForwardedSsl === "on") {
+ return "https";
+ } else if (this.env.httpXForwardedProto) {
+ return this.env.httpXForwardedProto.split(",")[0];
+ }
+
+ return this.env["link.urlScheme"];
+});
+
+/**
+ * Returns +true+ if this request was made over SSL, +false+ otherwise.
+ */
+Request.prototype.__defineGetter__("ssl", function ssl() {
+ return this.scheme === "https";
+});
+
+Request.prototype.__defineGetter__("hostWithPort", function hostWithPort() {
+ var forwarded = this.env.httpXForwardedHost;
+
+ if (forwarded) {
+ var parts = forwarded.split(/,\s?/);
+ return parts[parts.length - 1];
+ } else if (this.env.httpHost) {
+ return this.env.httpHost;
+ }
+
+ return this.env.serverName + ":" + this.env.serverPort;
+});
+
+/**
+ * Returns the name of the host used in this request.
+ */
+Request.prototype.__defineGetter__("host", function host() {
+ return this.hostWithPort.replace(/:\d+$/, "");
+});
+
+/**
+ * Returns the port as a Number.
+ */
+Request.prototype.__defineGetter__("port", function port() {
+ var port = this.hostWithPort.split(":")[1] || this.env.httpXForwardedPort;
+
+ if (port) {
+ return parseInt(port, 10);
+ } else if (this.ssl) {
+ return 443;
+ } else if (this.env.httpXForwardedHost) {
+ return 80;
+ }
+
+ return parseInt(this.env.serverPort, 10);
+});
+
+/**
+ * Returns a string representing the scheme, hostname, and port of the original
+ * request.
+ */
+Request.prototype.__defineGetter__("baseUrl", function baseUrl() {
+ var scheme = this.scheme;
+
+ var url = scheme + "://";
+ url += this.host;
+
+ if ((scheme === "https" && port !== 443) || (scheme === "http" && port !== 80)) {
+ url += ":" + String(this.port);
+ }
+
+ return url;
+});
+
+/**
+ * Attempts to reconstruct the original URL of this request.
+ */
+Request.prototype.__defineGetter__("url", function url() {
+ return this.baseUrl + this.fullPath;
+});
+
+/**
+ * The path of this request, without the query string.
+ */
+Request.prototype.__defineGetter__("path", function path() {
+ return this.scriptName + this.pathInfo;
+});
+
+/**
+ * The path of this request, including the query string.
+ */
+Request.prototype.__defineGetter__("fullPath", function fullPath() {
+ var queryString = this.queryString;
+
+ if (queryString === "") {
+ return this.path;
+ }
+
+ return this.path + "?" + queryString;
+});
+
+/**
+ * Is +true+ if this request was made via XMLHttpRequest, +false+ otherwise.
+ */
+Request.prototype.__defineGetter__("xhr", function xhr() {
+ return this.env.httpXRequestedWith === "XMLHttpRequest";
+});
+
+/**
+ * Returns the query string.
+ */
+Request.prototype.__defineGetter__("queryString", function queryString() {
+ return this.env.queryString;
+});
+
+/**
+ * Returns the prefix that is used for the names of file uploads.
+ */
+Request.prototype.__defineGetter__("uploadPrefix", function uploadPrefix() {
+ return this.env["link.request.uploadPrefix"] || "LinkUpload-";
+});
+
+/**
+ * Sets the prefix that is used for the names of file uploads.
+ */
+Request.prototype.__defineSetter__("uploadPrefix", function setUploadPrefix(value) {
+ this.en