Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Adding support for directory listing and a setting for chosing index files. #10

Open
wants to merge 13 commits into from

2 participants

@Minos

I added those features three months ago. I’m going to write some test cases covering them (tomorrow, I promise :/), but until I commit them, I send you a pull request anyway.

If you have any critics, suggestions, insults, etc., please let me know :-)

@mhansen
Owner

Wow, this looks great! I'd love to merge it into antinode master. Would you be able to write a test case covering it? Love the explicit setting for 'index_files' - that shouldn't be hardcoded into antinode.

Ok, I fix some bugs, then I’ll try to write some test cases :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Aug 19, 2010
  1. Changing README according to the new features

    Simon Jacquin authored
Commits on Oct 5, 2010
  1. Merged updates

    Simon Jacquin authored
  2. Fixing bug with directory requests, making 404 errors show request’s …

    Simon Jacquin authored
    …url instead of the path on the server.
  3. ignore settings.json file

    Simon Jacquin authored
Commits on Nov 12, 2010
  1. Merge https://github.com/mhansen/antinode

    Simon Jacquin authored
  2. Execute sjs index files

    Simon Jacquin authored
Commits on Nov 21, 2010
  1. Fixing critical issue with index files.

    Simon Jacquin authored
Commits on Nov 22, 2010
  1. Cleaning the function for finding index files.

    Simon Jacquin authored
Commits on Jan 9, 2011
  1. Colored output.

    Simon Jacquin authored
This page is out of date. Refresh to see the latest.
View
1  .gitignore
@@ -1 +1,2 @@
*.swp
+settings.json
View
7 README.markdown
@@ -29,7 +29,8 @@ Example settings file:
},
"default_host" : {
"root" : "/www/default/"
- }
+ },
+ "index_files" : [ "index.xhtml", "index.html" ]
}
This server listens on port 8080 for HTTP requests.
@@ -44,6 +45,7 @@ Explanation of properties:
- `port` - the port to listen for HTTP connections on. default: 8080
- `hosts` - an object with one property name per virtual host address, with the value of a 'virtual host' object to
- `default_host` - the 'virtual host' object to default to if no other virtual hosts match, or the HTTP `Host` header is not given
+- `index_files` - if the request’s url is a directory, return the first index file from this list that has been found in the directory (if none was found, lists the directory).
'virtual host object' - has a property `root` giving the directory to serve
web requests from
@@ -59,7 +61,8 @@ E.g. an HTTP request for `/styles/site.css` will will look for the file `/var/ww
- HTTP `Date` header
- HTTP `Last-Modified` header
- Reads files in binary mode - so can serve images and other binary files (not just text)
-- Requests to any `directory` try to return `directory/index.html`
+- Requests to any `directory` try to return an index file (from the settings)
+- Directory listing
- Virtual Hosts
# Test Suite
View
100 lib/antinode.js
@@ -4,6 +4,7 @@ var http = require('http'),
uri = require('url'),
mime = require('./content-type'),
log = require('./log'),
+ xhtml_res = require('./xhtml_res'),
package = JSON.parse(fs.readFileSync(__dirname+'/../package.json', 'utf8')),
sys = require('sys'),
Script = process.binding('evals').Script;
@@ -15,7 +16,8 @@ exports.default_settings = {
"default_host" : {
"root" : process.cwd()
},
- "log_level" : log.levels.DEBUG
+ "log_level" : log.levels.DEBUG,
+ "index_files" : [ "index.html" ]
};
exports.log_levels = log.levels;
@@ -111,7 +113,10 @@ function map_request_to_local_file(req, resp) {
return;
}
}
- var path = pathlib.join(vhost.root, clean_pathname);
+ serve_file(pathlib.join(vhost.root, clean_pathname), req, resp);
+}
+
+function serve_file(path, req, resp) {
if (path.match(/\.sjs$/)) {
execute_sjs(path, req, resp);
} else {
@@ -147,7 +152,26 @@ function serve_static_file(path, req, resp) {
if (stats.isDirectory()) {
var parsed_url = uri.parse(req.url);
if (parsed_url.pathname.match(/\/$/)) {
- return serve_static_file(pathlib.join(path, "index.html"), req, resp);
+ // Search for an index file configured in settings.index_files.
+ // If none found, list the content of the directory.
+ fs.readdir(path, function (err, files) {
+ if (err) {
+ server_error(err);
+ return;
+ }
+ var found = false;
+ for (var i=0; i<settings.index_files.length && !found; i++)
+ for (var j=0; j<files.length && !found; j++)
+ if (files[j] === settings.index_files[i])
+ found = true;
+ i--;
+ if (found) {
+ serve_file(pathlib.join(path, settings.index_files[i]), req, resp);
+ }
+ else {
+ return list_directory(path, files);
+ }
+ });
}
else {
var redirect_host = req.headers.host ? ('http://' + req.headers['host']) : '';
@@ -155,7 +179,7 @@ function serve_static_file(path, req, resp) {
return redirect_302_found(redirect_host + redirect_path);
}
}
- if (!stats.isFile()) {
+ else if (!stats.isFile()) {
return file_not_found();
} else {
var if_modified_since = req.headers['if-modified-since'];
@@ -218,6 +242,63 @@ function serve_static_file(path, req, resp) {
});
}
+ function list_directory(dir, list) {
+ var files = { 'folders' : [], 'files' : [] }
+ var count = { 'folders' : 0, 'files' : 0 };
+ var pathname = uri.parse(req.url).pathname || '/';
+ pathname = decodeURIComponent(pathname);
+ list.sort(function(a, b) { return (a.toLowerCase() < b.toLowerCase()) ? -1 : 1 });
+ if (pathname != '/') {
+ files.folders[count.folders] = {
+ 'path' : pathlib.normalize(pathlib.join(pathname, '../')),
+ 'name' : 'Parent Directory',
+ 'mtime' : ''
+ };
+ count.folders++;
+ }
+ if (list.length > 0)
+ fs.stat(pathlib.join(dir, list[0]), add_file);
+ else {
+ resp.writeHead(200, {'Content-Type' : 'application/xhtml+xml'});
+ resp.end(xhtml_res.listdir(pathname, files));
+ }
+ function add_file (err, stats) {
+ if (err) {
+ server_error(err);
+ return;
+ }
+ if (stats.isDirectory()) {
+ files.folders[count.folders] = {
+ 'path' : pathlib.join(pathname, list[0]) + "/",
+ 'name' : list[0],
+ 'mtime' : stats.mtime.toUTCString(),
+ }
+ count.folders++;
+ }
+ else if (stats.isFile()) {
+ files.files[count.files] = {
+ 'path' : pathlib.join(pathname, list[0]),
+ 'name' : list[0],
+ 'size' : stats.size,
+ 'mtime' : stats.mtime.toUTCString(),
+ 'mime_type' : mime.mime_type(list[0])
+ }
+ count.files++;
+ }
+ list.shift();
+ if (list.length > 0)
+ fs.stat(pathlib.join(dir, list[0]), add_file);
+ else {
+ var body = xhtml_res.listdir(pathname, files);
+ send_headers(200, {
+ "Content-Length":Buffer.byteLength(body, 'utf8'),
+ "Content-Type":"application/xhtml+xml"
+ });
+ resp.end(body);
+ }
+ }
+ }
+
function not_modified() {
// no need to send content length or type
log.debug("304 for resource ", path);
@@ -227,10 +308,10 @@ function serve_static_file(path, req, resp) {
function file_not_found() {
log.debug("404 opening path: '"+path+"'");
- var body = "404: " + req.url + " not found.\n";
+ var body = xhtml_res.error(404, "Not Found", req.url+" was not found on this server");
send_headers(404,{
- 'Content-Length':body.length,
- 'Content-Type':"text/plain"
+ 'Content-Length':Buffer.byteLength(body, 'utf8'),
+ 'Content-Type':"application/xhtml+xml"
});
if (req.method != 'HEAD') {
resp.end(body, 'utf-8');
@@ -241,8 +322,9 @@ function serve_static_file(path, req, resp) {
function server_error(message) {
log.error(message);
+ var body = xhtml_res.error(500, "Internal Server Error", message);
send_headers(500, {
- 'Content-Length':message.length,
+ 'Content-Length':Buffer.byteLength(body, 'utf8'),
'Content-Type':"text/plain"
});
if (req.method !== 'HEAD') {
@@ -283,3 +365,5 @@ function close(fd) {
fs.close(fd);
log.debug("closed fd",fd);
}
+
+// vim:set expandtab shiftwidth=4 tabstop=4:
View
27 lib/content-type.js
@@ -46,7 +46,7 @@ var path = require('path'),
'.cer' : 'application/x-x509-ca-cert',
'.cgm' : 'image/cgm',
'.chm' : 'application/vnd.ms-htmlhelp',
- '.class' : 'application/octet-stream',
+ '.class' : 'application/x-java-applet',
'.clp' : 'application/x-msclip',
'.cmx' : 'image/x-cmx',
'.cod' : 'image/cis-cod',
@@ -133,10 +133,11 @@ var path = require('path'),
'.kar' : 'audio/midi',
'.latex' : 'application/x-latex',
'.lha' : 'application/octet-stream',
+ '.log' : 'text/plain',
'.lsf' : 'video/x-la-asf',
'.lsx' : 'video/x-la-asf',
+ '.ly' : 'text/plain',
'.lzh' : 'application/octet-stream',
- '.log' : 'text/plain',
'.m13' : 'application/x-msmediaview',
'.m14' : 'application/x-msmediaview',
'.m3u' : 'audio/x-mpegurl',
@@ -186,7 +187,10 @@ var path = require('path'),
'.odp' : 'application/vnd.oasis.opendocument.presentation',
'.ods' : 'application/vnd.oasis.opendocument.spreadsheet',
'.odt' : 'application/vnd.oasis.opendocument.text',
- '.ogg' : 'application/ogg',
+ '.oga' : 'audio/ogg',
+ '.ogg' : 'audio/ogg',
+ '.ogv' : 'video/ogg',
+ '.ogx' : 'application/ogg',
'.p' : 'text/x-pascal',
'.p10' : 'application/pkcs10',
'.p12' : 'application/x-pkcs12',
@@ -248,6 +252,7 @@ var path = require('path'),
'.rtf' : 'application/rtf',
'.rtx' : 'text/richtext',
'.ru' : 'text/x-script.ruby',
+ '.rv' : 'video/vnd.rn-realvideo',
'.s' : 'text/x-asm',
'.scd' : 'application/x-msschedule',
'.sct' : 'text/scriptlet',
@@ -257,6 +262,7 @@ var path = require('path'),
'.sgml' : 'text/sgml',
'.sh' : 'application/x-sh',
'.shar' : 'application/x-shar',
+ '.shtml' : 'text/html',
'.sig' : 'application/pgp-signature',
'.silo' : 'model/mesh',
'.sit' : 'application/x-stuffit',
@@ -270,6 +276,7 @@ var path = require('path'),
'.so' : 'application/octet-stream',
'.spc' : 'application/x-pkcs7-certificates',
'.spl' : 'application/x-futuresplash',
+ '.spx' : 'audio/ogg',
'.src' : 'application/x-wais-source',
'.sst' : 'application/vnd.ms-pkicertstore',
'.stl' : 'application/vnd.ms-pkistl',
@@ -304,23 +311,28 @@ var path = require('path'),
'.vxml' : 'application/voicexml+xml',
'.war' : 'application/java-archive',
'.wav' : 'audio/x-wav',
+ '.wax' : 'audio/x-ms-wax',
'.wbmp' : 'image/vnd.wap.wbmp',
'.wbxml' : 'application/vnd.wap.wbxml',
'.wcm' : 'application/vnd.ms-works',
'.wdb' : 'application/vnd.ms-works',
'.wks' : 'application/vnd.ms-works',
+ '.wm' : 'video/x-ms-wm',
'.wma' : 'audio/x-ms-wma',
+ '.wmd' : 'application/x-ms-wmd',
'.wmf' : 'application/x-msmetafile',
'.wml' : 'text/vnd.wap.wml',
'.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',
'.wps' : 'application/vnd.ms-works',
'.wri' : 'application/x-mswrite',
'.wrl' : 'model/vrml',
'.wrz' : 'x-world/x-vrml',
'.wsdl' : 'application/wsdl+xml',
+ '.wvx' : 'video/x-ms-wvx',
'.xaf' : 'x-world/x-vrml',
'.xbm' : 'image/x-xbitmap',
'.xht' : 'application/xhtml+xml',
@@ -330,8 +342,9 @@ var path = require('path'),
'.xlm' : 'application/vnd.ms-excel',
'.xls' : 'application/vnd.ms-excel',
'.xlt' : 'application/vnd.ms-excel',
- '.xml' : 'application/xml',
+ '.xml' : 'text/xml',
'.xof' : 'x-world/x-vrml',
+ '.xpi' : 'application/x-xpinstall',
'.xpm' : 'image/x-xpixmap',
'.xsl' : 'application/xml',
'.xslt' : 'application/xslt+xml',
@@ -341,11 +354,13 @@ var path = require('path'),
'.yaml' : 'text/yaml',
'.yml' : 'text/yaml',
'.z' : 'application/x-compress',
- '.zip' : 'application/zip'
+ '.zip' : 'application/zip',
};
function mime_type(pathname, default_type) {
- return ext_to_type[path.extname(pathname)] || default_type || 'application/octet-stream';
+ return ext_to_type[path.extname(pathname).toLowerCase()] || default_type || 'application/octet-stream';
}
exports.mime_type = mime_type;
+
+// vim:set expandtab shiftwidth=4 tabstop=4:
View
19 lib/log.js
@@ -2,11 +2,24 @@ var sys = require('sys');
function log(level, args) {
if (level >= exports.level) {
- sys.print((new Date()).toUTCString() + ": ");
+ sys.print("\033[1m" +(new Date()).toUTCString() + ": \033[0m");
+
+ switch (level) {
+ case 1:
+ sys.print("\033[32m"); //green
+ break;
+ case 2:
+ sys.print("\033[33m"); //yellow
+ break;
+ case 3:
+ sys.print("\033[31m"); //red
+ break;
+ }
// Convert arguments object to an array so we can use
// Array's join method on it
- sys.puts(Array.prototype.slice.call(args).join(' '));
+ sys.print(Array.prototype.slice.call(args).join(' '));
+ sys.puts("\033[0m");
}
}
@@ -31,3 +44,5 @@ exports.info = function() {
exports.error = function() {
log(levels.ERROR, arguments);
};
+
+// vim:set expandtab shiftwidth=4 tabstop=4:
View
74 lib/xhtml_res.js
@@ -0,0 +1,74 @@
+var header = '<?xml version="1.0" encoding="utf-8" ?>\n<!DOCTYPE html>\n';
+
+var namespace = {
+ 'begin' : '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">\n',
+ 'end' : '</html>\n'
+}
+
+var head = {
+ 'begin' : '<head>\n',
+ 'end' : '</head>\n',
+ 'title' : function(title) { return '<title>' + title + '</title>\n'; },
+ 'css' : function(style) { return '<style type="text/css">\n' + style + '</style>\n' }
+}
+
+var body = {
+ 'begin' : '<body>\n',
+ 'end' : '</body>\n'
+}
+
+exports.error = function (code, title, desc) {
+ return header + namespace.begin + head.begin
+ + head.title(code + ': ' + title) + head.end
+ + body.begin + '<h1>' + title + '</h1>\n'
+ + '<p>' + desc + '</p>\n'
+ + body.end + namespace.end;
+}
+
+/* List the content of a directory, lighttpd way. */
+/* TODO: Display human readable sizes and dates, cut too long file names. */
+exports.listdir = function (dir, files) {
+ var style = 'a, a:active {text-decoration: none; color: blue;}\n'
+ + 'a:visited {color: #48468F;}\n'
+ + 'a:hover, a:focus {text-decoration: underline; color: red;}\n'
+ + 'body {background-color: #F5F5F5;}\n'
+ + 'h2 {margin-bottom: 12px;}\n'
+ + 'table {margin-left: 12px; border-collapse: collapse;}\n'
+ + 'th, td { font: 90% monospace; text-align: left; padding: 0px;}\n'
+ + 'th { font-weight: bold; padding-right: 14px; padding-bottom: 3px;}\n'
+ + 'td {padding-right: 14px;}\n'
+ + 'td.s, th.s {text-align: right;}\n'
+ + 'div.list { background-color: white; border-top: 1px solid #646464; border-bottom: 1px solid #646464; padding-top: 10px; padding-bottom: 14px;}\n'
+ + 'div.foot { font: 90% monospace; color: #787878; padding-top: 4px;}\n';
+ var resp = header + namespace.begin + head.begin
+ + head.title('Index of ' + dir)
+ + head.css(style) + head.end
+ + body.begin + '<h2>Index of ' + dir + '</h2>\n'
+ + '<div class="list">\n'
+ + '<table>\n'
+ + '<thead><tr><th class="n">Name</th><th class="m">Last Modified</th><th class="s">Size</th><th class="t">Type</th></tr></thead>\n'
+ + '<tbody>\n';
+ for (var i=0; i<files.folders.length; i++) {
+ resp += '<tr>'
+ + '<td class="n"><a href="' + files.folders[i].path + '">' + files.folders[i].name + '</a>/</td>'
+ + '<td class="m">' + (files.folders[i].mtime || ' ') + '</td>'
+ + '<td class="s">-  </td>'
+ + '<td class="t">Directory</td>'
+ + '</tr>\n';
+ }
+ for (var i=0; i<files.files.length; i++) {
+ resp += '<tr>'
+ + '<td class="n"><a href="' + files.files[i].path + '">' + files.files[i].name + '</a></td>'
+ + '<td class="m">' + files.files[i].mtime + '</td>'
+ + '<td class="s">' + files.files[i].size + '</td>'
+ + '<td class="t">' + files.files[i].mime_type + '</td>'
+ + '</tr>\n';
+ }
+ resp += '</tbody>\n'
+ + '</table>\n'
+ + '</div>\n'
+ + '<div class="foot"></div>\n'
+ + body.end
+ + namespace.end;
+ return resp;
+}
View
2  runtests.js
@@ -1,3 +1,5 @@
+#!/usr/bin/env node
+
require.paths.push(__dirname);
var run = require('./lib/nodeunit/lib/nodeunit').testrunner.run;
run(['tests']); // test all js files in tests dir
View
3  settings-sample.json
@@ -15,6 +15,7 @@
"default_host" : {
"root" : "/home/Moon/www/default/"
- }
+ },
+ "index_files" : [ "index.xhtml", "index.html" ]
}
View
1  tests/common.js
@@ -24,6 +24,7 @@ exports.settings = {
"default_host" : {
"root": path.join(fixturesDir,"default-host")
},
+ "index_files" : [ "index.html" ],
/* turn this up to 'log_levels.DEBUG' when debugging failing tests */
"log_level": antinode.log_levels.ERROR
};
Something went wrong with that request. Please try again.