diff --git a/lib/middleware/static.js b/lib/middleware/static.js
index 9b10f43..3422bbc 100644
--- a/lib/middleware/static.js
+++ b/lib/middleware/static.js
@@ -13,11 +13,21 @@
* * `options`: an object with fine-grained configuration options
* ** `servePrecompressed`: if true (default), static resources with a pre-compressed gzip equivalent will be
* served instead of the original file.
+ * ** `maxAge`: set the `Cache-Control` header in seconds, default is 0.
+ * ** `lastModified`: if true (default), set the `Last-Modified` header to the modification date of the resource.
+ * ** `setHeaders`: allows a user to specify a function which returns additional headers to be set for a given path.
+ * The given function produces an object containing the header names as keys and takes the path as
+ * argument. User-provided headers take precedence over all other headers.
+ * ** `dotfiles`: determines how should files starting with a dot character be treated.
+ * `allow` (default) serves dotfiles, `deny` respond with status 403, `ignore` respond with status 404.
*
* You can call `static()` multiple times to register multiple resources to be served.
*/
+const RFC_822 = "EEE, dd MMM yyyy HH:mm:ss z";
+
var objects = require("ringo/utils/objects");
+var dates = require("ringo/utils/dates");
var response = require("ringo/jsgi/response");
var {mimeType} = require("ringo/mime");
@@ -34,7 +44,11 @@ exports.middleware = function static(next, app) {
app.static = function(base, index, baseURI, options) {
var opts = objects.merge(options || {}, {
- "servePrecompressed": true
+ "servePrecompressed": true,
+ "dotfiles": "allow",
+ "lastModified": true,
+ "maxAge": 0,
+ "setHeaders": null
});
var baseRepository;
if (typeof base === "string") {
@@ -63,18 +77,39 @@ exports.middleware = function static(next, app) {
if (path.length > 1) {
var resource = config.repository.getResource(path);
if (resource && resource.exists()) {
+ if (resource.getName().charAt(0) === ".") {
+ switch (config.options.dotfiles.toLowerCase()) {
+ case "deny": return response.text("403 Forbidden").setStatus(403);
+ case "ignore": return response.text("404 Not Found").setStatus(404);
+ }
+ }
+
+ let userHeaders = typeof config.options.setHeaders === "function" ? config.options.setHeaders() : {};
+
+ let defaultHeaders = {
+ "Cache-Control": "max-age=" + config.options.maxAge || 0
+ };
+
+ if (config.options.lastModified === true) {
+ defaultHeaders["Last-Modified"] = dates.format(new Date(resource.lastModified()), RFC_822, "en", "GMT");
+ }
+
// check if precompressed gzip resource is available and it's serving is enabled
let acceptEncoding = (request.headers["accept-encoding"] || "").toLowerCase();
if (acceptEncoding.indexOf("gzip") > -1 && config.options.servePrecompressed === true) {
let gzippedResource = config.repository.getResource(path + ".gz");
if (gzippedResource && gzippedResource.exists()) {
let jsgiResponse = response.static(gzippedResource, mimeType(path, "text/plain"));
- jsgiResponse.headers["Content-Encoding"] = "gzip";
+ jsgiResponse.headers = objects.merge(userHeaders, jsgiResponse.headers, {
+ "Content-Encoding": "gzip"
+ }, defaultHeaders);
return jsgiResponse;
}
}
- return response.static(resource, mimeType(path, "text/plain"));
+ let jsgiResponse = response.static(resource, mimeType(path, "text/plain"));
+ jsgiResponse.headers = objects.merge(userHeaders, jsgiResponse.headers, defaultHeaders);
+ return jsgiResponse;
}
}
}
diff --git a/test/middleware/fixtures/.DotfileExample b/test/middleware/fixtures/.DotfileExample
new file mode 100644
index 0000000..1f5b106
--- /dev/null
+++ b/test/middleware/fixtures/.DotfileExample
@@ -0,0 +1 @@
+This is a dotfile
\ No newline at end of file
diff --git a/test/middleware/static_test.js b/test/middleware/static_test.js
index 969880d..bac514d 100644
--- a/test/middleware/static_test.js
+++ b/test/middleware/static_test.js
@@ -1,3 +1,4 @@
+const fs = require("fs");
const system = require("system");
const assert = require("assert");
@@ -58,10 +59,174 @@ exports.testStaticFile = function() {
});
assert.equal(response.headers["Content-Type"], "text/html");
assert.equal(bodyAsString(response.body, "utf-8"), "");
+ assert.equal(response.headers["Cache-Control"], "max-age=0");
};
-exports.testPrecompressedStaticFile = function() {
+exports.testMaxAge = function() {
+ let app = new Application();
+
+ app.configure("static");
+ app.static(module.resolve("./fixtures"), "index.html", "/customStatic", {
+ maxAge: 1000
+ });
+
+ let response = app({
+ method: 'GET',
+ headers: {},
+ env: {},
+ pathInfo: '/customStatic/'
+ });
+ assert.equal(response.headers["Content-Type"], "text/html");
+ assert.equal(bodyAsString(response.body, "utf-8"), "");
+ assert.equal(response.headers["Cache-Control"], "max-age=1000");
+
+ app = new Application();
+
+ app.configure("static");
+ app.static(module.resolve("./fixtures"), "index.html", "/customStatic");
+
+ response = app({
+ method: 'GET',
+ headers: {},
+ env: {},
+ pathInfo: '/customStatic/'
+ });
+ assert.equal(response.headers["Content-Type"], "text/html");
+ assert.equal(bodyAsString(response.body, "utf-8"), "");
+ assert.equal(response.headers["Cache-Control"], "max-age=0");
+};
+
+exports.testLastModified = function() {
var app = new Application();
+ app.configure("static");
+ app.static(module.resolve("./fixtures"), "index.html", "/customStatic", {
+ lastModified: false
+ });
+
+ let response = app({
+ method: 'GET',
+ headers: {},
+ env: {},
+ pathInfo: '/customStatic/'
+ });
+ assert.equal(response.headers["Content-Type"], "text/html");
+ assert.equal(bodyAsString(response.body, "utf-8"), "");
+ assert.isUndefined(response.headers["Last-Modified"]);
+
+ app = new Application();
+ app.configure("static");
+ app.static(module.resolve("./fixtures"), "index.html", "/customStatic");
+
+ response = app({
+ method: 'GET',
+ headers: {},
+ env: {},
+ pathInfo: '/customStatic/'
+ });
+ assert.equal(response.headers["Content-Type"], "text/html");
+ assert.equal(bodyAsString(response.body, "utf-8"), "");
+ assert.isNotUndefined(response.headers["Last-Modified"]);
+
+ let lastModified = fs.lastModified(fs.join(module.resolve("./fixtures"), "index.html"));
+ let sdf = new java.text.SimpleDateFormat("E, d MMM yyyy HH:mm:ss z", java.util.Locale.US);
+ sdf.setTimeZone(java.util.TimeZone.getTimeZone("GMT"));
+ assert.equal(response.headers["Last-Modified"], sdf.format(lastModified));
+};
+
+exports.testSetHeaders = function() {
+ let app = new Application();
+
+ app.configure("static");
+ app.static(module.resolve("./fixtures"), "index.html", "/customStatic", {
+ setHeaders: function() {
+ return {
+ "x-foo": "bar",
+ "x-seestadt-cafe": "united in cycling",
+ "Cache-Control": "max-age=123456"
+ }
+ }
+ });
+
+ let response = app({
+ method: 'GET',
+ headers: {
+ "accept-encoding": "gzip"
+ },
+ env: {},
+ pathInfo: '/customStatic/'
+ });
+
+ assert.equal(response.status, 200);
+ assert.equal(response.headers["Content-Type"], "text/html");
+ assert.equal(response.headers["Content-Encoding"], "gzip");
+ assert.equal(response.headers["Cache-Control"], "max-age=123456");
+ assert.equal(response.headers["x-foo"], "bar");
+ assert.equal(response.headers["x-seestadt-cafe"], "united in cycling");
+};
+
+exports.testDotfiles = function() {
+ let app = new Application();
+
+ app.configure("static");
+ app.static(module.resolve("./fixtures"), "index.html", "/customStatic", {
+ "dotfiles": "allow"
+ });
+
+ let response = app({
+ method: 'GET',
+ headers: {
+ "accept-encoding": "gzip"
+ },
+ env: {},
+ pathInfo: '/customStatic/.DotfileExample'
+ });
+
+ assert.equal(response.status, 200);
+ assert.equal(response.headers["Content-Type"], "text/plain");
+
+ // DENY
+ app = new Application();
+
+ app.configure("static");
+ app.static(module.resolve("./fixtures"), "index.html", "/customStatic", {
+ "dotfiles": "deny"
+ });
+
+ response = app({
+ method: 'GET',
+ headers: {
+ "accept-encoding": "gzip"
+ },
+ env: {},
+ pathInfo: '/customStatic/.DotfileExample'
+ });
+
+ assert.equal(response.status, 403);
+ assert.equal(response.headers["content-type"], "text/plain; charset=utf-8");
+
+ // IGNORE
+ app = new Application();
+
+ app.configure("static");
+ app.static(module.resolve("./fixtures"), "index.html", "/customStatic", {
+ "dotfiles": "IgNoRe"
+ });
+
+ response = app({
+ method: 'GET',
+ headers: {
+ "accept-encoding": "gzip"
+ },
+ env: {},
+ pathInfo: '/customStatic/.DotfileExample'
+ });
+
+ assert.equal(response.status, 404);
+ assert.equal(response.headers["content-type"], "text/plain; charset=utf-8");
+};
+
+exports.testPrecompressedStaticFile = function() {
+ let app = new Application();
app.configure("static");
app.static(module.resolve("./fixtures"), "index.html", "/customStatic", {});
@@ -78,6 +243,7 @@ exports.testPrecompressedStaticFile = function() {
assert.equal(response.status, 200);
assert.equal(response.headers["Content-Type"], "text/html");
assert.equal(response.headers["Content-Encoding"], "gzip");
+ assert.equal(response.headers["Cache-Control"], "max-age=0");
response = app({
method: 'GET',
@@ -91,6 +257,7 @@ exports.testPrecompressedStaticFile = function() {
assert.equal(response.status, 200);
assert.equal(response.headers["Content-Type"], "text/html");
assert.equal(response.headers["Content-Encoding"], "gzip");
+ assert.equal(response.headers["Cache-Control"], "max-age=0");
const ba = new ByteArray([0x1F, 0x8B, 0x08, 0x08, 0x81, 0xF1, 0xD6, 0x56, 0x00, 0x03, 0x69, 0x6E, 0x64, 0x65, 0x78,
0x2E, 0x68, 0x74, 0x6D, 0x6C, 0x00, 0xB3, 0x51, 0x74, 0xF1, 0x77, 0x0E, 0x89, 0x0C, 0x70,
@@ -105,6 +272,7 @@ exports.testPrecompressedStaticFile = function() {
});
assert.equal(response.headers["Content-Type"], "text/html");
assert.equal(bodyAsString(response.body, "utf-8"), "");
+ assert.equal(response.headers["Cache-Control"], "max-age=0");
};
exports.testDeactivatedPrecompression = function() {
@@ -123,6 +291,7 @@ exports.testDeactivatedPrecompression = function() {
});
assert.equal(response.headers["Content-Type"], "text/html");
assert.equal(bodyAsString(response.body, "utf-8"), "");
+ assert.equal(response.headers["Cache-Control"], "max-age=0");
response = app({
method: 'GET',
@@ -132,6 +301,7 @@ exports.testDeactivatedPrecompression = function() {
});
assert.equal(response.headers["Content-Type"], "text/html");
assert.equal(bodyAsString(response.body, "utf-8"), "");
+ assert.equal(response.headers["Cache-Control"], "max-age=0");
};
if (require.main === module) {