Inital Commit
[oweals/finalsclub.git] / bruml / lib / paperboy / lib / paperboy.js
1 var
2   events = require('events'),
3   fs   = require('fs'),
4   url  = require('url'),
5   path = require('path');
6
7 exports.filepath = function (webroot, url) {
8   // Unescape URL to prevent security holes
9   url = decodeURIComponent(url);
10   // Append index.html if path ends with '/'
11   fp = path.normalize(path.join(webroot, (url.match(/\/$/)=='/')  ? url+'index.html' : url));
12   // Sanitize input, make sure people can't use .. to get above webroot
13   if (webroot[webroot.length - 1] !== '/') webroot += '/';
14   if (fp.substr(0, webroot.length) != webroot)
15     return(['Permission Denied', null]);
16   else
17     return([null, fp]);
18 };
19
20 exports.streamFile = function (filepath, headerFields, stat, res, req, emitter) {
21   var
22     emitter = new events.EventEmitter(),
23     extension = filepath.split('.').pop(),
24     contentType = exports.contentTypes[extension] || 'application/octet-stream',
25     charset = exports.charsets[contentType];
26
27   process.nextTick( function() {
28     if (charset)
29       contentType += '; charset=' + charset;
30     headerFields['Content-Type'] = contentType;
31
32     var etag = '"' + stat.ino + '-' + stat.size + '-' + Date.parse(stat.mtime) +'"';
33     headerFields['ETag'] = etag;
34      
35     var statCode;
36     //Check to see if we can send a 304 and skip the send
37     if(req.headers['if-none-match'] == etag){
38       statCode = 304;
39       headerFields['Content-Length'] = 0;
40     }else {
41       headerFields['Content-Length'] = stat.size;
42       statCode = 200;
43       if (headerFields['Expires'] != undefined) {
44         var expires = new Date;
45         expires.setTime(expires.getTime() + headerFields['Expires']);
46         headerFields['Expires'] = expires.toUTCString();
47       }
48     }
49     
50     res.writeHead(statCode, headerFields);
51     
52     //If we sent a 304, skip sending a body
53     if (statCode == 304 || req.method === 'HEAD') {
54       res.end();
55       emitter.emit("success", statCode);
56     }
57     else {
58       fs.createReadStream(filepath,{'flags': 'r', 'encoding': 
59                                     'binary', 'mode': 0666, 'bufferSize': 4 * 1024})
60         .addListener("data", function(chunk){
61           res.write(chunk, 'binary');
62         })
63         .addListener("end", function(){
64           emitter.emit("success", statCode);
65         })
66         .addListener("close",function() {
67           res.end();
68         })
69         .addListener("error", function (e) {
70           emitter.emit("error", 500, e);
71         });
72     }
73   });
74   return emitter;
75 };
76
77 exports.deliver = function (webroot, req, res) {
78   var
79     stream,
80     fpRes = exports.filepath(webroot, url.parse(req.url).pathname),
81     fpErr = fpRes[0],
82     filepath = fpRes[1],
83     beforeCallback,
84     afterCallback,
85     otherwiseCallback,
86     errorCallback,
87     headerFields = {},
88     addHeaderCallback,
89     delegate = {
90       error: function (callback) {
91         errorCallback = callback;
92         return delegate;
93       },
94       before: function (callback) {
95         beforeCallback = callback;
96         return delegate;
97       },
98       after: function (callback) {
99         afterCallback = callback;
100         return delegate;
101       },
102       otherwise: function (callback) {
103         otherwiseCallback = callback;
104         return delegate;
105       },
106       addHeader: function (name, value) {
107         headerFields[name] = value;
108         return delegate;
109       }
110     };
111     
112   process.nextTick(function() {
113     // Create default error and otherwise callbacks if none were given.
114     errorCallback = errorCallback || function(statCode) {
115       res.writeHead(statCode, {'Content-Type': 'text/html'});
116       res.end("<h1>HTTP " + statCode + "</h1>");
117     };
118     otherwiseCallback = otherwiseCallback || function() {
119       res.writeHead(404, {'Content-Type': 'text/html'});
120       res.end("<h1>HTTP 404 File not found</h1>");
121     };
122
123     //If file is in a directory outside of the webroot, deny the request
124     if (fpErr) {
125       statCode = 403;
126       if (beforeCallback)
127         beforeCallback();
128       errorCallback(403, 'Forbidden');
129     }
130     else {
131       fs.stat(filepath, function (err, stat) {
132         if( (err || !stat.isFile())) {
133           var exactErr = err || 'File not found';
134           if (beforeCallback)
135             beforeCallback();
136           if (otherwiseCallback)
137             otherwiseCallback(exactErr);
138         } else {
139           //The before callback can abort the transfer by returning false
140           var cancel = beforeCallback && (beforeCallback() === false);
141           if (cancel && otherwiseCallback) {
142             otherwiseCallback();
143           }
144           else {
145             stream = exports.streamFile(filepath, headerFields, stat, res, req)
146            
147             if(afterCallback){
148               stream.addListener("success", afterCallback);
149             } 
150             if(errorCallback){
151               stream.addListener("error", errorCallback);
152             }
153           }
154         }
155       });
156     }
157   });
158   
159   return delegate;
160 };
161
162 exports.contentTypes = {
163   "aiff": "audio/x-aiff",
164   "arj": "application/x-arj-compressed",
165   "asf": "video/x-ms-asf",
166   "asx": "video/x-ms-asx",
167   "au": "audio/ulaw",
168   "avi": "video/x-msvideo",
169   "bcpio": "application/x-bcpio",
170   "ccad": "application/clariscad",
171   "cod": "application/vnd.rim.cod",
172   "com": "application/x-msdos-program",
173   "cpio": "application/x-cpio",
174   "cpt": "application/mac-compactpro",
175   "csh": "application/x-csh",
176   "css": "text/css",
177   "deb": "application/x-debian-package",
178   "dl": "video/dl",
179   "doc": "application/msword",
180   "drw": "application/drafting",
181   "dvi": "application/x-dvi",
182   "dwg": "application/acad",
183   "dxf": "application/dxf",
184   "dxr": "application/x-director",
185   "etx": "text/x-setext",
186   "ez": "application/andrew-inset",
187   "fli": "video/x-fli",
188   "flv": "video/x-flv",
189   "gif": "image/gif",
190   "gl": "video/gl",
191   "gtar": "application/x-gtar",
192   "gz": "application/x-gzip",
193   "hdf": "application/x-hdf",
194   "hqx": "application/mac-binhex40",
195   "html": "text/html",
196   "ice": "x-conference/x-cooltalk",
197   "ief": "image/ief",
198   "igs": "model/iges",
199   "ips": "application/x-ipscript",
200   "ipx": "application/x-ipix",
201   "jad": "text/vnd.sun.j2me.app-descriptor",
202   "jar": "application/java-archive",
203   "jpeg": "image/jpeg",
204   "jpg": "image/jpeg",
205   "js": "text/javascript",
206   "json": "application/json",
207   "latex": "application/x-latex",
208   "lsp": "application/x-lisp",
209   "lzh": "application/octet-stream",
210   "m": "text/plain",
211   "m3u": "audio/x-mpegurl",
212   "man": "application/x-troff-man",
213   "me": "application/x-troff-me",
214   "midi": "audio/midi",
215   "mif": "application/x-mif",
216   "mime": "www/mime",
217   "movie": "video/x-sgi-movie",
218   "mp4": "video/mp4",
219   "mpg": "video/mpeg",
220   "mpga": "audio/mpeg",
221   "ms": "application/x-troff-ms",
222   "nc": "application/x-netcdf",
223   "oda": "application/oda",
224   "ogm": "application/ogg",
225   "pbm": "image/x-portable-bitmap",
226   "pdf": "application/pdf",
227   "pgm": "image/x-portable-graymap",
228   "pgn": "application/x-chess-pgn",
229   "pgp": "application/pgp",
230   "pm": "application/x-perl",
231   "png": "image/png",
232   "pnm": "image/x-portable-anymap",
233   "ppm": "image/x-portable-pixmap",
234   "ppz": "application/vnd.ms-powerpoint",
235   "pre": "application/x-freelance",
236   "prt": "application/pro_eng",
237   "ps": "application/postscript",
238   "qt": "video/quicktime",
239   "ra": "audio/x-realaudio",
240   "rar": "application/x-rar-compressed",
241   "ras": "image/x-cmu-raster",
242   "rgb": "image/x-rgb",
243   "rm": "audio/x-pn-realaudio",
244   "rpm": "audio/x-pn-realaudio-plugin",
245   "rtf": "text/rtf",
246   "rtx": "text/richtext",
247   "scm": "application/x-lotusscreencam",
248   "set": "application/set",
249   "sgml": "text/sgml",
250   "sh": "application/x-sh",
251   "shar": "application/x-shar",
252   "silo": "model/mesh",
253   "sit": "application/x-stuffit",
254   "skt": "application/x-koan",
255   "smil": "application/smil",
256   "snd": "audio/basic",
257   "sol": "application/solids",
258   "spl": "application/x-futuresplash",
259   "src": "application/x-wais-source",
260   "stl": "application/SLA",
261   "stp": "application/STEP",
262   "sv4cpio": "application/x-sv4cpio",
263   "sv4crc": "application/x-sv4crc",
264   "svg": "image/svg+xml",
265   "swf": "application/x-shockwave-flash",
266   "tar": "application/x-tar",
267   "tcl": "application/x-tcl",
268   "tex": "application/x-tex",
269   "texinfo": "application/x-texinfo",
270   "tgz": "application/x-tar-gz",
271   "tiff": "image/tiff",
272   "tr": "application/x-troff",
273   "tsi": "audio/TSP-audio",
274   "tsp": "application/dsptype",
275   "tsv": "text/tab-separated-values",
276   "txt": "text/plain",
277   "unv": "application/i-deas",
278   "ustar": "application/x-ustar",
279   "vcd": "application/x-cdlink",
280   "vda": "application/vda",
281   "vivo": "video/vnd.vivo",
282   "vrm": "x-world/x-vrml",
283   "wav": "audio/x-wav",
284   "wax": "audio/x-ms-wax",
285   "wma": "audio/x-ms-wma",
286   "wmv": "video/x-ms-wmv",
287   "wmx": "video/x-ms-wmx",
288   "wrl": "model/vrml",
289   "wvx": "video/x-ms-wvx",
290   "xbm": "image/x-xbitmap",
291   "xlw": "application/vnd.ms-excel",
292   "xml": "text/xml",
293   "xpm": "image/x-xpixmap",
294   "xwd": "image/x-xwindowdump",
295   "xyz": "chemical/x-pdb",
296   "zip": "application/zip"
297 };
298
299 exports.charsets = {
300   'text/javascript': 'UTF-8',
301   'text/html': 'UTF-8'
302 };